@ryupold/vode 1.8.6 → 1.8.7
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/dist/vode.cjs.min.js +1 -1
- package/dist/vode.d.ts +0 -5
- package/dist/vode.es5.min.js +7 -7
- package/dist/vode.js +42 -70
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +42 -70
- package/package.json +1 -1
- package/src/vode.ts +53 -81
- package/test/tests-mount-unmount.ts +265 -1
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
|
|
8
|
+
export type AttachedVode<S> = Vode<S> & { node: ChildNode } | Text & { node?: never };
|
|
9
9
|
export type Tag = keyof (HTMLElementTagNameMap & SVGElementTagNameMap & MathMLElementTagNameMap) | (string & {});
|
|
10
10
|
export type Component<S> = (s: S) => ChildVode<S>;
|
|
11
11
|
|
|
@@ -97,7 +97,6 @@ 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)[],
|
|
101
100
|
/** stats about the overall patches & last render time */
|
|
102
101
|
stats: {
|
|
103
102
|
patchCount: number,
|
|
@@ -150,7 +149,6 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
150
149
|
_vode.qSync = null;
|
|
151
150
|
_vode.qAsync = null;
|
|
152
151
|
_vode.stats = { lastSyncRenderTime: 0, lastAsyncRenderTime: 0, syncRenderCount: 0, asyncRenderCount: 0, liveEffectCount: 0, patchCount: 0, syncRenderPatchCount: 0, asyncRenderPatchCount: 0 };
|
|
153
|
-
_vode.unmounts = [];
|
|
154
152
|
|
|
155
153
|
const patchableState = state as PatchableState<S> & { patch: (action: Patch<S>, animate?: boolean) => void };
|
|
156
154
|
|
|
@@ -221,7 +219,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
221
219
|
function renderDom(isAsync: boolean) {
|
|
222
220
|
const sw = Date.now();
|
|
223
221
|
const vom = dom(_vode.state);
|
|
224
|
-
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom
|
|
222
|
+
_vode.vode = render<S>(_vode.state, container.parentElement as Element, 0, 0, _vode.vode, vom)!;
|
|
225
223
|
|
|
226
224
|
if ((<ContainerNode<S>>container).tagName.toUpperCase() !== (vom[0] as Tag).toUpperCase()) { //the tag name was changed during render -> update reference to vode-app-root
|
|
227
225
|
container = _vode.vode.node as Element;
|
|
@@ -282,6 +280,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
282
280
|
const root = container as ContainerNode<S>;
|
|
283
281
|
root._vode = _vode;
|
|
284
282
|
const indexInParent = Array.from(container.parentElement.children).indexOf(container);
|
|
283
|
+
|
|
285
284
|
_vode.isRendering = true;
|
|
286
285
|
_vode.vode = render(
|
|
287
286
|
<S>state,
|
|
@@ -289,10 +288,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
289
288
|
indexInParent,
|
|
290
289
|
indexInParent,
|
|
291
290
|
hydrate<S>(container, true) as AttachedVode<S>,
|
|
292
|
-
dom(<S>state)
|
|
293
|
-
null,
|
|
294
|
-
_vode.unmounts,
|
|
295
|
-
0
|
|
291
|
+
dom(<S>state)
|
|
296
292
|
)!;
|
|
297
293
|
_vode.isRendering = false;
|
|
298
294
|
if (_vode.qSync) _vode.renderSync();
|
|
@@ -511,20 +507,14 @@ function mergeState(target: any, source: any, allowDeletion: boolean) {
|
|
|
511
507
|
return target;
|
|
512
508
|
};
|
|
513
509
|
|
|
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
|
+
function render<S extends PatchableState>(state: S, parent: Element, childIndex: number, indexInParent: number, oldVode: AttachedVode<S> | undefined, newVode: ChildVode<S>, xmlns?: string | null): AttachedVode<S> | undefined {
|
|
521
511
|
try {
|
|
522
512
|
// unwrap component if it is memoized
|
|
523
513
|
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
524
514
|
|
|
525
515
|
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
526
516
|
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
527
|
-
return oldVode
|
|
517
|
+
return oldVode;
|
|
528
518
|
}
|
|
529
519
|
|
|
530
520
|
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
@@ -532,17 +522,7 @@ function render<S extends PatchableState>(
|
|
|
532
522
|
|
|
533
523
|
// falsy|text|element(A) -> undefined
|
|
534
524
|
if (isNoVode) {
|
|
535
|
-
|
|
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
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
546
526
|
oldNode?.remove();
|
|
547
527
|
return undefined;
|
|
548
528
|
}
|
|
@@ -566,23 +546,13 @@ function render<S extends PatchableState>(
|
|
|
566
546
|
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
567
547
|
(<Text>oldNode).nodeValue = <string>newVode;
|
|
568
548
|
}
|
|
569
|
-
return oldVode
|
|
549
|
+
return oldVode;
|
|
570
550
|
}
|
|
571
551
|
// falsy|element -> text
|
|
572
552
|
if (isText && (!oldNode || !oldIsText)) {
|
|
573
553
|
const text = document.createTextNode(newVode as string)
|
|
574
554
|
if (oldNode) {
|
|
575
|
-
|
|
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
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
586
556
|
oldNode.replaceWith(text);
|
|
587
557
|
} else {
|
|
588
558
|
let inserted = false;
|
|
@@ -628,21 +598,9 @@ function render<S extends PatchableState>(
|
|
|
628
598
|
}
|
|
629
599
|
|
|
630
600
|
if (oldNode) {
|
|
631
|
-
|
|
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
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
643
602
|
oldNode.replaceWith(newNode);
|
|
644
603
|
} else {
|
|
645
|
-
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
646
604
|
let inserted = false;
|
|
647
605
|
for (let i = indexInParent; i < parent.childNodes.length; i++) {
|
|
648
606
|
const nextSibling = parent.childNodes[i];
|
|
@@ -657,28 +615,23 @@ function render<S extends PatchableState>(
|
|
|
657
615
|
}
|
|
658
616
|
}
|
|
659
617
|
|
|
660
|
-
let totalChildUnmounts = 0;
|
|
661
|
-
let childUnmountStart = unmountStart + 1;
|
|
662
618
|
const newKids = children(newVode);
|
|
663
619
|
if (newKids) {
|
|
664
620
|
const childOffset = !!properties ? 2 : 1;
|
|
665
621
|
let indexP = 0;
|
|
666
622
|
for (let i = 0; i < newKids.length; i++) {
|
|
667
623
|
const child = newKids[i];
|
|
668
|
-
|
|
669
|
-
(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
673
|
-
totalChildUnmounts += childUnmounts;
|
|
674
|
-
childUnmountStart += childUnmounts;
|
|
675
|
-
}
|
|
624
|
+
// render child in xml mode to prevent using the dom properties
|
|
625
|
+
const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null);
|
|
626
|
+
(<Vode<S>>newVode!)[i + childOffset] = <Vode<S>>attached;
|
|
627
|
+
if (attached) indexP++;
|
|
676
628
|
}
|
|
677
629
|
}
|
|
678
630
|
|
|
679
|
-
(<any>
|
|
680
|
-
(
|
|
681
|
-
|
|
631
|
+
(<any>newVode)._unmountCount = (properties?.onUnmount ? 1 : 0) + sumChildUnmountCounts(newVode as Vode<S>);
|
|
632
|
+
if (typeof properties?.onMount === "function") {
|
|
633
|
+
state.patch(properties.onMount(state, newNode as HTMLElement & SVGSVGElement & MathMLElement));
|
|
634
|
+
}
|
|
682
635
|
return <AttachedVode<S>>newVode;
|
|
683
636
|
}
|
|
684
637
|
|
|
@@ -710,11 +663,6 @@ function render<S extends PatchableState>(
|
|
|
710
663
|
(<any>newVode).node.removeAttribute('catch');
|
|
711
664
|
}
|
|
712
665
|
|
|
713
|
-
// own unmount slot (always reserved per element)
|
|
714
|
-
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
715
|
-
|
|
716
|
-
let totalChildUnmounts = 0;
|
|
717
|
-
let childUnmountStart = unmountStart + 1;
|
|
718
666
|
const newKids = children(newVode);
|
|
719
667
|
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
720
668
|
if (newKids) {
|
|
@@ -723,26 +671,21 @@ function render<S extends PatchableState>(
|
|
|
723
671
|
for (let i = 0; i < newKids.length; i++) {
|
|
724
672
|
const child = newKids[i];
|
|
725
673
|
const oldChild = oldKids && oldKids[i];
|
|
726
|
-
|
|
674
|
+
|
|
675
|
+
const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
|
|
727
676
|
(<Vode<S>>newVode)[i + childOffset] = <Vode<S>>attached;
|
|
728
|
-
if (attached)
|
|
729
|
-
indexP++;
|
|
730
|
-
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
731
|
-
totalChildUnmounts += childUnmounts;
|
|
732
|
-
childUnmountStart += childUnmounts;
|
|
733
|
-
}
|
|
677
|
+
if (attached) indexP++;
|
|
734
678
|
}
|
|
735
679
|
}
|
|
736
680
|
|
|
737
681
|
if (oldKids) {
|
|
738
682
|
const newKidsCount = newKids ? newKids.length : 0;
|
|
739
683
|
for (let i = oldKids.length - 1; i >= newKidsCount; i--) {
|
|
740
|
-
render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns
|
|
684
|
+
render(state, oldNode as Element, i, i, oldKids[i], undefined, xmlns);
|
|
741
685
|
}
|
|
742
686
|
}
|
|
743
687
|
|
|
744
|
-
(<any>newVode).
|
|
745
|
-
(<any>newVode).unmountStart = unmountStart;
|
|
688
|
+
(<any>newVode)._unmountCount = (properties?.onUnmount ? 1 : 0) + sumChildUnmountCounts(newVode as Vode<S>);
|
|
746
689
|
return <AttachedVode<S>>newVode;
|
|
747
690
|
}
|
|
748
691
|
} catch (error) {
|
|
@@ -755,7 +698,7 @@ function render<S extends PatchableState>(
|
|
|
755
698
|
return render(state, parent, childIndex, indexInParent,
|
|
756
699
|
hydrate(((<AttachedVode<S>>newVode)?.node || oldVode?.node) as Element, true) as AttachedVode<S>,
|
|
757
700
|
handledVode,
|
|
758
|
-
xmlns
|
|
701
|
+
xmlns);
|
|
759
702
|
} else {
|
|
760
703
|
throw error;
|
|
761
704
|
}
|
|
@@ -764,6 +707,35 @@ function render<S extends PatchableState>(
|
|
|
764
707
|
return undefined;
|
|
765
708
|
}
|
|
766
709
|
|
|
710
|
+
function unmountTree<S extends PatchableState>(state: S, v: AttachedVode<S> | ChildVode<S> | undefined): void {
|
|
711
|
+
if (!v || !Array.isArray(v)) return;
|
|
712
|
+
if (((<any>v)._unmountCount | 0) === 0) return;
|
|
713
|
+
|
|
714
|
+
const kids = children(v as Vode<S>) as AttachedVode<S>[] | null;
|
|
715
|
+
if (kids) {
|
|
716
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
717
|
+
unmountTree(state, kids[i]);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const p = props(v);
|
|
722
|
+
if (typeof p?.onUnmount === "function") {
|
|
723
|
+
state.patch(p.onUnmount(state, (v as AttachedVode<S>).node as HTMLElement & SVGSVGElement & MathMLElement));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function sumChildUnmountCounts<S>(v: Vode<S>): number {
|
|
728
|
+
const kids = children(v) as AttachedVode<S>[] | null;
|
|
729
|
+
if (!kids) return 0;
|
|
730
|
+
let n = 0;
|
|
731
|
+
for (const k of kids) {
|
|
732
|
+
if (k && Array.isArray(k)) {
|
|
733
|
+
n += ((<any>k)._unmountCount | 0);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return n;
|
|
737
|
+
}
|
|
738
|
+
|
|
767
739
|
function isNaturalVode<S>(x: ChildVode<S>): x is Vode<S> {
|
|
768
740
|
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
769
741
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { app, createState } from "../src/vode"
|
|
1
|
+
import { app, createState, memo } from "../src/vode"
|
|
2
2
|
import { ARTICLE, ASIDE, DIV, INPUT, MAIN, NAV, P, SECTION, SPAN } from "../src/vode-tags";
|
|
3
3
|
import { expect } from "./helper";
|
|
4
4
|
|
|
@@ -1081,6 +1081,82 @@ export default {
|
|
|
1081
1081
|
expect(unmounts).toEqual(["unmount p-inner"]);
|
|
1082
1082
|
},
|
|
1083
1083
|
|
|
1084
|
+
"onUnmount(): memo hit + earlier sibling growth corrupts unmount indices": () => {
|
|
1085
|
+
const container = setup();
|
|
1086
|
+
const fired: string[] = [];
|
|
1087
|
+
const state = createState({ expanded: false, showB: true });
|
|
1088
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1089
|
+
[DIV,
|
|
1090
|
+
[SPAN,
|
|
1091
|
+
{
|
|
1092
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1093
|
+
fired.push("unmount A");
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
s.expanded && [ASIDE,
|
|
1097
|
+
{
|
|
1098
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1099
|
+
fired.push("unmount A-child");
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
"x"
|
|
1103
|
+
],
|
|
1104
|
+
],
|
|
1105
|
+
s.showB && memo([], () => [SECTION,
|
|
1106
|
+
{
|
|
1107
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1108
|
+
fired.push("unmount B");
|
|
1109
|
+
}
|
|
1110
|
+
},
|
|
1111
|
+
])
|
|
1112
|
+
]
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
expect(fired).toEqual([]);
|
|
1116
|
+
|
|
1117
|
+
patch({ expanded: true });
|
|
1118
|
+
expect(fired).toEqual([]);
|
|
1119
|
+
|
|
1120
|
+
patch({ showB: false });
|
|
1121
|
+
expect(fired).toEqual(["unmount B"]);
|
|
1122
|
+
},
|
|
1123
|
+
|
|
1124
|
+
"onUnmount(): excess child removal + same-render sibling growth": () => {
|
|
1125
|
+
const container = setup();
|
|
1126
|
+
const fired: string[] = [];
|
|
1127
|
+
const state = createState({ expanded: false, showB: true });
|
|
1128
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1129
|
+
[DIV,
|
|
1130
|
+
[SPAN,
|
|
1131
|
+
{
|
|
1132
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1133
|
+
fired.push("unmount A");
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
s.expanded && [ASIDE,
|
|
1137
|
+
{
|
|
1138
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1139
|
+
fired.push("unmount A-child");
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
"x"
|
|
1143
|
+
],
|
|
1144
|
+
],
|
|
1145
|
+
s.showB && [P,
|
|
1146
|
+
{
|
|
1147
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1148
|
+
fired.push("unmount B");
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
]
|
|
1152
|
+
]
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
expect(fired).toEqual([]);
|
|
1156
|
+
patch({ expanded: true, showB: false });
|
|
1157
|
+
expect(fired).toEqual(["unmount B"]);
|
|
1158
|
+
},
|
|
1159
|
+
|
|
1084
1160
|
"onMount() + onUnmount: symmetry of calls": () => {
|
|
1085
1161
|
const container = setup();
|
|
1086
1162
|
const state = createState({
|
|
@@ -1137,4 +1213,192 @@ export default {
|
|
|
1137
1213
|
'Timer removed'
|
|
1138
1214
|
]);
|
|
1139
1215
|
},
|
|
1216
|
+
|
|
1217
|
+
"onMount(): with catched component, replacement vode's onMount fires when error occurs": () => {
|
|
1218
|
+
const container = setup();
|
|
1219
|
+
const mounts: string[] = [];
|
|
1220
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1221
|
+
app(container, {}, () =>
|
|
1222
|
+
[DIV,
|
|
1223
|
+
{
|
|
1224
|
+
catch: [SECTION,
|
|
1225
|
+
{
|
|
1226
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1227
|
+
mounts.push("mount fallback");
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
"fallback"
|
|
1231
|
+
]
|
|
1232
|
+
},
|
|
1233
|
+
broken
|
|
1234
|
+
]
|
|
1235
|
+
);
|
|
1236
|
+
|
|
1237
|
+
expect(mounts).toEqual(["mount fallback"]);
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
"onMount(): with catched component, returned vode's onMount fires and receives error": () => {
|
|
1241
|
+
const container = setup();
|
|
1242
|
+
const mounts: string[] = [];
|
|
1243
|
+
const caughtErrors: string[] = [];
|
|
1244
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1245
|
+
app(container, {}, () =>
|
|
1246
|
+
[DIV,
|
|
1247
|
+
{
|
|
1248
|
+
catch: (s: unknown, err: Error) => {
|
|
1249
|
+
caughtErrors.push(err.message);
|
|
1250
|
+
return [SECTION,
|
|
1251
|
+
{
|
|
1252
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1253
|
+
mounts.push("mount fallback");
|
|
1254
|
+
}
|
|
1255
|
+
},
|
|
1256
|
+
"fallback"
|
|
1257
|
+
];
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
broken
|
|
1261
|
+
]
|
|
1262
|
+
);
|
|
1263
|
+
|
|
1264
|
+
expect(mounts).toEqual(["mount fallback"]);
|
|
1265
|
+
expect(caughtErrors).toEqual(["boom"]);
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1268
|
+
"onUnmount(): with catched component, replacement vode's onUnmount fires when removed": () => {
|
|
1269
|
+
const container = setup();
|
|
1270
|
+
const unmounts: string[] = [];
|
|
1271
|
+
const state = createState({ show: true });
|
|
1272
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1273
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1274
|
+
[DIV,
|
|
1275
|
+
s.show && [SECTION,
|
|
1276
|
+
{
|
|
1277
|
+
catch: [ARTICLE,
|
|
1278
|
+
{
|
|
1279
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1280
|
+
unmounts.push("unmount fallback");
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
"fallback"
|
|
1284
|
+
]
|
|
1285
|
+
},
|
|
1286
|
+
broken
|
|
1287
|
+
]
|
|
1288
|
+
]
|
|
1289
|
+
);
|
|
1290
|
+
|
|
1291
|
+
expect(unmounts).toEqual([]);
|
|
1292
|
+
patch({ show: false });
|
|
1293
|
+
expect(unmounts).toEqual(["unmount fallback"]);
|
|
1294
|
+
},
|
|
1295
|
+
|
|
1296
|
+
"onUnmount(): with catched component, deep replacement tree fires in post-order": () => {
|
|
1297
|
+
const container = setup();
|
|
1298
|
+
const unmounts: string[] = [];
|
|
1299
|
+
const state = createState({ show: true });
|
|
1300
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1301
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1302
|
+
[DIV,
|
|
1303
|
+
s.show && [SECTION,
|
|
1304
|
+
{
|
|
1305
|
+
catch: [ARTICLE,
|
|
1306
|
+
{
|
|
1307
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1308
|
+
unmounts.push("unmount article");
|
|
1309
|
+
}
|
|
1310
|
+
},
|
|
1311
|
+
[P,
|
|
1312
|
+
{
|
|
1313
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1314
|
+
unmounts.push("unmount p");
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
"x"
|
|
1318
|
+
],
|
|
1319
|
+
[SPAN,
|
|
1320
|
+
{
|
|
1321
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1322
|
+
unmounts.push("unmount span");
|
|
1323
|
+
}
|
|
1324
|
+
},
|
|
1325
|
+
"y"
|
|
1326
|
+
]
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1329
|
+
broken
|
|
1330
|
+
]
|
|
1331
|
+
]
|
|
1332
|
+
);
|
|
1333
|
+
|
|
1334
|
+
expect(unmounts).toEqual([]);
|
|
1335
|
+
patch({ show: false });
|
|
1336
|
+
expect(unmounts).toEqual(["unmount span", "unmount p", "unmount article"]);
|
|
1337
|
+
},
|
|
1338
|
+
|
|
1339
|
+
"onMount()/onUnmount(): with catched component, full lifecycle symmetry of catch replacement": () => {
|
|
1340
|
+
const container = setup();
|
|
1341
|
+
const logs: string[] = [];
|
|
1342
|
+
const state = createState({ show: true });
|
|
1343
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1344
|
+
const patch = app<typeof state>(container, state, (s) =>
|
|
1345
|
+
[DIV,
|
|
1346
|
+
s.show && [SECTION,
|
|
1347
|
+
{
|
|
1348
|
+
catch: [ARTICLE,
|
|
1349
|
+
{
|
|
1350
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1351
|
+
logs.push("mount article");
|
|
1352
|
+
},
|
|
1353
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1354
|
+
logs.push("unmount article");
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
"fallback"
|
|
1358
|
+
]
|
|
1359
|
+
},
|
|
1360
|
+
broken
|
|
1361
|
+
]
|
|
1362
|
+
]
|
|
1363
|
+
);
|
|
1364
|
+
|
|
1365
|
+
expect(logs).toEqual(["mount article"]);
|
|
1366
|
+
patch({ show: false });
|
|
1367
|
+
expect(logs).toEqual(["mount article", "unmount article"]);
|
|
1368
|
+
},
|
|
1369
|
+
|
|
1370
|
+
"onMount(): with catched component, original element's onMount does NOT fire when error caused replacement": () => {
|
|
1371
|
+
const container = setup();
|
|
1372
|
+
const logs: string[] = [];
|
|
1373
|
+
const broken: any = () => { throw new Error("boom"); };
|
|
1374
|
+
app(container, {}, () =>
|
|
1375
|
+
[DIV,
|
|
1376
|
+
{
|
|
1377
|
+
catch: [ARTICLE,
|
|
1378
|
+
{
|
|
1379
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1380
|
+
logs.push("mount fallback");
|
|
1381
|
+
}
|
|
1382
|
+
},
|
|
1383
|
+
"fallback"
|
|
1384
|
+
]
|
|
1385
|
+
},
|
|
1386
|
+
[SECTION,
|
|
1387
|
+
{
|
|
1388
|
+
onMount: (s: unknown, ele: HTMLElement) => {
|
|
1389
|
+
logs.push("mount original section");
|
|
1390
|
+
},
|
|
1391
|
+
onUnmount: (s: unknown, ele: HTMLElement) => {
|
|
1392
|
+
logs.push("unmount original section");
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
broken
|
|
1396
|
+
]
|
|
1397
|
+
]
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// SECTION never finishes mounting (its child broke), so its onMount must not fire.
|
|
1401
|
+
// The catch on DIV replaces the broken subtree with ARTICLE whose onMount must fire.
|
|
1402
|
+
expect(logs).toEqual(["mount fallback"]);
|
|
1403
|
+
},
|
|
1140
1404
|
}
|