@ryupold/vode 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/vode.ts CHANGED
@@ -1,694 +1,694 @@
1
- export type Vode<S> = FullVode<S> | JustTagVode | NoPropsVode<S>;
2
- export type ChildVode<S> = Vode<S> | TextVode | NoVode | Component<S>;
3
- export type FullVode<S> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
4
- export type NoPropsVode<S> = [tag: Tag, ...children: ChildVode<S>[]] | string[];
5
- export type JustTagVode = [tag: Tag];
6
- export type TextVode = string;
7
- export type NoVode = undefined | null | number | boolean | bigint | void;
8
- export type AttachedVode<S> = Vode<S> & { node: ChildNode, id?: string } | Text & { node?: never, id?: never };
9
- export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap);
10
- export type Component<S> = (s: S) => ChildVode<S>;
11
-
12
- export type Patch<S> =
13
- | NoRenderPatch // ignored
14
- | typeof EmptyPatch | DeepPartial<S> // render patches
15
- | Promise<Patch<S>> | Effect<S> // effects resulting in patches
16
-
17
- export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
18
- export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
19
-
20
- export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
21
-
22
- export type Effect<S> =
23
- | (() => Patch<S>)
24
- | EffectFunction<S>
25
- | [effect: EffectFunction<S>, ...args: any[]]
26
- | Generator<Patch<S>, unknown, void>
27
- | AsyncGenerator<Patch<S>, unknown, void>;
28
-
29
- export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
30
-
31
- export type Dispatch<S> = (action: Patch<S>) => void;
32
- export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
33
-
34
- export type Props<S> = Partial<
35
- Omit<HTMLElement,
36
- keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
37
- { [K in keyof EventsMap]: Patch<S> } // all on* events
38
- > & {
39
- [_: string]: unknown,
40
- class?: ClassProp,
41
- style?: StyleProp,
42
- /** called after the element was attached */
43
- onMount?: MountFunction<S>,
44
- /** called before the element is detached */
45
- onUnmount?: MountFunction<S>,
46
- };
47
-
48
- export type MountFunction<S> =
49
- | ((s: S, node: HTMLElement) => Patch<S>)
50
- | ((s: S, node: SVGSVGElement) => Patch<S>)
51
- | ((s: S, node: MathMLElement) => Patch<S>)
52
-
53
- export type ClassProp =
54
- | "" | false | null | undefined // no class
55
- | string // "class1 class2"
56
- | string[] // ["class1", "class2"]
57
- | Record<string, boolean | undefined | null> // { class1: true, class2: false }
58
-
59
- export type StyleProp = Record<number, never> & {
60
- [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
61
- }
62
-
63
- export type EventsMap =
64
- & { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
65
- & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
66
- & { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
67
- & { onsearch: Event }
68
-
69
- export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
70
-
71
- export type ContainerNode<S> = HTMLElement & {
72
- state: PatchableState<S>,
73
- vode: AttachedVode<S>,
74
- patch: Dispatch<S>,
75
- render: () => void,
76
- q: Patch<S>[]
77
- isRendering: boolean,
78
- stats: {
79
- patchCount: number,
80
- liveEffectCount: number,
81
- renderPatchCount: number,
82
- renderCount: number,
83
- renderTime: number,
84
- queueLengthBeforeRender: number,
85
- queueLengthAfterRender: number,
86
- },
87
- };
88
-
89
- /** type-safe way to create a vode. useful for type inference and autocompletion.
90
- *
91
- * overloads:
92
- * - just a tag: `vode("div") // => ["div"]`
93
- * - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
94
- * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
95
- * - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
96
- */
97
- export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
98
- if (Array.isArray(tag)) {
99
- return tag;
100
- }
101
- if (props) {
102
- return [tag, props as Props<S>, ...children];
103
- }
104
- return [tag, ...children];
105
- }
106
-
107
- /** pass an object whose type determines the initial state */
108
- export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
109
-
110
-
111
- /** for a type safe way to create a deeply partial patch object or effect */
112
- export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
113
-
114
- /**
115
- * create a vode app inside a container element
116
- * @param container will use this container as root and places the result of the dom function and further renderings in it
117
- * @param initialState @see createState
118
- * @param dom creates the initial dom from the state and is called on every render
119
- * @param initialPatches variadic list of patches that are applied after the first render
120
- * @returns a patch function that can be used to update the state
121
- */
122
- export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
123
- const root = container as ContainerNode<S>;
124
- root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
125
-
126
- Object.defineProperty(initialState, "patch", {
127
- enumerable: false, configurable: true,
128
- writable: false, value: async (action: Patch<S>) => {
129
- if (!action || (typeof action !== "function" && typeof action !== "object")) return;
130
- root.stats.patchCount++;
131
-
132
- if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
133
- const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
134
- root.stats.liveEffectCount++;
135
- try {
136
- let v = await generator.next();
137
- while (v.done === false) {
138
- root.stats.liveEffectCount++;
139
- try {
140
- root.patch!(v.value);
141
- v = await generator.next();
142
- } finally {
143
- root.stats.liveEffectCount--;
144
- }
145
- }
146
- root.patch!(v.value as Patch<S>);
147
- } finally {
148
- root.stats.liveEffectCount--;
149
- }
150
- } else if ((action as Promise<S>).then) {
151
- root.stats.liveEffectCount++;
152
- try {
153
- const nextState = await (action as Promise<S>);
154
- root.patch!(<Patch<S>>nextState);
155
- } finally {
156
- root.stats.liveEffectCount--;
157
- }
158
- } else if (Array.isArray(action)) {
159
- if (typeof action[0] === "function") {
160
- if (action.length > 1)
161
- root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
162
- else root.patch!(action[0](root.state!));
163
- } else {
164
- root.stats.patchCount--;
165
- }
166
- } else if (typeof action === "function") {
167
- root.patch!((<EffectFunction<S>>action)(root.state));
168
- } else {
169
- root.stats.renderPatchCount++;
170
- root.q!.push(<Patch<S>>action);
171
- if (!root.isRendering) root.render!();
172
- }
173
- }
174
- });
175
-
176
- Object.defineProperty(root, "render", {
177
- enumerable: false, configurable: true,
178
- writable: false, value: () => requestAnimationFrame(() => {
179
- if (root.isRendering || root.q!.length === 0) return;
180
- root.isRendering = true;
181
- const sw = Date.now();
182
- try {
183
- root.stats.queueLengthBeforeRender = root.q!.length;
184
-
185
- while (root.q!.length > 0) {
186
- const patch = root.q!.shift();
187
- if(patch === EmptyPatch) continue;
188
- mergeState(root.state, patch);
189
- }
190
- root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
191
- } finally {
192
- root.isRendering = false;
193
- root.stats.renderCount++;
194
- root.stats.renderTime = Date.now() - sw;
195
- root.stats.queueLengthAfterRender = root.q!.length;
196
- if (root.q!.length > 0) {
197
- root.render!();
198
- }
199
- }
200
- })
201
- });
202
-
203
- root.patch = (<PatchableState<S>>initialState).patch;
204
- root.state = <PatchableState<S>>initialState;
205
- root.q = [];
206
- const initialVode = dom(<S>initialState);
207
- root.vode = <AttachedVode<S>>initialVode;
208
- root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
209
-
210
- for (const effect of initialPatches) {
211
- root.patch!(effect);
212
- }
213
-
214
- return root.patch;
215
- }
216
-
217
- /** get properties of a vode, if there are any */
218
- export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
219
- if (Array.isArray(vode)
220
- && vode.length > 1
221
- && vode[1]
222
- && !Array.isArray(vode[1])
223
- ) {
224
- if (
225
- typeof vode[1] === "object"
226
- && (vode[1] as unknown as Node).nodeType !== Node.TEXT_NODE
227
- ) {
228
- return vode[1];
229
- }
230
- }
231
-
232
- return undefined;
233
- }
234
-
235
- export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
236
- if (!a) return b;
237
- if (!b) return a;
238
-
239
- if (typeof a === "string" && typeof b === "string") {
240
- const aSplit = a.split(" ");
241
- const bSplit = b.split(" ");
242
- const classSet = new Set([...aSplit, ...bSplit]);
243
- return Array.from(classSet).join(" ").trim();
244
- }
245
- else if (typeof a === "string" && Array.isArray(b)) {
246
- const classSet = new Set([...b, ...a.split(" ")]);
247
- return Array.from(classSet).join(" ").trim();
248
- }
249
- else if (Array.isArray(a) && typeof b === "string") {
250
- const classSet = new Set([...a, ...b.split(" ")]);
251
- return Array.from(classSet).join(" ").trim();
252
- }
253
- else if (Array.isArray(a) && Array.isArray(b)) {
254
- const classSet = new Set([...a, ...b]);
255
- return Array.from(classSet).join(" ").trim();
256
- }
257
- else if (typeof a === "string" && typeof b === "object") {
258
- return { [a]: true, ...b };
259
- }
260
- else if (typeof a === "object" && typeof b === "string") {
261
- return { ...a, [b]: true };
262
- }
263
- else if (typeof a === "object" && typeof b === "object") {
264
- return { ...a, ...b };
265
- } else if (typeof a === "object" && Array.isArray(b)) {
266
- const aa = { ...a };
267
- for (const item of b as string[]) {
268
- (<Record<string, boolean | null | undefined>>aa)[item] = true;
269
- }
270
- return aa;
271
- } else if (Array.isArray(a) && typeof b === "object") {
272
- const aa: Record<string, any> = {};
273
- for (const item of a as string[]) {
274
- aa[item] = true;
275
- }
276
- for (const bKey of (<Record<string, any>>b).keys) {
277
- aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
278
- }
279
- return b;
280
- }
281
-
282
- throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
283
- }
284
-
285
- export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
286
- if (!Array.isArray(vode)) return;
287
-
288
- if (vode.length > 1) {
289
- if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
290
- vode[1] = merge(vode[1], props);
291
- return;
292
- }
293
-
294
- if (childCount(vode) > 0) {
295
- (<FullVode<S>>vode).push(null);
296
- }
297
- for (let i = vode.length - 1; i > 0; i--) {
298
- if (i > 1) vode[i] = vode[i - 1];
299
- }
300
- vode[1] = props;
301
- } else {
302
- (<FullVode<S>>vode).push(props);
303
- }
304
- }
305
-
306
- /** get a slice of all children of a vode, if there are any */
307
- export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
308
- const start = childrenStart(vode);
309
- if (start > 0) {
310
- return (<Vode<S>>vode).slice(start) as Vode<S>[];
311
- }
312
-
313
- return undefined;
314
- }
315
-
316
- /** index in vode at which child-vodes start */
317
- export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
318
- return props(vode) ? 2 : 1;
319
- }
320
-
321
- /** html tag of the vode or #text if it is a text node */
322
- export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
323
- return !!v ? (Array.isArray(v)
324
- ? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
325
- ? "#text" : undefined) as Tag
326
- : undefined;
327
- }
328
-
329
- export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
330
-
331
- export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined {
332
- return vode[index + childrenStart(vode)] as ChildVode<S>;
333
- }
334
-
335
- /** merge multiple objects into one, applying from left to right
336
- * @param first object to merge
337
- * @returns merged object
338
- */
339
- export function merge(first?: any, ...p: any[]): any {
340
- first = mergeState({}, first);
341
- for (const pp of p) {
342
- if (!pp) continue;
343
- first = mergeState(first, pp);
344
- }
345
- return first!;
346
- }
347
-
348
- function classString(classProp: ClassProp): string {
349
- if (typeof classProp === "string") {
350
- return classProp;
351
- } else if (Array.isArray(classProp)) {
352
- return classProp.map(classString).join(" ");
353
- } else if (typeof classProp === "object") {
354
- return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
355
- } else {
356
- return "";
357
- }
358
- }
359
-
360
- function isNaturalVode(x: ChildVode<any>) {
361
- return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
362
- }
363
-
364
- function isTextVode(x: ChildVode<any>) {
365
- return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
366
- }
367
-
368
- function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
369
- if (typeof c === "function") {
370
- return unwrap(c(s), s);
371
- } else {
372
- return c;
373
- }
374
- }
375
-
376
- /** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
377
- export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
378
- (<any>componentOrProps).__memo = compare;
379
- return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
380
- }
381
-
382
- function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
383
- if (typeof present !== "function")
384
- return present;
385
-
386
- const presentMemo = present?.__memo;
387
- const pastMemo = past?.__memo;
388
-
389
- if (Array.isArray(presentMemo)
390
- && Array.isArray(pastMemo)
391
- && presentMemo.length === pastMemo.length
392
- ) {
393
- let same = true;
394
- for (let i = 0; i < presentMemo.length; i++) {
395
- if (presentMemo[i] !== pastMemo[i]) {
396
- same = false;
397
- break;
398
- }
399
- }
400
- if (same) return past;
401
- }
402
- const newRender = unwrap(present, state);
403
- if (typeof newRender === "object") {
404
- (<any>newRender).__memo = present?.__memo;
405
- }
406
- return newRender;
407
- }
408
-
409
- function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
410
- // unwrap component if it is memoized
411
- newVode = remember(state, newVode, oldVode) as ChildVode<S>;
412
-
413
- const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
414
- if (newVode === oldVode || (!oldVode && isNoVode)) {
415
- return oldVode;
416
- }
417
-
418
- const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
419
- const oldNode: ChildNode | undefined = oldIsText ? oldVode as Text : oldVode?.node;
420
-
421
- // falsy|text|element(A) -> undefined
422
- if (isNoVode) {
423
- (<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
424
- oldNode?.remove();
425
- return undefined;
426
- }
427
-
428
- const isText = !isNoVode && isTextVode(newVode);
429
- const isNode = !isNoVode && isNaturalVode(newVode);
430
- const alreadyAttached = !!newVode && typeof newVode !== "string" && !!((<any>newVode)?.node || (<any>newVode)?.nodeType === Node.TEXT_NODE);
431
-
432
- if (!isText && !isNode && !alreadyAttached && !oldVode) {
433
- throw new Error("Invalid vode: " + typeof newVode + " " + JSON.stringify(newVode));
434
- }
435
- else if (alreadyAttached && isText) {
436
- newVode = (<Text><any>newVode).wholeText;
437
- }
438
- else if (alreadyAttached && isNode) {
439
- newVode = [...<Vode<S>>newVode];
440
- }
441
-
442
- // text -> text
443
- if (oldIsText && isText) {
444
- if ((<Text>oldNode).nodeValue !== <string>newVode) {
445
- (<Text>oldNode).nodeValue = <string>newVode;
446
- }
447
- return oldVode;
448
- }
449
- // falsy|element -> text
450
- if (isText && (!oldNode || !oldIsText)) {
451
- const text = document.createTextNode(newVode as string)
452
- if (oldNode) {
453
- (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
454
- oldNode.replaceWith(text);
455
- } else {
456
- if (parent.childNodes[childIndex]) {
457
- parent.insertBefore(text, parent.childNodes[childIndex]);
458
- } else {
459
- parent.appendChild(text);
460
- }
461
- }
462
-
463
- return text as Text;
464
- }
465
-
466
- // falsy|text|element(A) -> element(B)
467
- if (
468
- (isNode && (!oldNode || oldIsText || (<Vode<S>>oldVode)[0] !== (<Vode<S>>newVode)[0]))
469
- ) {
470
- svg = svg || (<Vode<S>>newVode)[0] === "svg";
471
- const newNode: ChildNode = svg
472
- ? document.createElementNS("http://www.w3.org/2000/svg", (<Vode<S>>newVode)[0])
473
- : document.createElement((<Vode<S>>newVode)[0]);
474
- (<AttachedVode<S>>newVode).node = newNode;
475
-
476
- const newvode = <Vode<S>>newVode;
477
- if (1 in newvode) {
478
- newvode[1] = remember(state, newvode[1], undefined) as Vode<S>;
479
- }
480
-
481
- const properties = props(newVode);
482
- patchProperties(patch, newNode, undefined, properties, svg);
483
-
484
- if (oldNode) {
485
- (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
486
- oldNode.replaceWith(newNode);
487
- } else {
488
- if (parent.childNodes[childIndex]) {
489
- parent.insertBefore(newNode, parent.childNodes[childIndex]);
490
- } else {
491
- parent.appendChild(newNode);
492
- }
493
- }
494
-
495
- const newChildren = children(newVode);
496
- if (newChildren) {
497
- for (let i = 0; i < newChildren.length; i++) {
498
- const child = newChildren[i];
499
- const attached = render(state, patch, newNode, i, undefined, child, svg);
500
- (<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
501
- }
502
- }
503
-
504
- (<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
505
- return <AttachedVode<S>>newVode;
506
- }
507
-
508
- //element(A) -> element(A)
509
- if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
510
- svg = svg || (<Vode<S>>newVode)[0] === "svg";
511
- (<AttachedVode<S>>newVode).node = oldNode;
512
-
513
- const newvode = <Vode<S>>newVode;
514
- const oldvode = <Vode<S>>oldVode;
515
-
516
- let hasProps = false;
517
- if ((<any>newvode[1])?.__memo) {
518
- const prev = newvode[1] as any;
519
- newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
520
- if (prev !== newvode[1]) {
521
- const properties = props(newVode);
522
- patchProperties(patch, oldNode!, props(oldVode), properties, svg);
523
- hasProps = !!properties;
524
- }
525
- }
526
- else {
527
- const properties = props(newVode);
528
- patchProperties(patch, oldNode!, props(oldVode), properties, svg);
529
- hasProps = !!properties;
530
- }
531
-
532
- const newKids = children(newVode);
533
- const oldKids = children(oldVode) as AttachedVode<S>[];
534
- if (newKids) {
535
- for (let i = 0; i < newKids.length; i++) {
536
- const child = newKids[i];
537
- const oldChild = oldKids && oldKids[i];
538
-
539
- const attached = render(state, patch, oldNode!, i, oldChild, child, svg);
540
- if (attached) {
541
- (<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
542
- }
543
- }
544
- for (let i = newKids.length; oldKids && i < oldKids.length; i++) {
545
- if (oldKids[i]?.node)
546
- oldKids[i].node!.remove();
547
- else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
548
- (oldKids[i] as Text).remove();
549
- }
550
- }
551
-
552
- for (let i = newKids?.length || 0; i < oldKids?.length || 0; i++) {
553
- if (oldKids[i]?.node)
554
- oldKids[i].node!.remove();
555
- else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
556
- (oldKids[i] as Text).remove();
557
- }
558
- return <AttachedVode<S>>newVode;
559
- }
560
-
561
- return undefined;
562
- }
563
-
564
- function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
565
- if (!newProps && !oldProps) return;
566
- if (!oldProps) { // set new props
567
- for (const key in newProps) {
568
- const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
569
- newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, undefined, newValue, isSvg);
570
- }
571
- } else if (newProps) { // clear old props and set new in one loop
572
- const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
573
- for (const key of combinedKeys) {
574
- const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
575
- const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
576
- if (key[0] === "o" && key[1] === "n") {
577
- const oldEvent = (<any>node)["__" + key];
578
- if ((oldEvent && oldEvent !== newValue) || (!oldEvent && oldValue !== newValue)) {
579
- newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
580
- }
581
- (<any>node)["__" + key] = newValue;
582
- }
583
- else if (oldValue !== newValue) {
584
- newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
585
- }
586
- }
587
- } else { //delete all old props, cause there are no new props
588
- for (const key in oldProps) {
589
- const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
590
- oldProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, undefined, isSvg);
591
- }
592
- }
593
- }
594
-
595
- function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>, isSvg?: boolean) {
596
- if (key === "style") {
597
- if (!newValue) {
598
- (node as HTMLElement).style.cssText = "";
599
- } else if (oldValue) {
600
- for (let k in { ...(oldValue as Props<S>), ...(newValue as Props<S>) }) {
601
- if (!oldValue || newValue[k as keyof PropertyValue<S>] !== oldValue[k as keyof PropertyValue<S>]) {
602
- (node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
603
- }
604
- else if (oldValue[k as keyof PropertyValue<S>] && !newValue[k as keyof PropertyValue<S>]) {
605
- (<any>(node as HTMLElement).style)[k as keyof PropertyValue<S>] = undefined;
606
- }
607
- }
608
- } else {
609
- for (let k in (newValue as Props<S>)) {
610
- (node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
611
- }
612
- }
613
- } else if (key === "class") {
614
- if (isSvg) {
615
- if (newValue) {
616
- const newClass = classString(newValue as ClassProp);
617
- (<SVGSVGElement>node).classList.value = newClass;
618
- } else {
619
- (<SVGSVGElement>node).classList.value = '';
620
- }
621
- } else {
622
- if (newValue) {
623
- const newClass = classString(newValue as ClassProp);
624
- (<HTMLElement>node).className = newClass;
625
- } else {
626
- (<HTMLElement>node).className = '';
627
- }
628
- }
629
- } else if (key[0] === "o" && key[1] === "n") {
630
- if (newValue) {
631
- let eventHandler: Function | null = null;
632
- if (typeof newValue === "function") {
633
- const action = newValue as EffectFunction<S>;
634
- eventHandler = (evt: Event) => patch([action, evt]);
635
- } else if (Array.isArray(newValue)) {
636
- const arr = (newValue as Array<any>);
637
- const action = newValue[0] as EffectFunction<S>;
638
- if (arr.length > 1) {
639
- eventHandler = () => patch([action, ...arr.slice(1)]);
640
- }
641
- else {
642
- eventHandler = (evt: Event) => patch([action, evt]);
643
- }
644
- } else if (typeof newValue === "object") {
645
- eventHandler = () => patch(newValue as Patch<S>);
646
- }
647
-
648
- (<any>node)[key] = eventHandler;
649
- } else {
650
- (<any>node)[key] = null;
651
- }
652
- } else if (newValue !== null && newValue !== undefined && newValue !== false) {
653
- (<HTMLElement>node).setAttribute(key, <string>newValue);
654
- } else {
655
- (<HTMLElement>node).removeAttribute(key);
656
- }
657
-
658
- return newValue;
659
- }
660
-
661
- function mergeState(target: any, source: any) {
662
- if (!source) return target;
663
-
664
- for (const key in source) {
665
- const value = source[key];
666
- if (value && typeof value === "object") {
667
- const targetValue = target[key];
668
- if (targetValue) {
669
- if (Array.isArray(value)) {
670
- target[key] = [...value];
671
- } else if (value instanceof Date && targetValue !== value) {
672
- target[key] = new Date(value);
673
- } else {
674
- if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
675
- else if (typeof targetValue === "object") mergeState(target[key], value);
676
- else target[key] = mergeState({}, value);
677
- }
678
- } else if (Array.isArray(value)) {
679
- target[key] = [...value];
680
- } else if (value instanceof Date) {
681
- target[key] = new Date(value);
682
- } else {
683
- target[key] = mergeState({}, value);
684
- }
685
- }
686
- else if (value === undefined) {
687
- delete target[key];
688
- }
689
- else {
690
- target[key] = value;
691
- }
692
- }
693
- return target;
1
+ export type Vode<S> = FullVode<S> | JustTagVode | NoPropsVode<S>;
2
+ export type ChildVode<S> = Vode<S> | TextVode | NoVode | Component<S>;
3
+ export type FullVode<S> = [tag: Tag, props: Props<S>, ...children: ChildVode<S>[]];
4
+ export type NoPropsVode<S> = [tag: Tag, ...children: ChildVode<S>[]] | string[];
5
+ export type JustTagVode = [tag: Tag];
6
+ export type TextVode = string;
7
+ export type NoVode = undefined | null | number | boolean | bigint | void;
8
+ export type AttachedVode<S> = Vode<S> & { node: ChildNode, id?: string } | Text & { node?: never, id?: never };
9
+ export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap);
10
+ export type Component<S> = (s: S) => ChildVode<S>;
11
+
12
+ export type Patch<S> =
13
+ | NoRenderPatch // ignored
14
+ | typeof EmptyPatch | DeepPartial<S> // render patches
15
+ | Promise<Patch<S>> | Effect<S> // effects resulting in patches
16
+
17
+ export const EmptyPatch = {} as const; // smallest patch to cause a render without any changes
18
+ export type NoRenderPatch = undefined | null | number | boolean | bigint | string | symbol | void;
19
+
20
+ export type DeepPartial<S> = { [P in keyof S]?: S[P] extends Array<infer I> ? Array<Patch<I>> : Patch<S[P]> };
21
+
22
+ export type Effect<S> =
23
+ | (() => Patch<S>)
24
+ | EffectFunction<S>
25
+ | [effect: EffectFunction<S>, ...args: any[]]
26
+ | Generator<Patch<S>, unknown, void>
27
+ | AsyncGenerator<Patch<S>, unknown, void>;
28
+
29
+ export type EffectFunction<S> = (state: S, ...args: any[]) => Patch<S>;
30
+
31
+ export type Dispatch<S> = (action: Patch<S>) => void;
32
+ export type PatchableState<S> = S & { patch: Dispatch<Patch<S>> };
33
+
34
+ export type Props<S> = Partial<
35
+ Omit<HTMLElement,
36
+ keyof (DocumentFragment & ElementCSSInlineStyle & GlobalEventHandlers)> &
37
+ { [K in keyof EventsMap]: Patch<S> } // all on* events
38
+ > & {
39
+ [_: string]: unknown,
40
+ class?: ClassProp,
41
+ style?: StyleProp,
42
+ /** called after the element was attached */
43
+ onMount?: MountFunction<S>,
44
+ /** called before the element is detached */
45
+ onUnmount?: MountFunction<S>,
46
+ };
47
+
48
+ export type MountFunction<S> =
49
+ | ((s: S, node: HTMLElement) => Patch<S>)
50
+ | ((s: S, node: SVGSVGElement) => Patch<S>)
51
+ | ((s: S, node: MathMLElement) => Patch<S>)
52
+
53
+ export type ClassProp =
54
+ | "" | false | null | undefined // no class
55
+ | string // "class1 class2"
56
+ | string[] // ["class1", "class2"]
57
+ | Record<string, boolean | undefined | null> // { class1: true, class2: false }
58
+
59
+ export type StyleProp = Record<number, never> & {
60
+ [K in keyof CSSStyleDeclaration]?: CSSStyleDeclaration[K] | null
61
+ }
62
+
63
+ export type EventsMap =
64
+ & { [K in keyof HTMLElementEventMap as `on${K}`]: HTMLElementEventMap[K] }
65
+ & { [K in keyof WindowEventMap as `on${K}`]: WindowEventMap[K] }
66
+ & { [K in keyof SVGElementEventMap as `on${K}`]: SVGElementEventMap[K] }
67
+ & { onsearch: Event }
68
+
69
+ export type PropertyValue<S> = string | boolean | null | undefined | StyleProp | ClassProp | Patch<S> | void;
70
+
71
+ export type ContainerNode<S> = HTMLElement & {
72
+ state: PatchableState<S>,
73
+ vode: AttachedVode<S>,
74
+ patch: Dispatch<S>,
75
+ render: () => void,
76
+ q: Patch<S>[]
77
+ isRendering: boolean,
78
+ stats: {
79
+ patchCount: number,
80
+ liveEffectCount: number,
81
+ renderPatchCount: number,
82
+ renderCount: number,
83
+ renderTime: number,
84
+ queueLengthBeforeRender: number,
85
+ queueLengthAfterRender: number,
86
+ },
87
+ };
88
+
89
+ /** type-safe way to create a vode. useful for type inference and autocompletion.
90
+ *
91
+ * overloads:
92
+ * - just a tag: `vode("div") // => ["div"]`
93
+ * - tag and props: `vode("div", { class: "foo" }) // => ["div", { class: "foo" }]`
94
+ * - tag, props and children: `vode("div", { class: "foo" }, ["span", "bar"]) // => ["div", { class: "foo" }, ["span", "bar"]]`
95
+ * - identity: `vode(["div", ["span", "bar"]]) // => ["div", ["span", "bar"]]`
96
+ */
97
+ export function vode<S extends object | unknown>(tag: Tag | Vode<S>, props?: Props<S> | ChildVode<S>, ...children: ChildVode<S>[]): Vode<S> {
98
+ if (Array.isArray(tag)) {
99
+ return tag;
100
+ }
101
+ if (props) {
102
+ return [tag, props as Props<S>, ...children];
103
+ }
104
+ return [tag, ...children];
105
+ }
106
+
107
+ /** pass an object whose type determines the initial state */
108
+ export function createState<S>(state: S): PatchableState<S> { return state as PatchableState<S>; }
109
+
110
+
111
+ /** for a type safe way to create a deeply partial patch object or effect */
112
+ export function patch<S>(p: DeepPartial<S> | Effect<S> | NoRenderPatch): typeof p { return p; }
113
+
114
+ /**
115
+ * create a vode app inside a container element
116
+ * @param container will use this container as root and places the result of the dom function and further renderings in it
117
+ * @param initialState @see createState
118
+ * @param dom creates the initial dom from the state and is called on every render
119
+ * @param initialPatches variadic list of patches that are applied after the first render
120
+ * @returns a patch function that can be used to update the state
121
+ */
122
+ export function app<S>(container: HTMLElement, initialState: Omit<S, "patch">, dom: Component<S>, ...initialPatches: Patch<S>[]) {
123
+ const root = container as ContainerNode<S>;
124
+ root.stats = { renderTime: 0, renderCount: 0, queueLengthBeforeRender: 0, queueLengthAfterRender: 0, liveEffectCount: 0, patchCount: 0, renderPatchCount: 0 };
125
+
126
+ Object.defineProperty(initialState, "patch", {
127
+ enumerable: false, configurable: true,
128
+ writable: false, value: async (action: Patch<S>) => {
129
+ if (!action || (typeof action !== "function" && typeof action !== "object")) return;
130
+ root.stats.patchCount++;
131
+
132
+ if ((action as AsyncGenerator<Patch<S>, unknown, void>)?.next) {
133
+ const generator = action as AsyncGenerator<Patch<S>, unknown, void>;
134
+ root.stats.liveEffectCount++;
135
+ try {
136
+ let v = await generator.next();
137
+ while (v.done === false) {
138
+ root.stats.liveEffectCount++;
139
+ try {
140
+ root.patch!(v.value);
141
+ v = await generator.next();
142
+ } finally {
143
+ root.stats.liveEffectCount--;
144
+ }
145
+ }
146
+ root.patch!(v.value as Patch<S>);
147
+ } finally {
148
+ root.stats.liveEffectCount--;
149
+ }
150
+ } else if ((action as Promise<S>).then) {
151
+ root.stats.liveEffectCount++;
152
+ try {
153
+ const nextState = await (action as Promise<S>);
154
+ root.patch!(<Patch<S>>nextState);
155
+ } finally {
156
+ root.stats.liveEffectCount--;
157
+ }
158
+ } else if (Array.isArray(action)) {
159
+ if (typeof action[0] === "function") {
160
+ if (action.length > 1)
161
+ root.patch!(action[0](root.state!, ...(action as any[]).slice(1)));
162
+ else root.patch!(action[0](root.state!));
163
+ } else {
164
+ root.stats.patchCount--;
165
+ }
166
+ } else if (typeof action === "function") {
167
+ root.patch!((<EffectFunction<S>>action)(root.state));
168
+ } else {
169
+ root.stats.renderPatchCount++;
170
+ root.q!.push(<Patch<S>>action);
171
+ if (!root.isRendering) root.render!();
172
+ }
173
+ }
174
+ });
175
+
176
+ Object.defineProperty(root, "render", {
177
+ enumerable: false, configurable: true,
178
+ writable: false, value: () => requestAnimationFrame(() => {
179
+ if (root.isRendering || root.q!.length === 0) return;
180
+ root.isRendering = true;
181
+ const sw = Date.now();
182
+ try {
183
+ root.stats.queueLengthBeforeRender = root.q!.length;
184
+
185
+ while (root.q!.length > 0) {
186
+ const patch = root.q!.shift();
187
+ if(patch === EmptyPatch) continue;
188
+ mergeState(root.state, patch);
189
+ }
190
+ root.vode = render(root.state, root.patch, container, 0, root.vode, dom(root.state))!;
191
+ } finally {
192
+ root.isRendering = false;
193
+ root.stats.renderCount++;
194
+ root.stats.renderTime = Date.now() - sw;
195
+ root.stats.queueLengthAfterRender = root.q!.length;
196
+ if (root.q!.length > 0) {
197
+ root.render!();
198
+ }
199
+ }
200
+ })
201
+ });
202
+
203
+ root.patch = (<PatchableState<S>>initialState).patch;
204
+ root.state = <PatchableState<S>>initialState;
205
+ root.q = [];
206
+ const initialVode = dom(<S>initialState);
207
+ root.vode = <AttachedVode<S>>initialVode;
208
+ root.vode = render(<S>initialState, root.patch!, container, 0, undefined, initialVode)!;
209
+
210
+ for (const effect of initialPatches) {
211
+ root.patch!(effect);
212
+ }
213
+
214
+ return root.patch;
215
+ }
216
+
217
+ /** get properties of a vode, if there are any */
218
+ export function props<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): Props<S> | undefined {
219
+ if (Array.isArray(vode)
220
+ && vode.length > 1
221
+ && vode[1]
222
+ && !Array.isArray(vode[1])
223
+ ) {
224
+ if (
225
+ typeof vode[1] === "object"
226
+ && (vode[1] as unknown as Node).nodeType !== Node.TEXT_NODE
227
+ ) {
228
+ return vode[1];
229
+ }
230
+ }
231
+
232
+ return undefined;
233
+ }
234
+
235
+ export function mergeClass(a: ClassProp, b: ClassProp): ClassProp {
236
+ if (!a) return b;
237
+ if (!b) return a;
238
+
239
+ if (typeof a === "string" && typeof b === "string") {
240
+ const aSplit = a.split(" ");
241
+ const bSplit = b.split(" ");
242
+ const classSet = new Set([...aSplit, ...bSplit]);
243
+ return Array.from(classSet).join(" ").trim();
244
+ }
245
+ else if (typeof a === "string" && Array.isArray(b)) {
246
+ const classSet = new Set([...b, ...a.split(" ")]);
247
+ return Array.from(classSet).join(" ").trim();
248
+ }
249
+ else if (Array.isArray(a) && typeof b === "string") {
250
+ const classSet = new Set([...a, ...b.split(" ")]);
251
+ return Array.from(classSet).join(" ").trim();
252
+ }
253
+ else if (Array.isArray(a) && Array.isArray(b)) {
254
+ const classSet = new Set([...a, ...b]);
255
+ return Array.from(classSet).join(" ").trim();
256
+ }
257
+ else if (typeof a === "string" && typeof b === "object") {
258
+ return { [a]: true, ...b };
259
+ }
260
+ else if (typeof a === "object" && typeof b === "string") {
261
+ return { ...a, [b]: true };
262
+ }
263
+ else if (typeof a === "object" && typeof b === "object") {
264
+ return { ...a, ...b };
265
+ } else if (typeof a === "object" && Array.isArray(b)) {
266
+ const aa = { ...a };
267
+ for (const item of b as string[]) {
268
+ (<Record<string, boolean | null | undefined>>aa)[item] = true;
269
+ }
270
+ return aa;
271
+ } else if (Array.isArray(a) && typeof b === "object") {
272
+ const aa: Record<string, any> = {};
273
+ for (const item of a as string[]) {
274
+ aa[item] = true;
275
+ }
276
+ for (const bKey of (<Record<string, any>>b).keys) {
277
+ aa[bKey] = (<Record<string, boolean | null | undefined>>b)[bKey];
278
+ }
279
+ return b;
280
+ }
281
+
282
+ throw new Error(`cannot merge classes of ${a} (${typeof a}) and ${b} (${typeof b})`);
283
+ }
284
+
285
+ export function patchProps<S extends object | unknown>(vode: Vode<S>, props: Props<S>): void {
286
+ if (!Array.isArray(vode)) return;
287
+
288
+ if (vode.length > 1) {
289
+ if (!Array.isArray(vode[1]) && typeof vode[1] === "object") {
290
+ vode[1] = merge(vode[1], props);
291
+ return;
292
+ }
293
+
294
+ if (childCount(vode) > 0) {
295
+ (<FullVode<S>>vode).push(null);
296
+ }
297
+ for (let i = vode.length - 1; i > 0; i--) {
298
+ if (i > 1) vode[i] = vode[i - 1];
299
+ }
300
+ vode[1] = props;
301
+ } else {
302
+ (<FullVode<S>>vode).push(props);
303
+ }
304
+ }
305
+
306
+ /** get a slice of all children of a vode, if there are any */
307
+ export function children<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): ChildVode<S>[] | undefined {
308
+ const start = childrenStart(vode);
309
+ if (start > 0) {
310
+ return (<Vode<S>>vode).slice(start) as Vode<S>[];
311
+ }
312
+
313
+ return undefined;
314
+ }
315
+
316
+ /** index in vode at which child-vodes start */
317
+ export function childrenStart<S extends object | unknown>(vode: ChildVode<S> | AttachedVode<S>): number {
318
+ return props(vode) ? 2 : 1;
319
+ }
320
+
321
+ /** html tag of the vode or #text if it is a text node */
322
+ export function tag<S extends object | unknown>(v: Vode<S> | TextVode | NoVode | AttachedVode<S>): Tag | "#text" | undefined {
323
+ return !!v ? (Array.isArray(v)
324
+ ? v[0] : (typeof v === "string" || (<any>v).nodeType === Node.TEXT_NODE)
325
+ ? "#text" : undefined) as Tag
326
+ : undefined;
327
+ }
328
+
329
+ export function childCount<S>(vode: Vode<S>) { return vode.length - childrenStart(vode); }
330
+
331
+ export function child<S>(vode: Vode<S>, index: number): ChildVode<S> | undefined {
332
+ return vode[index + childrenStart(vode)] as ChildVode<S>;
333
+ }
334
+
335
+ /** merge multiple objects into one, applying from left to right
336
+ * @param first object to merge
337
+ * @returns merged object
338
+ */
339
+ export function merge(first?: any, ...p: any[]): any {
340
+ first = mergeState({}, first);
341
+ for (const pp of p) {
342
+ if (!pp) continue;
343
+ first = mergeState(first, pp);
344
+ }
345
+ return first!;
346
+ }
347
+
348
+ function classString(classProp: ClassProp): string {
349
+ if (typeof classProp === "string") {
350
+ return classProp;
351
+ } else if (Array.isArray(classProp)) {
352
+ return classProp.map(classString).join(" ");
353
+ } else if (typeof classProp === "object") {
354
+ return Object.keys(classProp!).filter(k => classProp![k]).join(" ");
355
+ } else {
356
+ return "";
357
+ }
358
+ }
359
+
360
+ function isNaturalVode(x: ChildVode<any>) {
361
+ return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
362
+ }
363
+
364
+ function isTextVode(x: ChildVode<any>) {
365
+ return typeof x === "string" || (<Text><unknown>x)?.nodeType === Node.TEXT_NODE;
366
+ }
367
+
368
+ function unwrap<S>(c: Component<S> | ChildVode<S>, s: S): ChildVode<S> {
369
+ if (typeof c === "function") {
370
+ return unwrap(c(s), s);
371
+ } else {
372
+ return c;
373
+ }
374
+ }
375
+
376
+ /** memoization of the given component or props (compare array is compared element by element (===) with the previous render) */
377
+ export function memo<S extends object | unknown>(compare: any[], componentOrProps: Component<S> | ((s: S) => Props<S>)): typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S> {
378
+ (<any>componentOrProps).__memo = compare;
379
+ return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
380
+ }
381
+
382
+ function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
383
+ if (typeof present !== "function")
384
+ return present;
385
+
386
+ const presentMemo = present?.__memo;
387
+ const pastMemo = past?.__memo;
388
+
389
+ if (Array.isArray(presentMemo)
390
+ && Array.isArray(pastMemo)
391
+ && presentMemo.length === pastMemo.length
392
+ ) {
393
+ let same = true;
394
+ for (let i = 0; i < presentMemo.length; i++) {
395
+ if (presentMemo[i] !== pastMemo[i]) {
396
+ same = false;
397
+ break;
398
+ }
399
+ }
400
+ if (same) return past;
401
+ }
402
+ const newRender = unwrap(present, state);
403
+ if (typeof newRender === "object") {
404
+ (<any>newRender).__memo = present?.__memo;
405
+ }
406
+ return newRender;
407
+ }
408
+
409
+ function render<S>(state: S, patch: Dispatch<S>, parent: ChildNode, childIndex: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, svg?: boolean): AttachedVode<S> | undefined {
410
+ // unwrap component if it is memoized
411
+ newVode = remember(state, newVode, oldVode) as ChildVode<S>;
412
+
413
+ const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
414
+ if (newVode === oldVode || (!oldVode && isNoVode)) {
415
+ return oldVode;
416
+ }
417
+
418
+ const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
419
+ const oldNode: ChildNode | undefined = oldIsText ? oldVode as Text : oldVode?.node;
420
+
421
+ // falsy|text|element(A) -> undefined
422
+ if (isNoVode) {
423
+ (<any>oldNode)?.onUnmount && patch((<any>oldNode).onUnmount(oldNode));
424
+ oldNode?.remove();
425
+ return undefined;
426
+ }
427
+
428
+ const isText = !isNoVode && isTextVode(newVode);
429
+ const isNode = !isNoVode && isNaturalVode(newVode);
430
+ const alreadyAttached = !!newVode && typeof newVode !== "string" && !!((<any>newVode)?.node || (<any>newVode)?.nodeType === Node.TEXT_NODE);
431
+
432
+ if (!isText && !isNode && !alreadyAttached && !oldVode) {
433
+ throw new Error("Invalid vode: " + typeof newVode + " " + JSON.stringify(newVode));
434
+ }
435
+ else if (alreadyAttached && isText) {
436
+ newVode = (<Text><any>newVode).wholeText;
437
+ }
438
+ else if (alreadyAttached && isNode) {
439
+ newVode = [...<Vode<S>>newVode];
440
+ }
441
+
442
+ // text -> text
443
+ if (oldIsText && isText) {
444
+ if ((<Text>oldNode).nodeValue !== <string>newVode) {
445
+ (<Text>oldNode).nodeValue = <string>newVode;
446
+ }
447
+ return oldVode;
448
+ }
449
+ // falsy|element -> text
450
+ if (isText && (!oldNode || !oldIsText)) {
451
+ const text = document.createTextNode(newVode as string)
452
+ if (oldNode) {
453
+ (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
454
+ oldNode.replaceWith(text);
455
+ } else {
456
+ if (parent.childNodes[childIndex]) {
457
+ parent.insertBefore(text, parent.childNodes[childIndex]);
458
+ } else {
459
+ parent.appendChild(text);
460
+ }
461
+ }
462
+
463
+ return text as Text;
464
+ }
465
+
466
+ // falsy|text|element(A) -> element(B)
467
+ if (
468
+ (isNode && (!oldNode || oldIsText || (<Vode<S>>oldVode)[0] !== (<Vode<S>>newVode)[0]))
469
+ ) {
470
+ svg = svg || (<Vode<S>>newVode)[0] === "svg";
471
+ const newNode: ChildNode = svg
472
+ ? document.createElementNS("http://www.w3.org/2000/svg", (<Vode<S>>newVode)[0])
473
+ : document.createElement((<Vode<S>>newVode)[0]);
474
+ (<AttachedVode<S>>newVode).node = newNode;
475
+
476
+ const newvode = <Vode<S>>newVode;
477
+ if (1 in newvode) {
478
+ newvode[1] = remember(state, newvode[1], undefined) as Vode<S>;
479
+ }
480
+
481
+ const properties = props(newVode);
482
+ patchProperties(patch, newNode, undefined, properties, svg);
483
+
484
+ if (oldNode) {
485
+ (<any>oldNode).onUnmount && patch((<any>oldNode).onUnmount(oldNode));
486
+ oldNode.replaceWith(newNode);
487
+ } else {
488
+ if (parent.childNodes[childIndex]) {
489
+ parent.insertBefore(newNode, parent.childNodes[childIndex]);
490
+ } else {
491
+ parent.appendChild(newNode);
492
+ }
493
+ }
494
+
495
+ const newChildren = children(newVode);
496
+ if (newChildren) {
497
+ for (let i = 0; i < newChildren.length; i++) {
498
+ const child = newChildren[i];
499
+ const attached = render(state, patch, newNode, i, undefined, child, svg);
500
+ (<Vode<S>>newVode!)[properties ? i + 2 : i + 1] = <Vode<S>>attached;
501
+ }
502
+ }
503
+
504
+ (<any>newNode).onMount && patch((<any>newNode).onMount(newNode));
505
+ return <AttachedVode<S>>newVode;
506
+ }
507
+
508
+ //element(A) -> element(A)
509
+ if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
510
+ svg = svg || (<Vode<S>>newVode)[0] === "svg";
511
+ (<AttachedVode<S>>newVode).node = oldNode;
512
+
513
+ const newvode = <Vode<S>>newVode;
514
+ const oldvode = <Vode<S>>oldVode;
515
+
516
+ let hasProps = false;
517
+ if ((<any>newvode[1])?.__memo) {
518
+ const prev = newvode[1] as any;
519
+ newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
520
+ if (prev !== newvode[1]) {
521
+ const properties = props(newVode);
522
+ patchProperties(patch, oldNode!, props(oldVode), properties, svg);
523
+ hasProps = !!properties;
524
+ }
525
+ }
526
+ else {
527
+ const properties = props(newVode);
528
+ patchProperties(patch, oldNode!, props(oldVode), properties, svg);
529
+ hasProps = !!properties;
530
+ }
531
+
532
+ const newKids = children(newVode);
533
+ const oldKids = children(oldVode) as AttachedVode<S>[];
534
+ if (newKids) {
535
+ for (let i = 0; i < newKids.length; i++) {
536
+ const child = newKids[i];
537
+ const oldChild = oldKids && oldKids[i];
538
+
539
+ const attached = render(state, patch, oldNode!, i, oldChild, child, svg);
540
+ if (attached) {
541
+ (<Vode<S>>newVode)[hasProps ? i + 2 : i + 1] = <Vode<S>>attached;
542
+ }
543
+ }
544
+ for (let i = newKids.length; oldKids && i < oldKids.length; i++) {
545
+ if (oldKids[i]?.node)
546
+ oldKids[i].node!.remove();
547
+ else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
548
+ (oldKids[i] as Text).remove();
549
+ }
550
+ }
551
+
552
+ for (let i = newKids?.length || 0; i < oldKids?.length || 0; i++) {
553
+ if (oldKids[i]?.node)
554
+ oldKids[i].node!.remove();
555
+ else if ((oldKids[i] as Text)?.nodeType === Node.TEXT_NODE)
556
+ (oldKids[i] as Text).remove();
557
+ }
558
+ return <AttachedVode<S>>newVode;
559
+ }
560
+
561
+ return undefined;
562
+ }
563
+
564
+ function patchProperties<S>(patch: Dispatch<S>, node: ChildNode, oldProps?: Props<S>, newProps?: Props<S>, isSvg?: boolean) {
565
+ if (!newProps && !oldProps) return;
566
+ if (!oldProps) { // set new props
567
+ for (const key in newProps) {
568
+ const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
569
+ newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, undefined, newValue, isSvg);
570
+ }
571
+ } else if (newProps) { // clear old props and set new in one loop
572
+ const combinedKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
573
+ for (const key of combinedKeys) {
574
+ const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
575
+ const newValue = newProps[key as keyof Props<S>] as PropertyValue<S>;
576
+ if (key[0] === "o" && key[1] === "n") {
577
+ const oldEvent = (<any>node)["__" + key];
578
+ if ((oldEvent && oldEvent !== newValue) || (!oldEvent && oldValue !== newValue)) {
579
+ newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
580
+ }
581
+ (<any>node)["__" + key] = newValue;
582
+ }
583
+ else if (oldValue !== newValue) {
584
+ newProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, newValue, isSvg);
585
+ }
586
+ }
587
+ } else { //delete all old props, cause there are no new props
588
+ for (const key in oldProps) {
589
+ const oldValue = oldProps[key as keyof Props<S>] as PropertyValue<S>;
590
+ oldProps[key as keyof Props<S>] = patchProperty(patch, <Element>node, key, oldValue, undefined, isSvg);
591
+ }
592
+ }
593
+ }
594
+
595
+ function patchProperty<S>(patch: Dispatch<S>, node: ChildNode, key: string | keyof ElementEventMap, oldValue?: PropertyValue<S>, newValue?: PropertyValue<S>, isSvg?: boolean) {
596
+ if (key === "style") {
597
+ if (!newValue) {
598
+ (node as HTMLElement).style.cssText = "";
599
+ } else if (oldValue) {
600
+ for (let k in { ...(oldValue as Props<S>), ...(newValue as Props<S>) }) {
601
+ if (!oldValue || newValue[k as keyof PropertyValue<S>] !== oldValue[k as keyof PropertyValue<S>]) {
602
+ (node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
603
+ }
604
+ else if (oldValue[k as keyof PropertyValue<S>] && !newValue[k as keyof PropertyValue<S>]) {
605
+ (<any>(node as HTMLElement).style)[k as keyof PropertyValue<S>] = undefined;
606
+ }
607
+ }
608
+ } else {
609
+ for (let k in (newValue as Props<S>)) {
610
+ (node as HTMLElement).style[k as keyof PropertyValue<S>] = newValue[k as keyof PropertyValue<S>];
611
+ }
612
+ }
613
+ } else if (key === "class") {
614
+ if (isSvg) {
615
+ if (newValue) {
616
+ const newClass = classString(newValue as ClassProp);
617
+ (<SVGSVGElement>node).classList.value = newClass;
618
+ } else {
619
+ (<SVGSVGElement>node).classList.value = '';
620
+ }
621
+ } else {
622
+ if (newValue) {
623
+ const newClass = classString(newValue as ClassProp);
624
+ (<HTMLElement>node).className = newClass;
625
+ } else {
626
+ (<HTMLElement>node).className = '';
627
+ }
628
+ }
629
+ } else if (key[0] === "o" && key[1] === "n") {
630
+ if (newValue) {
631
+ let eventHandler: Function | null = null;
632
+ if (typeof newValue === "function") {
633
+ const action = newValue as EffectFunction<S>;
634
+ eventHandler = (evt: Event) => patch([action, evt]);
635
+ } else if (Array.isArray(newValue)) {
636
+ const arr = (newValue as Array<any>);
637
+ const action = newValue[0] as EffectFunction<S>;
638
+ if (arr.length > 1) {
639
+ eventHandler = () => patch([action, ...arr.slice(1)]);
640
+ }
641
+ else {
642
+ eventHandler = (evt: Event) => patch([action, evt]);
643
+ }
644
+ } else if (typeof newValue === "object") {
645
+ eventHandler = () => patch(newValue as Patch<S>);
646
+ }
647
+
648
+ (<any>node)[key] = eventHandler;
649
+ } else {
650
+ (<any>node)[key] = null;
651
+ }
652
+ } else if (newValue !== null && newValue !== undefined && newValue !== false) {
653
+ (<HTMLElement>node).setAttribute(key, <string>newValue);
654
+ } else {
655
+ (<HTMLElement>node).removeAttribute(key);
656
+ }
657
+
658
+ return newValue;
659
+ }
660
+
661
+ function mergeState(target: any, source: any) {
662
+ if (!source) return target;
663
+
664
+ for (const key in source) {
665
+ const value = source[key];
666
+ if (value && typeof value === "object") {
667
+ const targetValue = target[key];
668
+ if (targetValue) {
669
+ if (Array.isArray(value)) {
670
+ target[key] = [...value];
671
+ } else if (value instanceof Date && targetValue !== value) {
672
+ target[key] = new Date(value);
673
+ } else {
674
+ if (Array.isArray(targetValue)) target[key] = mergeState({}, value);
675
+ else if (typeof targetValue === "object") mergeState(target[key], value);
676
+ else target[key] = mergeState({}, value);
677
+ }
678
+ } else if (Array.isArray(value)) {
679
+ target[key] = [...value];
680
+ } else if (value instanceof Date) {
681
+ target[key] = new Date(value);
682
+ } else {
683
+ target[key] = mergeState({}, value);
684
+ }
685
+ }
686
+ else if (value === undefined) {
687
+ delete target[key];
688
+ }
689
+ else {
690
+ target[key] = value;
691
+ }
692
+ }
693
+ return target;
694
694
  };