@ryupold/vode 1.8.4 → 1.8.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/.github/workflows/publish.yml +13 -0
- package/.github/workflows/tests.yml +17 -0
- package/README.md +55 -5
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +5 -0
- package/dist/vode.es5.min.js +4 -4
- package/dist/vode.js +100 -20
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +100 -20
- package/package.json +4 -3
- package/src/merge-style.ts +4 -1
- package/src/vode.ts +110 -26
- package/test/helper.ts +168 -0
- package/test/index.ts +82 -0
- package/test/mocks.ts +111 -0
- package/test/tests-app.ts +226 -0
- package/test/tests-children.ts +69 -0
- package/test/tests-createPatch.ts +28 -0
- package/test/tests-createState.ts +43 -0
- package/test/tests-defuse.ts +74 -0
- package/test/tests-hydrate.ts +68 -0
- package/test/tests-memo.ts +119 -0
- package/test/tests-mergeClass.ts +63 -0
- package/test/tests-mergeProps.ts +43 -0
- package/test/tests-mergeStyle.ts +39 -0
- package/test/tests-mount-unmount.ts +1140 -0
- package/test/tests-props.ts +34 -0
- package/test/tests-state-context.ts +106 -0
- package/test/tests-tag.ts +33 -0
- package/test/tests-vode.ts +27 -0
- package/tsconfig.test.json +18 -0
package/dist/vode.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// src/vode.ts
|
|
2
2
|
var globals = {
|
|
3
3
|
currentViewTransition: void 0,
|
|
4
|
-
requestAnimationFrame:
|
|
5
|
-
startViewTransition:
|
|
4
|
+
requestAnimationFrame: typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : ((cb) => cb()),
|
|
5
|
+
startViewTransition: typeof document !== "undefined" && typeof document.startViewTransition === "function" ? document.startViewTransition.bind(document) : null
|
|
6
6
|
};
|
|
7
7
|
function vode(tag2, props2, ...children2) {
|
|
8
8
|
if (!tag2) throw new Error("first argument to vode() must be a tag name or a vode");
|
|
9
9
|
if (Array.isArray(tag2)) return tag2;
|
|
10
|
-
else if (props2) return [tag2, props2, ...children2];
|
|
10
|
+
else if (typeof props2 === "object") return [tag2, props2, ...children2];
|
|
11
11
|
else return [tag2, ...children2];
|
|
12
12
|
}
|
|
13
13
|
function app(container, state, dom, ...initialPatches) {
|
|
@@ -20,6 +20,7 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
20
20
|
_vode.qSync = null;
|
|
21
21
|
_vode.qAsync = null;
|
|
22
22
|
_vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
|
|
23
|
+
_vode.unmounts = [];
|
|
23
24
|
const patchableState = state;
|
|
24
25
|
if ("patch" in state && typeof state.patch === "function" && Array.isArray(state.patch.initialPatches)) {
|
|
25
26
|
initialPatches = [...state.patch.initialPatches, ...initialPatches];
|
|
@@ -90,7 +91,7 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
90
91
|
function renderDom(isAsync) {
|
|
91
92
|
const sw = Date.now();
|
|
92
93
|
const vom = dom(_vode.state);
|
|
93
|
-
_vode.vode = render(_vode.state, container.parentElement, 0, 0, _vode.vode, vom);
|
|
94
|
+
_vode.vode = render(_vode.state, container.parentElement, 0, 0, _vode.vode, vom, null, _vode.unmounts, 0);
|
|
94
95
|
if (container.tagName.toUpperCase() !== vom[0].toUpperCase()) {
|
|
95
96
|
container = _vode.vode.node;
|
|
96
97
|
container._vode = _vode;
|
|
@@ -143,14 +144,20 @@ function app(container, state, dom, ...initialPatches) {
|
|
|
143
144
|
const root = container;
|
|
144
145
|
root._vode = _vode;
|
|
145
146
|
const indexInParent = Array.from(container.parentElement.children).indexOf(container);
|
|
147
|
+
_vode.isRendering = true;
|
|
146
148
|
_vode.vode = render(
|
|
147
149
|
state,
|
|
148
150
|
container.parentElement,
|
|
149
151
|
indexInParent,
|
|
150
152
|
indexInParent,
|
|
151
153
|
hydrate(container, true),
|
|
152
|
-
dom(state)
|
|
154
|
+
dom(state),
|
|
155
|
+
null,
|
|
156
|
+
_vode.unmounts,
|
|
157
|
+
0
|
|
153
158
|
);
|
|
159
|
+
_vode.isRendering = false;
|
|
160
|
+
if (_vode.qSync) _vode.renderSync();
|
|
154
161
|
for (const effect of initialPatches) {
|
|
155
162
|
patchableState.patch(effect);
|
|
156
163
|
}
|
|
@@ -200,8 +207,6 @@ function hydrate(element, prepareForRender) {
|
|
|
200
207
|
if (element.nodeValue?.trim() !== "")
|
|
201
208
|
return prepareForRender ? element : element.nodeValue;
|
|
202
209
|
return void 0;
|
|
203
|
-
} else if (element.nodeType === Node.COMMENT_NODE) {
|
|
204
|
-
return void 0;
|
|
205
210
|
} else if (element.nodeType === Node.ELEMENT_NODE) {
|
|
206
211
|
const tag2 = element.tagName.toLowerCase();
|
|
207
212
|
const root = [tag2];
|
|
@@ -319,7 +324,7 @@ function mergeState(target, source, allowDeletion) {
|
|
|
319
324
|
}
|
|
320
325
|
return target;
|
|
321
326
|
}
|
|
322
|
-
function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmlns) {
|
|
327
|
+
function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmlns, unmounts, unmountStart) {
|
|
323
328
|
try {
|
|
324
329
|
newVode = remember(state, newVode, oldVode);
|
|
325
330
|
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
@@ -329,7 +334,17 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
329
334
|
const oldIsText = oldVode?.nodeType === Node.TEXT_NODE;
|
|
330
335
|
const oldNode = oldIsText ? oldVode : oldVode?.node;
|
|
331
336
|
if (isNoVode) {
|
|
332
|
-
|
|
337
|
+
if (!oldIsText && typeof oldVode?.unmountCount === "number") {
|
|
338
|
+
const start = oldVode.unmountStart;
|
|
339
|
+
const count = oldVode.unmountCount;
|
|
340
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
341
|
+
const fn = unmounts[start + i];
|
|
342
|
+
if (fn) {
|
|
343
|
+
state.patch(fn(state, oldNode));
|
|
344
|
+
unmounts[start + i] = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
333
348
|
oldNode?.remove();
|
|
334
349
|
return void 0;
|
|
335
350
|
}
|
|
@@ -352,7 +367,17 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
352
367
|
if (isText && (!oldNode || !oldIsText)) {
|
|
353
368
|
const text = document.createTextNode(newVode);
|
|
354
369
|
if (oldNode) {
|
|
355
|
-
|
|
370
|
+
if (!oldIsText && typeof oldVode?.unmountCount === "number") {
|
|
371
|
+
const start = oldVode.unmountStart;
|
|
372
|
+
const count = oldVode.unmountCount;
|
|
373
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
374
|
+
const fn = unmounts[start + i];
|
|
375
|
+
if (fn) {
|
|
376
|
+
state.patch(fn(state, oldNode));
|
|
377
|
+
unmounts[start + i] = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
356
381
|
oldNode.replaceWith(text);
|
|
357
382
|
} else {
|
|
358
383
|
let inserted = false;
|
|
@@ -385,9 +410,21 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
385
410
|
newVode.node.removeAttribute("catch");
|
|
386
411
|
}
|
|
387
412
|
if (oldNode) {
|
|
388
|
-
|
|
413
|
+
if (!oldIsText && typeof oldVode?.unmountCount === "number") {
|
|
414
|
+
const start = oldVode.unmountStart;
|
|
415
|
+
const count = oldVode.unmountCount;
|
|
416
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
417
|
+
const fn = unmounts[start + i];
|
|
418
|
+
if (fn) {
|
|
419
|
+
state.patch(fn(state, oldNode));
|
|
420
|
+
unmounts[start + i] = null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
389
425
|
oldNode.replaceWith(newNode);
|
|
390
426
|
} else {
|
|
427
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
391
428
|
let inserted = false;
|
|
392
429
|
for (let i = indexInParent; i < parent.childNodes.length; i++) {
|
|
393
430
|
const nextSibling = parent.childNodes[i];
|
|
@@ -401,18 +438,27 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
401
438
|
parent.appendChild(newNode);
|
|
402
439
|
}
|
|
403
440
|
}
|
|
441
|
+
let totalChildUnmounts = 0;
|
|
442
|
+
let childUnmountStart = unmountStart + 1;
|
|
404
443
|
const newKids = children(newVode);
|
|
405
444
|
if (newKids) {
|
|
406
445
|
const childOffset = !!properties ? 2 : 1;
|
|
407
446
|
let indexP = 0;
|
|
408
447
|
for (let i = 0; i < newKids.length; i++) {
|
|
409
448
|
const child2 = newKids[i];
|
|
410
|
-
const attached = render(state, newNode, i, indexP, void 0, child2, xmlns ?? null);
|
|
449
|
+
const attached = render(state, newNode, i, indexP, void 0, child2, xmlns ?? null, unmounts, childUnmountStart);
|
|
411
450
|
newVode[i + childOffset] = attached;
|
|
412
|
-
if (attached)
|
|
451
|
+
if (attached) {
|
|
452
|
+
indexP++;
|
|
453
|
+
const childUnmounts = attached.unmountCount || 0;
|
|
454
|
+
totalChildUnmounts += childUnmounts;
|
|
455
|
+
childUnmountStart += childUnmounts;
|
|
456
|
+
}
|
|
413
457
|
}
|
|
414
458
|
}
|
|
415
459
|
newNode.onMount && state.patch(newNode.onMount(newNode));
|
|
460
|
+
newVode.unmountCount = 1 + totalChildUnmounts;
|
|
461
|
+
newVode.unmountStart = unmountStart;
|
|
416
462
|
return newVode;
|
|
417
463
|
}
|
|
418
464
|
if (!oldIsText && isNode && oldVode[0] === newVode[0]) {
|
|
@@ -435,6 +481,9 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
435
481
|
newVode.node["catch"] = null;
|
|
436
482
|
newVode.node.removeAttribute("catch");
|
|
437
483
|
}
|
|
484
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
485
|
+
let totalChildUnmounts = 0;
|
|
486
|
+
let childUnmountStart = unmountStart + 1;
|
|
438
487
|
const newKids = children(newVode);
|
|
439
488
|
const oldKids = children(oldVode);
|
|
440
489
|
if (newKids) {
|
|
@@ -443,17 +492,24 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
443
492
|
for (let i = 0; i < newKids.length; i++) {
|
|
444
493
|
const child2 = newKids[i];
|
|
445
494
|
const oldChild = oldKids && oldKids[i];
|
|
446
|
-
const attached = render(state, oldNode, i, indexP, oldChild, child2, xmlns);
|
|
495
|
+
const attached = render(state, oldNode, i, indexP, oldChild, child2, xmlns, unmounts, childUnmountStart);
|
|
447
496
|
newVode[i + childOffset] = attached;
|
|
448
|
-
if (attached)
|
|
497
|
+
if (attached) {
|
|
498
|
+
indexP++;
|
|
499
|
+
const childUnmounts = attached.unmountCount || 0;
|
|
500
|
+
totalChildUnmounts += childUnmounts;
|
|
501
|
+
childUnmountStart += childUnmounts;
|
|
502
|
+
}
|
|
449
503
|
}
|
|
450
504
|
}
|
|
451
505
|
if (oldKids) {
|
|
452
506
|
const newKidsCount = newKids ? newKids.length : 0;
|
|
453
507
|
for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
|
|
454
|
-
render(state, oldNode, i, i, oldKids[i], void 0, xmlns);
|
|
508
|
+
render(state, oldNode, i, i, oldKids[i], void 0, xmlns, unmounts, oldKids[i].unmountStart);
|
|
455
509
|
}
|
|
456
510
|
}
|
|
511
|
+
newVode.unmountCount = 1 + totalChildUnmounts;
|
|
512
|
+
newVode.unmountStart = unmountStart;
|
|
457
513
|
return newVode;
|
|
458
514
|
}
|
|
459
515
|
} catch (error) {
|
|
@@ -467,7 +523,9 @@ function render(state, parent, childIndex, indexInParent, oldVode, newVode, xmln
|
|
|
467
523
|
indexInParent,
|
|
468
524
|
hydrate(newVode?.node || oldVode?.node, true),
|
|
469
525
|
handledVode,
|
|
470
|
-
xmlns
|
|
526
|
+
xmlns,
|
|
527
|
+
unmounts,
|
|
528
|
+
unmountStart
|
|
471
529
|
);
|
|
472
530
|
} else {
|
|
473
531
|
throw error;
|
|
@@ -496,9 +554,28 @@ function remember(state, present, past) {
|
|
|
496
554
|
}
|
|
497
555
|
if (same) return past;
|
|
498
556
|
}
|
|
499
|
-
const
|
|
557
|
+
const result = present(state);
|
|
558
|
+
if (typeof result === "function" && result?.__memo) {
|
|
559
|
+
const resultMemo = result.__memo;
|
|
560
|
+
if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
|
|
561
|
+
let same = true;
|
|
562
|
+
for (let i = 0; i < resultMemo.length; i++) {
|
|
563
|
+
if (resultMemo[i] !== pastMemo[i]) {
|
|
564
|
+
same = false;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (same) return past;
|
|
569
|
+
}
|
|
570
|
+
const innerRender = result(state);
|
|
571
|
+
if (typeof innerRender === "object") {
|
|
572
|
+
innerRender.__memo = resultMemo;
|
|
573
|
+
}
|
|
574
|
+
return innerRender;
|
|
575
|
+
}
|
|
576
|
+
const newRender = typeof result === "function" ? unwrap(result, state) : result;
|
|
500
577
|
if (typeof newRender === "object") {
|
|
501
|
-
newRender.__memo = present?.__memo;
|
|
578
|
+
newRender.__memo = result?.__memo || present?.__memo;
|
|
502
579
|
}
|
|
503
580
|
return newRender;
|
|
504
581
|
}
|
|
@@ -857,8 +934,11 @@ function mergeClass(...classes) {
|
|
|
857
934
|
}
|
|
858
935
|
|
|
859
936
|
// src/merge-style.ts
|
|
860
|
-
var tempDivForStyling
|
|
937
|
+
var tempDivForStyling;
|
|
861
938
|
function mergeStyle(...props2) {
|
|
939
|
+
if (!tempDivForStyling) {
|
|
940
|
+
tempDivForStyling = document.createElement("div");
|
|
941
|
+
}
|
|
862
942
|
try {
|
|
863
943
|
const merged = tempDivForStyling.style;
|
|
864
944
|
for (const style of props2) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryupold/vode",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.6",
|
|
4
4
|
"description": "a minimalist web framework",
|
|
5
5
|
"author": "Michael Scherbakow (ryupold)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,12 +46,13 @@
|
|
|
46
46
|
"release": "npm run build && npm run build-min && npm run build-classic && npm run build-classic-min && npm run babel && npm run babel-classic && npm run types",
|
|
47
47
|
"publish": "npm publish --access public",
|
|
48
48
|
"clean": "tsc -b --clean && rm dist/*",
|
|
49
|
-
"watch": "tsc -b -w"
|
|
49
|
+
"watch": "tsc -b -w",
|
|
50
|
+
"test": "tsc -p tsconfig.test.json && esbuild test/index.ts --bundle --outfile=test/bundle.js --platform=node && node test/bundle.js"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
53
|
"@babel/cli": "7.28.6",
|
|
53
54
|
"@babel/core": "7.29.0",
|
|
54
|
-
"@babel/preset-env": "7.29.
|
|
55
|
+
"@babel/preset-env": "7.29.5",
|
|
55
56
|
"babel-preset-minify": "0.5.2",
|
|
56
57
|
"dts-bundle-generator": "9.5.1",
|
|
57
58
|
"esbuild": "0.28.0",
|
package/src/merge-style.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { StyleProp } from "./vode";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
let tempDivForStyling: HTMLElement | undefined;
|
|
4
4
|
|
|
5
5
|
/** merge `StyleProps`s regardless of type
|
|
6
6
|
* @returns {string} merged StyleProp */
|
|
7
7
|
export function mergeStyle(...props: StyleProp[]): StyleProp {
|
|
8
|
+
if (!tempDivForStyling) {
|
|
9
|
+
tempDivForStyling = document.createElement('div');
|
|
10
|
+
}
|
|
8
11
|
try{
|
|
9
12
|
const merged = tempDivForStyling.style;
|
|
10
13
|
for (const style of props) {
|
package/src/vode.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type JustTagVode = [tag: Tag];
|
|
|
5
5
|
export type ChildVode<S = PatchableState> = Vode<S> | TextVode | NoVode | Component<S>;
|
|
6
6
|
export type TextVode = string & {};
|
|
7
7
|
export type NoVode = undefined | null | number | boolean | bigint | void;
|
|
8
|
-
export type AttachedVode<S> = Vode<S> & { node: ChildNode } | Text & { node?: never };
|
|
8
|
+
export type AttachedVode<S> = Vode<S> & { node: ChildNode, unmountCount: number, unmountStart: number } | Text & { node?: never, unmounts?: never, unmountStart?: never };
|
|
9
9
|
export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap) | (string & {});
|
|
10
10
|
export type Component<S> = (s: S) => ChildVode<S>;
|
|
11
11
|
|
|
@@ -78,8 +78,8 @@ export type PatchableState<S = object> = S & Patchable<S>;
|
|
|
78
78
|
|
|
79
79
|
export const globals = {
|
|
80
80
|
currentViewTransition: <ViewTransition | null | undefined>undefined,
|
|
81
|
-
requestAnimationFrame:
|
|
82
|
-
startViewTransition:
|
|
81
|
+
requestAnimationFrame: typeof window !== "undefined" && typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame.bind(window) : ((cb: () => void) => cb()),
|
|
82
|
+
startViewTransition: typeof document !== "undefined" && typeof document.startViewTransition === "function" ? document.startViewTransition.bind(document) : null,
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
export interface ContainerNode<S = PatchableState> extends HTMLElement {
|
|
@@ -97,6 +97,7 @@ export interface ContainerNode<S = PatchableState> extends HTMLElement {
|
|
|
97
97
|
qAsync: {} | undefined | null, // next render-patches to be animated after another
|
|
98
98
|
isRendering: boolean,
|
|
99
99
|
isAnimating: boolean,
|
|
100
|
+
unmounts: (MountFunction<S> | null)[],
|
|
100
101
|
/** stats about the overall patches & last render time */
|
|
101
102
|
stats: {
|
|
102
103
|
patchCount: number,
|
|
@@ -122,7 +123,7 @@ export function vode<S = PatchableState>(tag: Tag | Vode<S>, props?: Props<S> |
|
|
|
122
123
|
if (!tag) throw new Error("first argument to vode() must be a tag name or a vode");
|
|
123
124
|
|
|
124
125
|
if (Array.isArray(tag)) return tag;
|
|
125
|
-
else if (props) return [tag, props as Props<S>, ...children];
|
|
126
|
+
else if (typeof props === "object") return [tag, props as Props<S>, ...children];
|
|
126
127
|
else return [tag, ...children];
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -149,6 +150,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
149
150
|
_vode.qSync = null;
|
|
150
151
|
_vode.qAsync = null;
|
|
151
152
|
_vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
|
|
153
|
+
_vode.unmounts = [];
|
|
152
154
|
|
|
153
155
|
const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void };
|
|
154
156
|
|
|
@@ -219,7 +221,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
219
221
|
function renderDom(isAsync: boolean) {
|
|
220
222
|
const sw = Date.now();
|
|
221
223
|
const vom = dom(_vode.state);
|
|
222
|
-
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
|
|
224
|
+
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom, null, _vode.unmounts, 0)!;
|
|
223
225
|
|
|
224
226
|
if ((<ContainerNode<S>>container).tagName.toUpperCase() !== (vom[0] as Tag).toUpperCase()) { //the tag name was changed during render -> update reference to vode-app-root
|
|
225
227
|
container = _vode.vode.node as Element;
|
|
@@ -280,14 +282,20 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
280
282
|
const root = container as ContainerNode<S>;
|
|
281
283
|
root._vode = _vode;
|
|
282
284
|
const indexInParent = Array.from(container.parentElement.children).indexOf(container);
|
|
285
|
+
_vode.isRendering = true;
|
|
283
286
|
_vode.vode = render(
|
|
284
287
|
<S>state,
|
|
285
288
|
container.parentElement,
|
|
286
289
|
indexInParent,
|
|
287
290
|
indexInParent,
|
|
288
291
|
hydrate<S>(container, true) as AttachedVode<S>,
|
|
289
|
-
dom(<S>state)
|
|
292
|
+
dom(<S>state),
|
|
293
|
+
null,
|
|
294
|
+
_vode.unmounts,
|
|
295
|
+
0
|
|
290
296
|
)!;
|
|
297
|
+
_vode.isRendering = false;
|
|
298
|
+
if (_vode.qSync) _vode.renderSync();
|
|
291
299
|
|
|
292
300
|
for (const effect of initialPatches) {
|
|
293
301
|
patchableState.patch(effect);
|
|
@@ -349,9 +357,6 @@ export function hydrate<S = PatchableState>(element: Element | Text, prepareForR
|
|
|
349
357
|
return prepareForRender ? element as Text : (element as Text).nodeValue!;
|
|
350
358
|
return undefined; //ignore (mostly html whitespace)
|
|
351
359
|
}
|
|
352
|
-
else if (element.nodeType === Node.COMMENT_NODE) {
|
|
353
|
-
return undefined; //ignore (not interesting)
|
|
354
|
-
}
|
|
355
360
|
else if (element.nodeType === Node.ELEMENT_NODE) {
|
|
356
361
|
const tag: Tag = (<Element>element).tagName.toLowerCase();
|
|
357
362
|
const root: Vode<S> = [tag];
|
|
@@ -506,14 +511,20 @@ function mergeState(target: any, source: any, allowDeletion: boolean) {
|
|
|
506
511
|
return target;
|
|
507
512
|
};
|
|
508
513
|
|
|
509
|
-
function render<S extends PatchableState>(
|
|
514
|
+
function render<S extends PatchableState>(
|
|
515
|
+
state: S, parent: Element,
|
|
516
|
+
childIndex: number, indexInParent: number,
|
|
517
|
+
oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>,
|
|
518
|
+
xmlns: string | null,
|
|
519
|
+
unmounts: (MountFunction<S> | null)[], unmountStart: number
|
|
520
|
+
): AttachedVode<S> | undefined {
|
|
510
521
|
try {
|
|
511
522
|
// unwrap component if it is memoized
|
|
512
523
|
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
513
524
|
|
|
514
525
|
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
515
526
|
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
516
|
-
return oldVode
|
|
527
|
+
return oldVode as AttachedVode<S>;
|
|
517
528
|
}
|
|
518
529
|
|
|
519
530
|
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
@@ -521,7 +532,17 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
521
532
|
|
|
522
533
|
// falsy|text|element(A) -> undefined
|
|
523
534
|
if (isNoVode) {
|
|
524
|
-
(
|
|
535
|
+
if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
|
|
536
|
+
const start = (<any>oldVode).unmountStart;
|
|
537
|
+
const count = (<any>oldVode).unmountCount;
|
|
538
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
539
|
+
const fn = unmounts[start + i];
|
|
540
|
+
if (fn) {
|
|
541
|
+
state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
|
|
542
|
+
unmounts[start + i] = null;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
525
546
|
oldNode?.remove();
|
|
526
547
|
return undefined;
|
|
527
548
|
}
|
|
@@ -545,13 +566,23 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
545
566
|
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
546
567
|
(<Text>oldNode).nodeValue = <string>newVode;
|
|
547
568
|
}
|
|
548
|
-
return oldVode
|
|
569
|
+
return oldVode as AttachedVode<S>;
|
|
549
570
|
}
|
|
550
571
|
// falsy|element -> text
|
|
551
572
|
if (isText && (!oldNode || !oldIsText)) {
|
|
552
573
|
const text = document.createTextNode(newVode as string)
|
|
553
574
|
if (oldNode) {
|
|
554
|
-
(
|
|
575
|
+
if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
|
|
576
|
+
const start = (<any>oldVode).unmountStart;
|
|
577
|
+
const count = (<any>oldVode).unmountCount;
|
|
578
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
579
|
+
const fn = unmounts[start + i];
|
|
580
|
+
if (fn) {
|
|
581
|
+
state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
|
|
582
|
+
unmounts[start + i] = null;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
555
586
|
oldNode.replaceWith(text);
|
|
556
587
|
} else {
|
|
557
588
|
let inserted = false;
|
|
@@ -597,9 +628,21 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
597
628
|
}
|
|
598
629
|
|
|
599
630
|
if (oldNode) {
|
|
600
|
-
(
|
|
631
|
+
if (!oldIsText && typeof (<any>oldVode)?.unmountCount === "number") {
|
|
632
|
+
const start = (<any>oldVode).unmountStart;
|
|
633
|
+
const count = (<any>oldVode).unmountCount;
|
|
634
|
+
for (let i = count - 1; i >= 0; i--) {
|
|
635
|
+
const fn = unmounts[start + i];
|
|
636
|
+
if (fn) {
|
|
637
|
+
state.patch(fn(state, oldNode as HTMLElement & SVGSVGElement & MathMLElement));
|
|
638
|
+
unmounts[start + i] = null;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
601
643
|
oldNode.replaceWith(newNode);
|
|
602
644
|
} else {
|
|
645
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
603
646
|
let inserted = false;
|
|
604
647
|
for (let i = indexInParent; i < parent.childNodes.length; i++) {
|
|
605
648
|
const nextSibling = parent.childNodes[i];
|
|
@@ -614,20 +657,28 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
614
657
|
}
|
|
615
658
|
}
|
|
616
659
|
|
|
660
|
+
let totalChildUnmounts = 0;
|
|
661
|
+
let childUnmountStart = unmountStart + 1;
|
|
617
662
|
const newKids = children(newVode);
|
|
618
663
|
if (newKids) {
|
|
619
664
|
const childOffset = !!properties ? 2 : 1;
|
|
620
665
|
let indexP = 0;
|
|
621
666
|
for (let i = 0; i < newKids.length; i++) {
|
|
622
667
|
const child = newKids[i];
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
(
|
|
626
|
-
|
|
668
|
+
const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null, unmounts, childUnmountStart);
|
|
669
|
+
(<Vode<S>>newVode!)[i + childOffset] = <Vode<S> | undefined>attached;
|
|
670
|
+
if (attached) {
|
|
671
|
+
indexP++;
|
|
672
|
+
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
673
|
+
totalChildUnmounts += childUnmounts;
|
|
674
|
+
childUnmountStart += childUnmounts;
|
|
675
|
+
}
|
|
627
676
|
}
|
|
628
677
|
}
|
|
629
678
|
|
|
630
679
|
(<any>newNode).onMount && state.patch((<any>newNode).onMount(newNode));
|
|
680
|
+
(<any>newVode).unmountCount = 1 + totalChildUnmounts;
|
|
681
|
+
(<any>newVode).unmountStart = unmountStart;
|
|
631
682
|
return <AttachedVode<S>>newVode;
|
|
632
683
|
}
|
|
633
684
|
|
|
@@ -659,6 +710,11 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
659
710
|
(<any>newVode).node.removeAttribute('catch');
|
|
660
711
|
}
|
|
661
712
|
|
|
713
|
+
// own unmount slot (always reserved per element)
|
|
714
|
+
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
715
|
+
|
|
716
|
+
let totalChildUnmounts = 0;
|
|
717
|
+
let childUnmountStart = unmountStart + 1;
|
|
662
718
|
const newKids = children(newVode);
|
|
663
719
|
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
664
720
|
if (newKids) {
|
|
@@ -667,20 +723,26 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
667
723
|
for (let i = 0; i < newKids.length; i++) {
|
|
668
724
|
const child = newKids[i];
|
|
669
725
|
const oldChild = oldKids && oldKids[i];
|
|
670
|
-
|
|
671
|
-
const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
|
|
726
|
+
const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns, unmounts, childUnmountStart);
|
|
672
727
|
(<Vode<S>>newVode)[i + childOffset] = <Vode<S>>attached;
|
|
673
|
-
if (attached)
|
|
728
|
+
if (attached) {
|
|
729
|
+
indexP++;
|
|
730
|
+
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
731
|
+
totalChildUnmounts += childUnmounts;
|
|
732
|
+
childUnmountStart += childUnmounts;
|
|
733
|
+
}
|
|
674
734
|
}
|
|
675
735
|
}
|
|
676
736
|
|
|
677
737
|
if (oldKids) {
|
|
678
738
|
const newKidsCount = newKids ? newKids.length : 0;
|
|
679
739
|
for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
|
|
680
|
-
render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns);
|
|
740
|
+
render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns, unmounts, (<any>oldKids[i]).unmountStart);
|
|
681
741
|
}
|
|
682
742
|
}
|
|
683
743
|
|
|
744
|
+
(<any>newVode).unmountCount = 1 + totalChildUnmounts;
|
|
745
|
+
(<any>newVode).unmountStart = unmountStart;
|
|
684
746
|
return <AttachedVode<S>>newVode;
|
|
685
747
|
}
|
|
686
748
|
} catch (error) {
|
|
@@ -693,7 +755,7 @@ function render<S extends PatchableState>(state: S, parent: Element, childIndex:
|
|
|
693
755
|
return render(state, parent, childIndex, indexInParent,
|
|
694
756
|
hydrate(((<AttachedVode<S>>newVode)?.node || oldVode?.node) as Element, true) as AttachedVode<S>,
|
|
695
757
|
handledVode,
|
|
696
|
-
xmlns);
|
|
758
|
+
xmlns, unmounts, unmountStart);
|
|
697
759
|
} else {
|
|
698
760
|
throw error;
|
|
699
761
|
}
|
|
@@ -730,9 +792,31 @@ function remember<S>(state: S, present: any, past: any): ChildVode<S> | Attached
|
|
|
730
792
|
}
|
|
731
793
|
if (same) return past;
|
|
732
794
|
}
|
|
733
|
-
|
|
795
|
+
|
|
796
|
+
const result = present(state);
|
|
797
|
+
|
|
798
|
+
if (typeof result === "function" && result?.__memo) {
|
|
799
|
+
const resultMemo = result.__memo;
|
|
800
|
+
if (Array.isArray(resultMemo) && Array.isArray(pastMemo) && resultMemo.length === pastMemo.length) {
|
|
801
|
+
let same = true;
|
|
802
|
+
for (let i = 0; i < resultMemo.length; i++) {
|
|
803
|
+
if (resultMemo[i] !== pastMemo[i]) {
|
|
804
|
+
same = false;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
if (same) return past;
|
|
809
|
+
}
|
|
810
|
+
const innerRender = result(state);
|
|
811
|
+
if (typeof innerRender === "object") {
|
|
812
|
+
innerRender.__memo = resultMemo;
|
|
813
|
+
}
|
|
814
|
+
return innerRender;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const newRender = typeof result === "function" ? unwrap(result, state) : result;
|
|
734
818
|
if (typeof newRender === "object") {
|
|
735
|
-
(<any>newRender).__memo = present?.__memo;
|
|
819
|
+
(<any>newRender).__memo = result?.__memo || present?.__memo;
|
|
736
820
|
}
|
|
737
821
|
return newRender;
|
|
738
822
|
}
|