@ryupold/vode 1.8.6 → 1.8.8
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/README.md +34 -56
- package/dist/vode.cjs.min.js +2 -2
- package/dist/vode.d.ts +6 -10
- package/dist/vode.es5.min.js +7 -7
- package/dist/vode.js +84 -138
- package/dist/vode.min.js +1 -1
- package/dist/vode.min.mjs +1 -1
- package/dist/vode.mjs +84 -138
- package/package.json +1 -1
- package/src/state-context.ts +6 -4
- package/src/vode.ts +105 -156
- package/test/helper.ts +78 -32
- package/test/index.ts +41 -16
- package/test/mocks.ts +132 -38
- package/test/tests-app.ts +117 -1
- package/test/tests-catch.ts +160 -0
- package/test/tests-defuse.ts +22 -1
- package/test/tests-examples.ts +992 -0
- package/test/tests-hydrate.ts +43 -9
- package/test/tests-memo.ts +91 -50
- package/test/tests-mount-unmount.ts +265 -1
- package/test/tests-patch-advanced.ts +84 -0
- package/test/tests-patch-merge.ts +66 -0
- package/test/tests-state-context.ts +32 -1
package/src/vode.ts
CHANGED
|
@@ -5,9 +5,9 @@ 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
|
-
export type Component<S> = (s: S) => ChildVode<S>;
|
|
10
|
+
export type Component<S = PatchableState> = (s: S) => ChildVode<S>;
|
|
11
11
|
|
|
12
12
|
export type Patch<S> =
|
|
13
13
|
| IgnoredPatch // ignored
|
|
@@ -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
|
|
|
@@ -219,9 +217,9 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
219
217
|
});
|
|
220
218
|
|
|
221
219
|
function renderDom(isAsync: boolean) {
|
|
222
|
-
const sw =
|
|
220
|
+
const sw = performance.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;
|
|
@@ -229,7 +227,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
229
227
|
}
|
|
230
228
|
|
|
231
229
|
if (!isAsync) {
|
|
232
|
-
_vode.stats.lastSyncRenderTime =
|
|
230
|
+
_vode.stats.lastSyncRenderTime = performance.now() - sw;
|
|
233
231
|
_vode.stats.syncRenderCount++;
|
|
234
232
|
_vode.isRendering = false;
|
|
235
233
|
if (_vode.qSync) _vode.renderSync();
|
|
@@ -260,7 +258,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
260
258
|
if (_vode.isAnimating || !_vode.qAsync || document.hidden) return;
|
|
261
259
|
|
|
262
260
|
_vode.isAnimating = true;
|
|
263
|
-
const sw =
|
|
261
|
+
const sw = performance.now();
|
|
264
262
|
try {
|
|
265
263
|
_vode.state = mergeState(_vode.state, _vode.qAsync, true);
|
|
266
264
|
_vode.qAsync = null;
|
|
@@ -269,7 +267,7 @@ export function app<S extends PatchableState = PatchableState>(
|
|
|
269
267
|
|
|
270
268
|
await globals.currentViewTransition?.updateCallbackDone;
|
|
271
269
|
} finally {
|
|
272
|
-
_vode.stats.lastAsyncRenderTime =
|
|
270
|
+
_vode.stats.lastAsyncRenderTime = performance.now() - sw;
|
|
273
271
|
_vode.stats.asyncRenderCount++;
|
|
274
272
|
_vode.isAnimating = false;
|
|
275
273
|
}
|
|
@@ -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();
|
|
@@ -388,13 +384,20 @@ export function hydrate<S = PatchableState>(element: Element | Text, prepareForR
|
|
|
388
384
|
}
|
|
389
385
|
|
|
390
386
|
/** memoizes the resulting component or props by comparing element by element (===) with the
|
|
391
|
-
* `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)
|
|
392
|
-
|
|
387
|
+
* `compare` of the previous render. otherwise skips the render step (not calling `componentOrProps`)
|
|
388
|
+
*/
|
|
389
|
+
export function memo<S = PatchableState>(compare: any[], component: Component<S>): Component<S> {
|
|
393
390
|
if (!compare || !Array.isArray(compare)) throw new Error("first argument to memo() must be an array of values to compare");
|
|
394
|
-
if (typeof
|
|
391
|
+
if (typeof component !== "function") throw new Error("second argument to memo() must be a function that returns a child vode");
|
|
392
|
+
|
|
393
|
+
if ((<any>component).__memo) { // wrap to prevent double memoization
|
|
394
|
+
const comp = component;
|
|
395
|
+
component = (s: S) => comp(s);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
(<any>component).__memo = compare;
|
|
395
399
|
|
|
396
|
-
|
|
397
|
-
return componentOrProps as typeof componentOrProps extends ((s: S) => Props<S>) ? ((s: S) => Props<S>) : Component<S>;
|
|
400
|
+
return component;
|
|
398
401
|
}
|
|
399
402
|
|
|
400
403
|
/**
|
|
@@ -511,20 +514,14 @@ function mergeState(target: any, source: any, allowDeletion: boolean) {
|
|
|
511
514
|
return target;
|
|
512
515
|
};
|
|
513
516
|
|
|
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 {
|
|
517
|
+
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
518
|
try {
|
|
522
519
|
// unwrap component if it is memoized
|
|
523
520
|
newVode = remember(state, newVode, oldVode) as ChildVode<S>;
|
|
524
521
|
|
|
525
522
|
const isNoVode = !newVode || typeof newVode === "number" || typeof newVode === "boolean";
|
|
526
523
|
if (newVode === oldVode || (!oldVode && isNoVode)) {
|
|
527
|
-
return oldVode
|
|
524
|
+
return oldVode;
|
|
528
525
|
}
|
|
529
526
|
|
|
530
527
|
const oldIsText = (oldVode as Text)?.nodeType === Node.TEXT_NODE;
|
|
@@ -532,17 +529,7 @@ function render<S extends PatchableState>(
|
|
|
532
529
|
|
|
533
530
|
// falsy|text|element(A) -> undefined
|
|
534
531
|
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
|
-
}
|
|
532
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
546
533
|
oldNode?.remove();
|
|
547
534
|
return undefined;
|
|
548
535
|
}
|
|
@@ -566,30 +553,20 @@ function render<S extends PatchableState>(
|
|
|
566
553
|
if ((<Text>oldNode).nodeValue !== <string>newVode) {
|
|
567
554
|
(<Text>oldNode).nodeValue = <string>newVode;
|
|
568
555
|
}
|
|
569
|
-
return oldVode
|
|
556
|
+
return oldVode;
|
|
570
557
|
}
|
|
571
558
|
// falsy|element -> text
|
|
572
559
|
if (isText && (!oldNode || !oldIsText)) {
|
|
573
560
|
const text = document.createTextNode(newVode as string)
|
|
574
561
|
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
|
-
}
|
|
562
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
586
563
|
oldNode.replaceWith(text);
|
|
587
564
|
} else {
|
|
588
565
|
let inserted = false;
|
|
589
566
|
for (let i = indexInParent; i < parent.childNodes.length; i++) {
|
|
590
567
|
const nextSibling = parent.childNodes[i];
|
|
591
568
|
if (nextSibling) {
|
|
592
|
-
nextSibling.before(text
|
|
569
|
+
nextSibling.before(text);
|
|
593
570
|
inserted = true;
|
|
594
571
|
break;
|
|
595
572
|
}
|
|
@@ -628,26 +605,14 @@ function render<S extends PatchableState>(
|
|
|
628
605
|
}
|
|
629
606
|
|
|
630
607
|
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;
|
|
608
|
+
unmountTree(state, oldVode as AttachedVode<S>);
|
|
643
609
|
oldNode.replaceWith(newNode);
|
|
644
610
|
} else {
|
|
645
|
-
unmounts[unmountStart] = properties?.onUnmount ?? null;
|
|
646
611
|
let inserted = false;
|
|
647
612
|
for (let i = indexInParent; i < parent.childNodes.length; i++) {
|
|
648
613
|
const nextSibling = parent.childNodes[i];
|
|
649
614
|
if (nextSibling) {
|
|
650
|
-
nextSibling.before(newNode
|
|
615
|
+
nextSibling.before(newNode);
|
|
651
616
|
inserted = true;
|
|
652
617
|
break;
|
|
653
618
|
}
|
|
@@ -657,28 +622,23 @@ function render<S extends PatchableState>(
|
|
|
657
622
|
}
|
|
658
623
|
}
|
|
659
624
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const newKids = children(newVode);
|
|
663
|
-
if (newKids) {
|
|
625
|
+
const newStart = childrenStart(newVode);
|
|
626
|
+
if (newStart > 0) {
|
|
664
627
|
const childOffset = !!properties ? 2 : 1;
|
|
665
628
|
let indexP = 0;
|
|
666
|
-
for (let i = 0; i <
|
|
667
|
-
const child =
|
|
668
|
-
|
|
669
|
-
(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
673
|
-
totalChildUnmounts += childUnmounts;
|
|
674
|
-
childUnmountStart += childUnmounts;
|
|
675
|
-
}
|
|
629
|
+
for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
|
|
630
|
+
const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
|
|
631
|
+
// render child in xml mode to prevent using the dom properties
|
|
632
|
+
const attached = render(state, newNode as Element, i, indexP, undefined, child, xmlns ?? null);
|
|
633
|
+
(<Vode<S>>newVode!)[i + childOffset] = <Vode<S>>attached;
|
|
634
|
+
if (attached) indexP++;
|
|
676
635
|
}
|
|
677
636
|
}
|
|
678
637
|
|
|
679
|
-
(<any>
|
|
680
|
-
(
|
|
681
|
-
|
|
638
|
+
(<any>newVode)._unmountCount = (properties?.onUnmount ? 1 : 0) + sumChildUnmountCounts(newVode as Vode<S>);
|
|
639
|
+
if (typeof properties?.onMount === "function") {
|
|
640
|
+
state.patch(properties.onMount(state, newNode as HTMLElement & SVGSVGElement & MathMLElement));
|
|
641
|
+
}
|
|
682
642
|
return <AttachedVode<S>>newVode;
|
|
683
643
|
}
|
|
684
644
|
|
|
@@ -686,63 +646,41 @@ function render<S extends PatchableState>(
|
|
|
686
646
|
if (!oldIsText && isNode && (<Vode<S>>oldVode)[0] === (<Vode<S>>newVode)[0]) {
|
|
687
647
|
(<AttachedVode<S>>newVode).node = oldNode;
|
|
688
648
|
|
|
689
|
-
const newvode = <Vode<S>>newVode;
|
|
690
|
-
const oldvode = <Vode<S>>oldVode;
|
|
691
|
-
|
|
692
649
|
const properties = props(newVode);
|
|
693
650
|
const oldProps = props(oldVode);
|
|
694
651
|
|
|
695
|
-
if (properties?.xmlns !== undefined)
|
|
652
|
+
if (properties?.xmlns !== undefined)
|
|
653
|
+
xmlns = properties.xmlns;
|
|
696
654
|
|
|
697
|
-
|
|
698
|
-
const prev = newvode[1] as any;
|
|
699
|
-
newvode[1] = remember(state, newvode[1], oldvode[1]) as Vode<S>;
|
|
700
|
-
if (prev !== newvode[1]) {
|
|
701
|
-
patchProperties(state, oldNode!, oldProps, properties, xmlns);
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
else {
|
|
705
|
-
patchProperties(state, oldNode!, oldProps, properties, xmlns);
|
|
706
|
-
}
|
|
655
|
+
patchProperties(state, oldNode!, oldProps, properties, xmlns);
|
|
707
656
|
|
|
708
657
|
if (!!properties?.catch && oldProps?.catch !== properties.catch) {
|
|
709
658
|
(<any>newVode).node['catch'] = null;
|
|
710
659
|
(<any>newVode).node.removeAttribute('catch');
|
|
711
660
|
}
|
|
712
661
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
let totalChildUnmounts = 0;
|
|
717
|
-
let childUnmountStart = unmountStart + 1;
|
|
718
|
-
const newKids = children(newVode);
|
|
719
|
-
const oldKids = children(oldVode) as AttachedVode<S>[];
|
|
720
|
-
if (newKids) {
|
|
721
|
-
const childOffset = !!properties ? 2 : 1;
|
|
662
|
+
const newStart = childrenStart(newVode);
|
|
663
|
+
const oldStart = childrenStart(oldVode);
|
|
664
|
+
if (newStart > 0) {
|
|
722
665
|
let indexP = 0;
|
|
723
|
-
for (let i = 0; i <
|
|
724
|
-
const child =
|
|
725
|
-
const oldChild =
|
|
726
|
-
|
|
727
|
-
(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
const childUnmounts = (<any>attached).unmountCount || 0;
|
|
731
|
-
totalChildUnmounts += childUnmounts;
|
|
732
|
-
childUnmountStart += childUnmounts;
|
|
733
|
-
}
|
|
666
|
+
for (let i = 0; i < (<Vode<S>>newVode).length - newStart; i++) {
|
|
667
|
+
const child = (<Vode<S>>newVode)[i + newStart] as ChildVode<S>;
|
|
668
|
+
const oldChild = oldStart > 0 ? (<Vode<S>>oldVode)[i + oldStart] as AttachedVode<S> : undefined;
|
|
669
|
+
|
|
670
|
+
const attached = render(state, oldNode as Element, i, indexP, oldChild, child, xmlns);
|
|
671
|
+
(<any>newVode)[i + newStart] = attached;
|
|
672
|
+
if (attached) indexP++;
|
|
734
673
|
}
|
|
735
674
|
}
|
|
736
675
|
|
|
737
|
-
if (
|
|
738
|
-
const newKidsCount =
|
|
739
|
-
for (let i =
|
|
740
|
-
render(state, oldNode as Element, i, i,
|
|
676
|
+
if (oldStart > 0) {
|
|
677
|
+
const newKidsCount = newStart > 0 ? (<Vode<S>>newVode).length - newStart : 0;
|
|
678
|
+
for (let i = (<Vode<S>>oldVode).length - 1 - oldStart; i >= newKidsCount; i--) {
|
|
679
|
+
render(state, oldNode as Element, i, i, (<any>oldVode)[i + oldStart], undefined, xmlns);
|
|
741
680
|
}
|
|
742
681
|
}
|
|
743
682
|
|
|
744
|
-
(<any>newVode).
|
|
745
|
-
(<any>newVode).unmountStart = unmountStart;
|
|
683
|
+
(<any>newVode)._unmountCount = (properties?.onUnmount ? 1 : 0) + sumChildUnmountCounts(newVode as Vode<S>);
|
|
746
684
|
return <AttachedVode<S>>newVode;
|
|
747
685
|
}
|
|
748
686
|
} catch (error) {
|
|
@@ -755,7 +693,7 @@ function render<S extends PatchableState>(
|
|
|
755
693
|
return render(state, parent, childIndex, indexInParent,
|
|
756
694
|
hydrate(((<AttachedVode<S>>newVode)?.node || oldVode?.node) as Element, true) as AttachedVode<S>,
|
|
757
695
|
handledVode,
|
|
758
|
-
xmlns
|
|
696
|
+
xmlns);
|
|
759
697
|
} else {
|
|
760
698
|
throw error;
|
|
761
699
|
}
|
|
@@ -764,6 +702,35 @@ function render<S extends PatchableState>(
|
|
|
764
702
|
return undefined;
|
|
765
703
|
}
|
|
766
704
|
|
|
705
|
+
function unmountTree<S extends PatchableState>(state: S, v: AttachedVode<S> | ChildVode<S> | undefined): void {
|
|
706
|
+
if (!v || !Array.isArray(v)) return;
|
|
707
|
+
if (((<any>v)._unmountCount | 0) === 0) return;
|
|
708
|
+
|
|
709
|
+
const kids = children(v as Vode<S>) as AttachedVode<S>[] | null;
|
|
710
|
+
if (kids) {
|
|
711
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
712
|
+
unmountTree(state, kids[i]);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const p = props(v);
|
|
717
|
+
if (typeof p?.onUnmount === "function") {
|
|
718
|
+
state.patch(p.onUnmount(state, (v as AttachedVode<S>).node as HTMLElement & SVGSVGElement & MathMLElement));
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function sumChildUnmountCounts<S>(v: Vode<S>): number {
|
|
723
|
+
const kids = children(v) as AttachedVode<S>[] | null;
|
|
724
|
+
if (!kids) return 0;
|
|
725
|
+
let n = 0;
|
|
726
|
+
for (const k of kids) {
|
|
727
|
+
if (k && Array.isArray(k)) {
|
|
728
|
+
n += ((<any>k)._unmountCount | 0);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return n;
|
|
732
|
+
}
|
|
733
|
+
|
|
767
734
|
function isNaturalVode<S>(x: ChildVode<S>): x is Vode<S> {
|
|
768
735
|
return Array.isArray(x) && x.length > 0 && typeof x[0] === "string";
|
|
769
736
|
}
|
|
@@ -773,14 +740,19 @@ function isTextVode<S>(x: ChildVode<S>): x is TextVode {
|
|
|
773
740
|
}
|
|
774
741
|
|
|
775
742
|
function remember<S>(state: S, present: any, past: any): ChildVode<S> | AttachedVode<S> {
|
|
743
|
+
while (typeof present === "function" && !present.__memo) {
|
|
744
|
+
present = present(state);
|
|
745
|
+
}
|
|
746
|
+
|
|
776
747
|
if (typeof present !== "function")
|
|
777
748
|
return present;
|
|
778
749
|
|
|
779
|
-
const presentMemo = present?.__memo;
|
|
780
|
-
const pastMemo = past?.__memo;
|
|
781
750
|
|
|
782
|
-
|
|
783
|
-
|
|
751
|
+
const presentMemo: unknown[] = present?.__memo;
|
|
752
|
+
const pastMemo: unknown[] = past?.__memo;
|
|
753
|
+
|
|
754
|
+
if (
|
|
755
|
+
Array.isArray(presentMemo) && Array.isArray(pastMemo)
|
|
784
756
|
&& presentMemo.length === pastMemo.length
|
|
785
757
|
) {
|
|
786
758
|
let same = true;
|
|
@@ -793,40 +765,17 @@ function remember<S>(state: S, present: any, past: any): ChildVode<S> | Attached
|
|
|
793
765
|
if (same) return past;
|
|
794
766
|
}
|
|
795
767
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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;
|
|
768
|
+
// memos are not equal so we unwrap the present
|
|
769
|
+
while (typeof present === "function") {
|
|
770
|
+
present = present(state);
|
|
815
771
|
}
|
|
816
772
|
|
|
817
|
-
|
|
818
|
-
if (typeof
|
|
819
|
-
(<any>
|
|
773
|
+
// attach memo to the unwrapped present for future comparisons
|
|
774
|
+
if (typeof present === "object") {
|
|
775
|
+
(<any>present).__memo = presentMemo;
|
|
820
776
|
}
|
|
821
|
-
return newRender;
|
|
822
|
-
}
|
|
823
777
|
|
|
824
|
-
|
|
825
|
-
if (typeof c === "function") {
|
|
826
|
-
return unwrap(c(s), s);
|
|
827
|
-
} else {
|
|
828
|
-
return c;
|
|
829
|
-
}
|
|
778
|
+
return present;
|
|
830
779
|
}
|
|
831
780
|
|
|
832
781
|
function patchProperties<S extends PatchableState>(
|
package/test/helper.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { children, ChildVode, PatchableState, tag, Vode } from "../src/vode";
|
|
2
|
-
import {
|
|
1
|
+
import { children, ChildVode, PatchableState, props, tag, Vode } from "../src/vode";
|
|
2
|
+
import { FakeElement, FakeTextNode } from "./mocks";
|
|
3
3
|
|
|
4
4
|
export class Expectation {
|
|
5
5
|
constructor(public readonly what: any) { }
|
|
6
6
|
|
|
7
|
-
toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol") {
|
|
7
|
+
toBeA(type: "undefined" | "object" | "function" | "bigint" | "boolean" | "number" | "string" | "symbol", failMessage?: string) {
|
|
8
8
|
if (typeof this.what !== type) {
|
|
9
|
-
throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}`);
|
|
9
|
+
throw new ExpectationError(this, `expected \n\ntypeof ${this.what}\n\nto be \n\n${type}${failMessage ? `\n\n${failMessage}` : ""}`);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
toEqual(other: any) {
|
|
13
|
+
toEqual(other: any, failMessage?: string) {
|
|
14
|
+
const failSuffix = failMessage ? `\n\n${failMessage}` : "";
|
|
15
|
+
|
|
14
16
|
function deepCompare(a: any, b: any, path: string[]): string[] | null {
|
|
15
17
|
if (typeof a !== typeof b) {
|
|
16
18
|
if (path.length === 0) path.push(``);
|
|
@@ -46,12 +48,12 @@ export class Expectation {
|
|
|
46
48
|
if (typeof this.what === "object" && typeof other === "object" && this.what !== null && other !== null) {
|
|
47
49
|
const unequal = deepCompare(this.what, other, []);
|
|
48
50
|
if (unequal) {
|
|
49
|
-
throw new ExpectationError(this, `expected \n\n${JSON.stringify(this.what, null, 2)}\n\n to equal \n\n${JSON.stringify(other, null, 2)}\n\nThey differ in: ${unequal.join(".")}`);
|
|
51
|
+
throw new ExpectationError(this, `expected \n\n${JSON.stringify(this.what, null, 2)}\n\n to equal \n\n${JSON.stringify(other, null, 2)}\n\nThey differ in: ${unequal.join(".")}${failSuffix}`);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
else {
|
|
53
55
|
if (this.what !== other) {
|
|
54
|
-
throw new ExpectationError(this, `expected (${typeof this.what})\n\n${this.what}\n\nto equal (${typeof other})\n\n${other}`);
|
|
56
|
+
throw new ExpectationError(this, `expected (${typeof this.what})\n\n${this.what}\n\nto equal (${typeof other})\n\n${other}${failSuffix}`);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
}
|
|
@@ -77,82 +79,126 @@ export class Expectation {
|
|
|
77
79
|
throw new ExpectationError(this, `expected function to fail\n\nbut it succeeded with a result of type ${typeof r}\n\n${r}`);
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
toMatch(v: ChildVode, state?: PatchableState) {
|
|
81
|
-
|
|
82
|
+
toMatch(v: ChildVode, state?: PatchableState | null, failMessage?: string) {
|
|
83
|
+
const failSuffix = failMessage ? `\n\n${failMessage}` : "";
|
|
84
|
+
|
|
85
|
+
if (this.what instanceof FakeElement || this.what instanceof FakeTextNode || typeof this.what === "string" || Array.isArray(this.what) || typeof this.what === "function") {
|
|
82
86
|
const that = this;
|
|
83
|
-
|
|
87
|
+
|
|
88
|
+
function deepCompare(e: FakeElement | FakeTextNode | ChildVode, cv: ChildVode, path: string[]): string[] | null {
|
|
84
89
|
|
|
85
90
|
// unwrap component
|
|
86
91
|
while (typeof cv === "function") {
|
|
87
92
|
if (!state) {
|
|
88
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
|
|
93
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
|
|
89
94
|
}
|
|
90
95
|
cv = cv(state);
|
|
91
96
|
}
|
|
92
97
|
while (typeof e === "function") {
|
|
93
98
|
if (!state) {
|
|
94
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]`);
|
|
99
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na Component\n\nbut got no state passed in [toMatch]${failSuffix}`);
|
|
95
100
|
}
|
|
96
101
|
e = e(state);
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
// string matches TextNode
|
|
105
|
+
if (typeof cv === "string" && e instanceof FakeTextNode) {
|
|
100
106
|
if (cv !== e.wholeText) {
|
|
101
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e.wholeText}`);
|
|
107
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e.wholeText}${failSuffix}`);
|
|
102
108
|
}
|
|
103
109
|
}
|
|
110
|
+
// string matches string
|
|
104
111
|
else if (typeof cv === "string" && typeof e === "string") {
|
|
105
112
|
if (cv !== e) {
|
|
106
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e}`);
|
|
113
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node with\n${cv}\n\nbut text was\n${e}${failSuffix}`);
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// vode matches element
|
|
118
|
+
else if (Array.isArray(cv) && e instanceof FakeElement) {
|
|
119
|
+
if (tag(cv)?.toUpperCase() !== e.tagName.toUpperCase()) {
|
|
120
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got <${e.tagName.toUpperCase()}>${failSuffix}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// compare attributes/props
|
|
124
|
+
const properties = props(cv);
|
|
125
|
+
if (properties) {
|
|
126
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
127
|
+
const attributeValue = e.fakeAttributes[k];
|
|
128
|
+
if (!attributeValue) {
|
|
129
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${v}"]\n\nbut it was not found${failSuffix}`);
|
|
130
|
+
}
|
|
131
|
+
if (attributeValue !== v) {
|
|
132
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nwith attribute [${k}="${v}"]\n\nbut it was [${k}="${attributeValue}"]${failSuffix}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
113
135
|
}
|
|
136
|
+
|
|
137
|
+
// compare children
|
|
114
138
|
const kids = children(cv) || [];
|
|
115
139
|
for (let i = 0; i < kids.length; i++) {
|
|
116
|
-
deepCompare(e.children
|
|
140
|
+
deepCompare(e.children.item(i) as any, kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
|
|
117
141
|
}
|
|
118
142
|
if (kids.length !== e.children.length) {
|
|
119
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut <${e.tagName}> has ${e.children.length} children`);
|
|
143
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut <${e.tagName.toUpperCase()}> has ${e.children.length} children${failSuffix}`);
|
|
120
144
|
}
|
|
121
145
|
}
|
|
146
|
+
// vode matches vode
|
|
122
147
|
else if (Array.isArray(cv) && Array.isArray(e)) {
|
|
123
|
-
if (tag(cv)?.
|
|
124
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\
|
|
148
|
+
if (tag(cv)?.toUpperCase() !== tag(e)?.toUpperCase()) {
|
|
149
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// compare attributes/props
|
|
153
|
+
const properties = props(cv);
|
|
154
|
+
const otherProperties = props(e) || {};
|
|
155
|
+
if (properties) {
|
|
156
|
+
for (const [k, v] of Object.entries(properties)) {
|
|
157
|
+
const attributeValue = otherProperties[k];
|
|
158
|
+
if (!attributeValue) {
|
|
159
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${v}"]\n\nbut it was not found${failSuffix}`);
|
|
160
|
+
}
|
|
161
|
+
if (attributeValue !== v) {
|
|
162
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na vode [${tag(cv)?.toUpperCase()}]\n\nwith attribute [${k}="${v}"]\n\nbut its value was [${k}="${attributeValue}"]${failSuffix}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
125
165
|
}
|
|
166
|
+
|
|
167
|
+
// compare children
|
|
126
168
|
const kids = children(cv) || [];
|
|
127
169
|
const otherKids = children(e) || [];
|
|
128
170
|
for (let i = 0; i < kids.length; i++) {
|
|
129
|
-
deepCompare(otherKids[i], kids[i], [...path,
|
|
171
|
+
deepCompare(otherKids[i], kids[i], [...path, `[${i}]${tag(kids[i] as Vode)?.toUpperCase() || "#text"}`]);
|
|
130
172
|
}
|
|
131
173
|
if (kids.length !== otherKids.length) {
|
|
132
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut [${tag(e)}] has ${otherKids.length} children`);
|
|
174
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\n${kids.length} children\n\nbut [${tag(e)?.toUpperCase()}] has ${otherKids.length} children${failSuffix}`);
|
|
133
175
|
}
|
|
134
176
|
}
|
|
135
177
|
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
// mismatch between text and element
|
|
179
|
+
else if (typeof cv === "string" && e instanceof FakeElement) {
|
|
180
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got <${e.tagName.toUpperCase()}>${failSuffix}`);
|
|
138
181
|
}
|
|
182
|
+
// mismatch between text and vode
|
|
139
183
|
else if (typeof cv === "string" && Array.isArray(e)) {
|
|
140
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got [${tag(e)}]`);
|
|
184
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\na text node\n\nbut got [${tag(e)?.toUpperCase()}]${failSuffix}`);
|
|
141
185
|
}
|
|
142
186
|
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
// mismatch between vode and text node
|
|
188
|
+
else if (Array.isArray(cv) && e instanceof FakeTextNode) {
|
|
189
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${e.wholeText})${failSuffix}`);
|
|
145
190
|
}
|
|
191
|
+
// mismatch between vode and text
|
|
146
192
|
else if (Array.isArray(cv) && typeof e === "string") {
|
|
147
|
-
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)}>\n\nbut got #text (${e})`);
|
|
193
|
+
throw new ExpectationError(that, `expected at\n${path.join(" > ")}\n\nan element <${tag(cv)?.toUpperCase()}>\n\nbut got #text (${e})${failSuffix}`);
|
|
148
194
|
}
|
|
149
195
|
|
|
150
196
|
return null;
|
|
151
197
|
}
|
|
152
198
|
|
|
153
|
-
deepCompare(this.what, v, [tag(v as Vode) || "#text"]);
|
|
199
|
+
deepCompare(this.what, v, [`${tag(v as Vode)?.toUpperCase() || "#text"}`]);
|
|
154
200
|
} else {
|
|
155
|
-
throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}`);
|
|
201
|
+
throw new ExpectationError(this, `expected an element or text node\n\nbut it is a ${typeof this.what}\n${this.what}${failSuffix}`);
|
|
156
202
|
}
|
|
157
203
|
}
|
|
158
204
|
};
|