rask-ui 0.1.0 → 0.2.0
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/component.d.ts +17 -0
- package/dist/component.d.ts.map +1 -1
- package/dist/component.js +4 -2
- package/dist/createAsync.d.ts +23 -0
- package/dist/createAsync.d.ts.map +1 -1
- package/dist/createAsync.js +23 -0
- package/dist/createAsync.test.d.ts +2 -0
- package/dist/createAsync.test.d.ts.map +1 -0
- package/dist/createAsync.test.js +110 -0
- package/dist/createContext.d.ts +26 -2
- package/dist/createContext.d.ts.map +1 -1
- package/dist/createContext.js +31 -5
- package/dist/createContext.test.d.ts +2 -0
- package/dist/createContext.test.d.ts.map +1 -0
- package/dist/createContext.test.js +136 -0
- package/dist/createMutation.d.ts +23 -0
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +23 -0
- package/dist/createMutation.test.d.ts +2 -0
- package/dist/createMutation.test.d.ts.map +1 -0
- package/dist/createMutation.test.js +168 -0
- package/dist/createQuery.d.ts +23 -0
- package/dist/createQuery.d.ts.map +1 -1
- package/dist/createQuery.js +23 -0
- package/dist/createQuery.test.d.ts +2 -0
- package/dist/createQuery.test.d.ts.map +1 -0
- package/dist/createQuery.test.js +156 -0
- package/dist/createRef.test.d.ts +2 -0
- package/dist/createRef.test.d.ts.map +1 -0
- package/dist/createRef.test.js +80 -0
- package/dist/createState.d.ts +24 -0
- package/dist/createState.d.ts.map +1 -1
- package/dist/createState.js +24 -0
- package/dist/createState.test.d.ts +2 -0
- package/dist/createState.test.d.ts.map +1 -0
- package/dist/createState.test.js +111 -0
- package/dist/createView.d.ts +54 -0
- package/dist/createView.d.ts.map +1 -0
- package/dist/createView.js +68 -0
- package/dist/createView.test.d.ts +2 -0
- package/dist/createView.test.d.ts.map +1 -0
- package/dist/createView.test.js +203 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +15 -2
- package/dist/error.test.d.ts +2 -0
- package/dist/error.test.d.ts.map +1 -0
- package/dist/error.test.js +144 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/integration.test.d.ts +2 -0
- package/dist/integration.test.d.ts.map +1 -0
- package/dist/integration.test.js +155 -0
- package/dist/jsx-dev-runtime.d.ts +3 -3
- package/dist/jsx-dev-runtime.d.ts.map +1 -1
- package/dist/jsx-dev-runtime.js +2 -2
- package/dist/jsx-runtime.d.ts +1 -4
- package/dist/jsx-runtime.d.ts.map +1 -1
- package/dist/jsx-runtime.js +3 -14
- package/dist/observation.d.ts +1 -0
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +5 -0
- package/dist/observation.test.d.ts +2 -0
- package/dist/observation.test.d.ts.map +1 -0
- package/dist/observation.test.js +150 -0
- package/dist/render-test.d.ts +2 -0
- package/dist/render-test.d.ts.map +1 -0
- package/dist/render-test.js +21 -0
- package/dist/render.d.ts +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +13 -1
- package/dist/test-setup.d.ts +16 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +39 -0
- package/dist/tests/class.test.d.ts +2 -0
- package/dist/tests/class.test.d.ts.map +1 -0
- package/dist/tests/class.test.js +143 -0
- package/dist/tests/complex-rendering.test.d.ts +2 -0
- package/dist/tests/complex-rendering.test.d.ts.map +1 -0
- package/dist/tests/complex-rendering.test.js +400 -0
- package/dist/tests/component.cleanup.test.d.ts +2 -0
- package/dist/tests/component.cleanup.test.d.ts.map +1 -0
- package/dist/tests/component.cleanup.test.js +325 -0
- package/dist/tests/component.counter.test.d.ts +2 -0
- package/dist/tests/component.counter.test.d.ts.map +1 -0
- package/dist/tests/component.counter.test.js +124 -0
- package/dist/tests/component.interaction.test.d.ts +2 -0
- package/dist/tests/component.interaction.test.d.ts.map +1 -0
- package/dist/tests/component.interaction.test.js +73 -0
- package/dist/tests/component.props.test.d.ts +2 -0
- package/dist/tests/component.props.test.d.ts.map +1 -0
- package/dist/tests/component.props.test.js +88 -0
- package/dist/tests/component.return-types.test.d.ts +2 -0
- package/dist/tests/component.return-types.test.d.ts.map +1 -0
- package/dist/tests/component.return-types.test.js +357 -0
- package/dist/tests/component.state.test.d.ts +2 -0
- package/dist/tests/component.state.test.d.ts.map +1 -0
- package/dist/tests/component.state.test.js +129 -0
- package/dist/tests/component.test.d.ts +2 -0
- package/dist/tests/component.test.d.ts.map +1 -0
- package/dist/tests/component.test.js +63 -0
- package/dist/tests/createAsync.test.d.ts +2 -0
- package/dist/tests/createAsync.test.d.ts.map +1 -0
- package/dist/tests/createAsync.test.js +110 -0
- package/dist/tests/createContext.test.d.ts +2 -0
- package/dist/tests/createContext.test.d.ts.map +1 -0
- package/dist/tests/createContext.test.js +141 -0
- package/dist/tests/createMutation.test.d.ts +2 -0
- package/dist/tests/createMutation.test.d.ts.map +1 -0
- package/dist/tests/createMutation.test.js +168 -0
- package/dist/tests/createQuery.test.d.ts +2 -0
- package/dist/tests/createQuery.test.d.ts.map +1 -0
- package/dist/tests/createQuery.test.js +156 -0
- package/dist/tests/createRef.test.d.ts +2 -0
- package/dist/tests/createRef.test.d.ts.map +1 -0
- package/dist/tests/createRef.test.js +84 -0
- package/dist/tests/createState.test.d.ts +2 -0
- package/dist/tests/createState.test.d.ts.map +1 -0
- package/dist/tests/createState.test.js +111 -0
- package/dist/tests/createView.test.d.ts +2 -0
- package/dist/tests/createView.test.d.ts.map +1 -0
- package/dist/tests/createView.test.js +203 -0
- package/dist/tests/edge-cases.test.d.ts +2 -0
- package/dist/tests/edge-cases.test.d.ts.map +1 -0
- package/dist/tests/edge-cases.test.js +637 -0
- package/dist/tests/error-no-boundary.test.d.ts +2 -0
- package/dist/tests/error-no-boundary.test.d.ts.map +1 -0
- package/dist/tests/error-no-boundary.test.js +174 -0
- package/dist/tests/error.test.d.ts +2 -0
- package/dist/tests/error.test.d.ts.map +1 -0
- package/dist/tests/error.test.js +199 -0
- package/dist/tests/fragment.test.d.ts +2 -0
- package/dist/tests/fragment.test.d.ts.map +1 -0
- package/dist/tests/fragment.test.js +618 -0
- package/dist/tests/integration.test.d.ts +2 -0
- package/dist/tests/integration.test.d.ts.map +1 -0
- package/dist/tests/integration.test.js +192 -0
- package/dist/tests/keys.test.d.ts +2 -0
- package/dist/tests/keys.test.d.ts.map +1 -0
- package/dist/tests/keys.test.js +293 -0
- package/dist/tests/mount.test.d.ts +2 -0
- package/dist/tests/mount.test.d.ts.map +1 -0
- package/dist/tests/mount.test.js +91 -0
- package/dist/tests/observation.test.d.ts +2 -0
- package/dist/tests/observation.test.d.ts.map +1 -0
- package/dist/tests/observation.test.js +150 -0
- package/dist/tests/patch.test.d.ts +2 -0
- package/dist/tests/patch.test.d.ts.map +1 -0
- package/dist/tests/patch.test.js +498 -0
- package/dist/tests/patchChildren.test.d.ts +2 -0
- package/dist/tests/patchChildren.test.d.ts.map +1 -0
- package/dist/tests/patchChildren.test.js +387 -0
- package/dist/tests/primitives.test.d.ts +2 -0
- package/dist/tests/primitives.test.d.ts.map +1 -0
- package/dist/tests/primitives.test.js +132 -0
- package/dist/vdom/AbstractVNode.d.ts +22 -0
- package/dist/vdom/AbstractVNode.d.ts.map +1 -0
- package/dist/vdom/AbstractVNode.js +106 -0
- package/dist/vdom/ComponentVNode.d.ts +48 -0
- package/dist/vdom/ComponentVNode.d.ts.map +1 -0
- package/dist/vdom/ComponentVNode.js +209 -0
- package/dist/vdom/ElementVNode.d.ts +24 -0
- package/dist/vdom/ElementVNode.d.ts.map +1 -0
- package/dist/vdom/ElementVNode.js +126 -0
- package/dist/vdom/FragmentVNode.d.ts +13 -0
- package/dist/vdom/FragmentVNode.d.ts.map +1 -0
- package/dist/vdom/FragmentVNode.js +34 -0
- package/dist/vdom/RootVNode.d.ts +22 -0
- package/dist/vdom/RootVNode.d.ts.map +1 -0
- package/dist/vdom/RootVNode.js +55 -0
- package/dist/vdom/TextVNode.d.ts +11 -0
- package/dist/vdom/TextVNode.d.ts.map +1 -0
- package/dist/vdom/TextVNode.js +32 -0
- package/dist/vdom/class.test.d.ts +2 -0
- package/dist/vdom/class.test.d.ts.map +1 -0
- package/dist/vdom/class.test.js +143 -0
- package/dist/vdom/complex-rendering.test.d.ts +2 -0
- package/dist/vdom/complex-rendering.test.d.ts.map +1 -0
- package/dist/vdom/complex-rendering.test.js +400 -0
- package/dist/vdom/component.cleanup.test.d.ts +2 -0
- package/dist/vdom/component.cleanup.test.d.ts.map +1 -0
- package/dist/vdom/component.cleanup.test.js +323 -0
- package/dist/vdom/component.counter.test.d.ts +2 -0
- package/dist/vdom/component.counter.test.d.ts.map +1 -0
- package/dist/vdom/component.counter.test.js +124 -0
- package/dist/vdom/component.interaction.test.d.ts +2 -0
- package/dist/vdom/component.interaction.test.d.ts.map +1 -0
- package/dist/vdom/component.interaction.test.js +73 -0
- package/dist/vdom/component.props.test.d.ts +2 -0
- package/dist/vdom/component.props.test.d.ts.map +1 -0
- package/dist/vdom/component.props.test.js +88 -0
- package/dist/vdom/component.return-types.test.d.ts +2 -0
- package/dist/vdom/component.return-types.test.d.ts.map +1 -0
- package/dist/vdom/component.return-types.test.js +357 -0
- package/dist/vdom/component.state.test.d.ts +2 -0
- package/dist/vdom/component.state.test.d.ts.map +1 -0
- package/dist/vdom/component.state.test.js +129 -0
- package/dist/vdom/component.test.d.ts +2 -0
- package/dist/vdom/component.test.d.ts.map +1 -0
- package/dist/vdom/component.test.js +63 -0
- package/dist/vdom/dom-utils.d.ts +9 -0
- package/dist/vdom/dom-utils.d.ts.map +1 -0
- package/dist/vdom/dom-utils.js +74 -0
- package/dist/vdom/edge-cases.test.d.ts +2 -0
- package/dist/vdom/edge-cases.test.d.ts.map +1 -0
- package/dist/vdom/edge-cases.test.js +637 -0
- package/dist/vdom/fragment.test.d.ts +2 -0
- package/dist/vdom/fragment.test.d.ts.map +1 -0
- package/dist/vdom/fragment.test.js +618 -0
- package/dist/vdom/index.d.ts +10 -0
- package/dist/vdom/index.d.ts.map +1 -0
- package/dist/vdom/index.js +26 -0
- package/dist/vdom/keys.test.d.ts +2 -0
- package/dist/vdom/keys.test.d.ts.map +1 -0
- package/dist/vdom/keys.test.js +293 -0
- package/dist/vdom/mount.test.d.ts +2 -0
- package/dist/vdom/mount.test.d.ts.map +1 -0
- package/dist/vdom/mount.test.js +91 -0
- package/dist/vdom/patch.test.d.ts +2 -0
- package/dist/vdom/patch.test.d.ts.map +1 -0
- package/dist/vdom/patch.test.js +498 -0
- package/dist/vdom/patchChildren.test.d.ts +2 -0
- package/dist/vdom/patchChildren.test.d.ts.map +1 -0
- package/dist/vdom/patchChildren.test.js +392 -0
- package/dist/vdom/primitives.test.d.ts +2 -0
- package/dist/vdom/primitives.test.d.ts.map +1 -0
- package/dist/vdom/primitives.test.js +132 -0
- package/dist/vdom/types.d.ts +8 -0
- package/dist/vdom/types.d.ts.map +1 -0
- package/dist/vdom/types.js +1 -0
- package/dist/vdom/utils.d.ts +6 -0
- package/dist/vdom/utils.d.ts.map +1 -0
- package/dist/vdom/utils.js +63 -0
- package/package.json +1 -4
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { AbstractVNode } from "../vdom/AbstractVNode";
|
|
3
|
+
// Helper to convert MockVNode arrays to VNode arrays
|
|
4
|
+
const toVNodes = (nodes) => nodes;
|
|
5
|
+
// Mock VNode for testing
|
|
6
|
+
class MockVNode extends AbstractVNode {
|
|
7
|
+
mountCalls = 0;
|
|
8
|
+
patchCalls = 0;
|
|
9
|
+
unmountCalls = 0;
|
|
10
|
+
patchedWith;
|
|
11
|
+
constructor(key) {
|
|
12
|
+
super();
|
|
13
|
+
this.key = key;
|
|
14
|
+
}
|
|
15
|
+
mount(parent) {
|
|
16
|
+
this.mountCalls++;
|
|
17
|
+
this.parent = parent;
|
|
18
|
+
this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
|
|
19
|
+
return this.elm;
|
|
20
|
+
}
|
|
21
|
+
patch(newNode) {
|
|
22
|
+
this.patchCalls++;
|
|
23
|
+
this.patchedWith = newNode;
|
|
24
|
+
// In real implementation, old node updates itself from new node
|
|
25
|
+
// but keeps the same object reference
|
|
26
|
+
}
|
|
27
|
+
unmount() {
|
|
28
|
+
this.unmountCalls++;
|
|
29
|
+
delete this.elm;
|
|
30
|
+
delete this.parent;
|
|
31
|
+
}
|
|
32
|
+
rerender() {
|
|
33
|
+
// No-op for mock
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Parent mock that has children
|
|
37
|
+
class MockParentVNode extends MockVNode {
|
|
38
|
+
constructor(initialChildren, key) {
|
|
39
|
+
super(key);
|
|
40
|
+
this.children = (initialChildren || []);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
44
|
+
describe("Edge cases", () => {
|
|
45
|
+
it("should mount all new children when starting with empty", () => {
|
|
46
|
+
const newChild1 = new MockVNode("a");
|
|
47
|
+
const newChild2 = new MockVNode("b");
|
|
48
|
+
const parent = new MockParentVNode([]);
|
|
49
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
50
|
+
// New children should be mounted
|
|
51
|
+
expect(newChild1.mountCalls).toBe(1);
|
|
52
|
+
expect(newChild2.mountCalls).toBe(1);
|
|
53
|
+
expect(newChild1.parent).toBe(parent);
|
|
54
|
+
expect(newChild2.parent).toBe(parent);
|
|
55
|
+
// Result should be the new children (since old was empty)
|
|
56
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
57
|
+
});
|
|
58
|
+
it("should unmount all old children when new is empty", () => {
|
|
59
|
+
const oldChild1 = new MockVNode("a");
|
|
60
|
+
const oldChild2 = new MockVNode("b");
|
|
61
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
62
|
+
const result = parent.patchChildren(toVNodes([]));
|
|
63
|
+
// Old children should be unmounted
|
|
64
|
+
expect(oldChild1.unmountCalls).toBe(1);
|
|
65
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
66
|
+
// Result should be empty
|
|
67
|
+
expect(result).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe("Patching with keys", () => {
|
|
71
|
+
it("should reuse old VNodes when keys match", () => {
|
|
72
|
+
const oldChild1 = new MockVNode("a");
|
|
73
|
+
const oldChild2 = new MockVNode("b");
|
|
74
|
+
const oldChild3 = new MockVNode("c");
|
|
75
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
76
|
+
const newChild1 = new MockVNode("a");
|
|
77
|
+
const newChild2 = new MockVNode("b");
|
|
78
|
+
const newChild3 = new MockVNode("c");
|
|
79
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
80
|
+
// OLD children should be patched with new children
|
|
81
|
+
expect(oldChild1.patchCalls).toBe(1);
|
|
82
|
+
expect(oldChild2.patchCalls).toBe(1);
|
|
83
|
+
expect(oldChild3.patchCalls).toBe(1);
|
|
84
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
85
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
86
|
+
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
87
|
+
// NEW children should NOT be mounted
|
|
88
|
+
expect(newChild1.mountCalls).toBe(0);
|
|
89
|
+
expect(newChild2.mountCalls).toBe(0);
|
|
90
|
+
expect(newChild3.mountCalls).toBe(0);
|
|
91
|
+
// OLD children should NOT be unmounted
|
|
92
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
93
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
94
|
+
expect(oldChild3.unmountCalls).toBe(0);
|
|
95
|
+
// Result should still be the OLD children (reused)
|
|
96
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
97
|
+
});
|
|
98
|
+
it("should handle reordered children with keys", () => {
|
|
99
|
+
const oldChild1 = new MockVNode("a");
|
|
100
|
+
const oldChild2 = new MockVNode("b");
|
|
101
|
+
const oldChild3 = new MockVNode("c");
|
|
102
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
103
|
+
// Reordered: c, a, b
|
|
104
|
+
const newChild1 = new MockVNode("c");
|
|
105
|
+
const newChild2 = new MockVNode("a");
|
|
106
|
+
const newChild3 = new MockVNode("b");
|
|
107
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
108
|
+
// Old nodes should be patched with corresponding new nodes by key
|
|
109
|
+
expect(oldChild1.patchedWith).toBe(newChild2); // a->a
|
|
110
|
+
expect(oldChild2.patchedWith).toBe(newChild3); // b->b
|
|
111
|
+
expect(oldChild3.patchedWith).toBe(newChild1); // c->c
|
|
112
|
+
// No unmounts
|
|
113
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
114
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
115
|
+
expect(oldChild3.unmountCalls).toBe(0);
|
|
116
|
+
// Result should be old children in NEW order (c, a, b)
|
|
117
|
+
expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
|
|
118
|
+
// Verify correct keys
|
|
119
|
+
expect(result[0].key).toBe("c");
|
|
120
|
+
expect(result[1].key).toBe("a");
|
|
121
|
+
expect(result[2].key).toBe("b");
|
|
122
|
+
});
|
|
123
|
+
it("should mount new children and unmount removed children", () => {
|
|
124
|
+
const oldChild1 = new MockVNode("a");
|
|
125
|
+
const oldChild2 = new MockVNode("b");
|
|
126
|
+
const oldChild3 = new MockVNode("c");
|
|
127
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
128
|
+
// Remove 'b', add 'd'
|
|
129
|
+
const newChild1 = new MockVNode("a");
|
|
130
|
+
const newChild2 = new MockVNode("c");
|
|
131
|
+
const newChild3 = new MockVNode("d");
|
|
132
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
133
|
+
// a and c should be patched (reused)
|
|
134
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
135
|
+
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
136
|
+
// d should be mounted
|
|
137
|
+
expect(newChild3.mountCalls).toBe(1);
|
|
138
|
+
// b should be unmounted
|
|
139
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
140
|
+
// Result should contain old a, old c, and new d
|
|
141
|
+
expect(result).toContain(oldChild1);
|
|
142
|
+
expect(result).toContain(oldChild3);
|
|
143
|
+
expect(result).toContain(newChild3);
|
|
144
|
+
expect(result).not.toContain(oldChild2);
|
|
145
|
+
expect(result.length).toBe(3);
|
|
146
|
+
});
|
|
147
|
+
it("should replace all children when all keys change", () => {
|
|
148
|
+
const oldChild1 = new MockVNode("a");
|
|
149
|
+
const oldChild2 = new MockVNode("b");
|
|
150
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
151
|
+
const newChild1 = new MockVNode("x");
|
|
152
|
+
const newChild2 = new MockVNode("y");
|
|
153
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
154
|
+
// All new children should be mounted
|
|
155
|
+
expect(newChild1.mountCalls).toBe(1);
|
|
156
|
+
expect(newChild2.mountCalls).toBe(1);
|
|
157
|
+
// All old children should be unmounted
|
|
158
|
+
expect(oldChild1.unmountCalls).toBe(1);
|
|
159
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
160
|
+
// Result should be the new children
|
|
161
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe("Patching without keys (index-based)", () => {
|
|
165
|
+
it("should patch children by index when no keys", () => {
|
|
166
|
+
const oldChild1 = new MockVNode();
|
|
167
|
+
const oldChild2 = new MockVNode();
|
|
168
|
+
const oldChild3 = new MockVNode();
|
|
169
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
170
|
+
const newChild1 = new MockVNode();
|
|
171
|
+
const newChild2 = new MockVNode();
|
|
172
|
+
const newChild3 = new MockVNode();
|
|
173
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
174
|
+
// Should patch by index: 0->0, 1->1, 2->2
|
|
175
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
176
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
177
|
+
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
178
|
+
// No unmounts
|
|
179
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
180
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
181
|
+
expect(oldChild3.unmountCalls).toBe(0);
|
|
182
|
+
// Result should be old children (reused)
|
|
183
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
184
|
+
});
|
|
185
|
+
it("should mount new children when growing without keys", () => {
|
|
186
|
+
const oldChild1 = new MockVNode();
|
|
187
|
+
const oldChild2 = new MockVNode();
|
|
188
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
189
|
+
const newChild1 = new MockVNode();
|
|
190
|
+
const newChild2 = new MockVNode();
|
|
191
|
+
const newChild3 = new MockVNode();
|
|
192
|
+
const newChild4 = new MockVNode();
|
|
193
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
|
|
194
|
+
// First two should be patched (reused)
|
|
195
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
196
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
197
|
+
// Last two should be mounted
|
|
198
|
+
expect(newChild3.mountCalls).toBe(1);
|
|
199
|
+
expect(newChild4.mountCalls).toBe(1);
|
|
200
|
+
// No unmounts
|
|
201
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
202
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
203
|
+
// Result should be [oldChild1, oldChild2, newChild3, newChild4]
|
|
204
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
|
|
205
|
+
});
|
|
206
|
+
it("should unmount old children when shrinking without keys", () => {
|
|
207
|
+
const oldChild1 = new MockVNode();
|
|
208
|
+
const oldChild2 = new MockVNode();
|
|
209
|
+
const oldChild3 = new MockVNode();
|
|
210
|
+
const oldChild4 = new MockVNode();
|
|
211
|
+
const parent = new MockParentVNode([
|
|
212
|
+
oldChild1,
|
|
213
|
+
oldChild2,
|
|
214
|
+
oldChild3,
|
|
215
|
+
oldChild4,
|
|
216
|
+
]);
|
|
217
|
+
const newChild1 = new MockVNode();
|
|
218
|
+
const newChild2 = new MockVNode();
|
|
219
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
220
|
+
// First two should be patched
|
|
221
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
222
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
223
|
+
// Last two old children should be unmounted
|
|
224
|
+
expect(oldChild3.unmountCalls).toBe(1);
|
|
225
|
+
expect(oldChild4.unmountCalls).toBe(1);
|
|
226
|
+
// First two should not be unmounted
|
|
227
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
228
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
229
|
+
// Result should be [oldChild1, oldChild2]
|
|
230
|
+
expect(result).toEqual([oldChild1, oldChild2]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
describe("Mixed keys and indices", () => {
|
|
234
|
+
it("should handle mix of keyed and non-keyed children", () => {
|
|
235
|
+
const oldChild1 = new MockVNode("a"); // key: "a"
|
|
236
|
+
const oldChild2 = new MockVNode(); // key: undefined -> index 1
|
|
237
|
+
const oldChild3 = new MockVNode("c"); // key: "c"
|
|
238
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
239
|
+
const newChild1 = new MockVNode("a"); // key: "a"
|
|
240
|
+
const newChild2 = new MockVNode(); // key: undefined -> index 1
|
|
241
|
+
const newChild3 = new MockVNode("c"); // key: "c"
|
|
242
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
243
|
+
// Keyed children should patch by key
|
|
244
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
245
|
+
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
246
|
+
// Non-keyed child should patch by index
|
|
247
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
248
|
+
// No unmounts
|
|
249
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
250
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
251
|
+
expect(oldChild3.unmountCalls).toBe(0);
|
|
252
|
+
// Result should be old children (reused)
|
|
253
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe("Real-world scenarios", () => {
|
|
257
|
+
it("should handle conditional rendering (null -> component)", () => {
|
|
258
|
+
const oldChild1 = new MockVNode("title");
|
|
259
|
+
const parent = new MockParentVNode([oldChild1]);
|
|
260
|
+
const newChild1 = new MockVNode("title");
|
|
261
|
+
const newChild2 = new MockVNode("details");
|
|
262
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
263
|
+
// Title should be patched (reused)
|
|
264
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
265
|
+
// Details should be mounted
|
|
266
|
+
expect(newChild2.mountCalls).toBe(1);
|
|
267
|
+
// No unmounts
|
|
268
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
269
|
+
// Result should be [oldChild1, newChild2]
|
|
270
|
+
expect(result).toEqual([oldChild1, newChild2]);
|
|
271
|
+
});
|
|
272
|
+
it("should handle conditional rendering (component -> null)", () => {
|
|
273
|
+
const oldChild1 = new MockVNode("title");
|
|
274
|
+
const oldChild2 = new MockVNode("details");
|
|
275
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
276
|
+
const newChild1 = new MockVNode("title");
|
|
277
|
+
const result = parent.patchChildren(toVNodes([newChild1]));
|
|
278
|
+
// Title should be patched (reused)
|
|
279
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
280
|
+
// Details should be unmounted
|
|
281
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
282
|
+
// Title should not be unmounted
|
|
283
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
284
|
+
// Result should be [oldChild1]
|
|
285
|
+
expect(result).toEqual([oldChild1]);
|
|
286
|
+
});
|
|
287
|
+
it("should handle list with items added at beginning", () => {
|
|
288
|
+
const oldChild1 = new MockVNode("item-1");
|
|
289
|
+
const oldChild2 = new MockVNode("item-2");
|
|
290
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
291
|
+
const newChild1 = new MockVNode("item-0"); // New item at start
|
|
292
|
+
const newChild2 = new MockVNode("item-1");
|
|
293
|
+
const newChild3 = new MockVNode("item-2");
|
|
294
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
295
|
+
// New item should be mounted
|
|
296
|
+
expect(newChild1.mountCalls).toBe(1);
|
|
297
|
+
// Existing items should be patched (reused)
|
|
298
|
+
expect(oldChild1.patchedWith).toBe(newChild2);
|
|
299
|
+
expect(oldChild2.patchedWith).toBe(newChild3);
|
|
300
|
+
// No unmounts
|
|
301
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
302
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
303
|
+
// Result should be [newChild1, oldChild1, oldChild2]
|
|
304
|
+
expect(result).toEqual([newChild1, oldChild1, oldChild2]);
|
|
305
|
+
});
|
|
306
|
+
it("should handle list with items added at end", () => {
|
|
307
|
+
const oldChild1 = new MockVNode("item-1");
|
|
308
|
+
const oldChild2 = new MockVNode("item-2");
|
|
309
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
310
|
+
const newChild1 = new MockVNode("item-1");
|
|
311
|
+
const newChild2 = new MockVNode("item-2");
|
|
312
|
+
const newChild3 = new MockVNode("item-3"); // New item at end
|
|
313
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
314
|
+
// Existing items should be patched (reused)
|
|
315
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
316
|
+
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
317
|
+
// New item should be mounted
|
|
318
|
+
expect(newChild3.mountCalls).toBe(1);
|
|
319
|
+
// No unmounts
|
|
320
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
321
|
+
expect(oldChild2.unmountCalls).toBe(0);
|
|
322
|
+
// Result should be [oldChild1, oldChild2, newChild3]
|
|
323
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3]);
|
|
324
|
+
});
|
|
325
|
+
it("should handle list with item removed from middle", () => {
|
|
326
|
+
const oldChild1 = new MockVNode("item-1");
|
|
327
|
+
const oldChild2 = new MockVNode("item-2");
|
|
328
|
+
const oldChild3 = new MockVNode("item-3");
|
|
329
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
330
|
+
const newChild1 = new MockVNode("item-1");
|
|
331
|
+
const newChild2 = new MockVNode("item-3"); // item-2 removed
|
|
332
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
333
|
+
// item-1 and item-3 should be patched (reused)
|
|
334
|
+
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
335
|
+
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
336
|
+
// item-2 should be unmounted
|
|
337
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
338
|
+
// Others should not be unmounted
|
|
339
|
+
expect(oldChild1.unmountCalls).toBe(0);
|
|
340
|
+
expect(oldChild3.unmountCalls).toBe(0);
|
|
341
|
+
// Result should be [oldChild1, oldChild3]
|
|
342
|
+
expect(result).toEqual([oldChild1, oldChild3]);
|
|
343
|
+
});
|
|
344
|
+
it("should handle empty -> multiple children", () => {
|
|
345
|
+
const parent = new MockParentVNode([]);
|
|
346
|
+
const newChild1 = new MockVNode("a");
|
|
347
|
+
const newChild2 = new MockVNode("b");
|
|
348
|
+
const newChild3 = new MockVNode("c");
|
|
349
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
350
|
+
// All should be mounted
|
|
351
|
+
expect(newChild1.mountCalls).toBe(1);
|
|
352
|
+
expect(newChild2.mountCalls).toBe(1);
|
|
353
|
+
expect(newChild3.mountCalls).toBe(1);
|
|
354
|
+
// Result should be the new children
|
|
355
|
+
expect(result).toEqual([newChild1, newChild2, newChild3]);
|
|
356
|
+
});
|
|
357
|
+
it("should handle multiple children -> empty", () => {
|
|
358
|
+
const oldChild1 = new MockVNode("a");
|
|
359
|
+
const oldChild2 = new MockVNode("b");
|
|
360
|
+
const oldChild3 = new MockVNode("c");
|
|
361
|
+
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
362
|
+
const result = parent.patchChildren(toVNodes([]));
|
|
363
|
+
// All should be unmounted
|
|
364
|
+
expect(oldChild1.unmountCalls).toBe(1);
|
|
365
|
+
expect(oldChild2.unmountCalls).toBe(1);
|
|
366
|
+
expect(oldChild3.unmountCalls).toBe(1);
|
|
367
|
+
// Result should be empty
|
|
368
|
+
expect(result).toEqual([]);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
describe("Object reference preservation", () => {
|
|
372
|
+
it("should preserve old VNode object references when patching", () => {
|
|
373
|
+
const oldChild1 = new MockVNode("a");
|
|
374
|
+
const oldChild2 = new MockVNode("b");
|
|
375
|
+
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
376
|
+
const newChild1 = new MockVNode("a");
|
|
377
|
+
const newChild2 = new MockVNode("b");
|
|
378
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
379
|
+
// The result should contain the EXACT SAME object references as the old children
|
|
380
|
+
expect(result[0]).toBe(oldChild1); // Same object reference
|
|
381
|
+
expect(result[1]).toBe(oldChild2); // Same object reference
|
|
382
|
+
// NOT the new children
|
|
383
|
+
expect(result[0]).not.toBe(newChild1);
|
|
384
|
+
expect(result[1]).not.toBe(newChild2);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitives.test.d.ts","sourceRoot":"","sources":["../../src/tests/primitives.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { jsx, render } from "../vdom/index";
|
|
3
|
+
describe("VDOM Primitives", () => {
|
|
4
|
+
it("should render element with string child", () => {
|
|
5
|
+
// Create a container element
|
|
6
|
+
const container = document.createElement("div");
|
|
7
|
+
// Create vnode with string child
|
|
8
|
+
const vnode = jsx("div", {
|
|
9
|
+
children: "Hello World",
|
|
10
|
+
});
|
|
11
|
+
// Render the vnode
|
|
12
|
+
render(vnode, container);
|
|
13
|
+
// Verify string was rendered as text content
|
|
14
|
+
const div = container.children[0];
|
|
15
|
+
expect(div.textContent).toBe("Hello World");
|
|
16
|
+
expect(div.childNodes.length).toBe(1);
|
|
17
|
+
expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
|
|
18
|
+
});
|
|
19
|
+
it("should render element with number child", () => {
|
|
20
|
+
// Create a container element
|
|
21
|
+
const container = document.createElement("div");
|
|
22
|
+
// Create vnode with number child
|
|
23
|
+
const vnode = jsx("span", {
|
|
24
|
+
children: 42,
|
|
25
|
+
});
|
|
26
|
+
// Render the vnode
|
|
27
|
+
render(vnode, container);
|
|
28
|
+
// Verify number was rendered as text content
|
|
29
|
+
const span = container.children[0];
|
|
30
|
+
expect(span.textContent).toBe("42");
|
|
31
|
+
expect(span.childNodes.length).toBe(1);
|
|
32
|
+
expect(span.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
|
|
33
|
+
});
|
|
34
|
+
it("should render element with null child", () => {
|
|
35
|
+
// Create a container element
|
|
36
|
+
const container = document.createElement("div");
|
|
37
|
+
// Create vnode with null child
|
|
38
|
+
const vnode = jsx("div", {
|
|
39
|
+
children: null,
|
|
40
|
+
});
|
|
41
|
+
// Render the vnode
|
|
42
|
+
render(vnode, container);
|
|
43
|
+
// Verify null renders as empty
|
|
44
|
+
const div = container.children[0];
|
|
45
|
+
expect(div.childNodes.length).toBe(0);
|
|
46
|
+
});
|
|
47
|
+
it("should render element with undefined child", () => {
|
|
48
|
+
// Create a container element
|
|
49
|
+
const container = document.createElement("div");
|
|
50
|
+
// Create vnode with undefined child
|
|
51
|
+
const vnode = jsx("div", {
|
|
52
|
+
children: undefined,
|
|
53
|
+
});
|
|
54
|
+
// Render the vnode
|
|
55
|
+
render(vnode, container);
|
|
56
|
+
// Verify undefined renders as empty
|
|
57
|
+
const div = container.children[0];
|
|
58
|
+
expect(div.childNodes.length).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
it("should render element with mixed children including primitives", () => {
|
|
61
|
+
// Create a container element
|
|
62
|
+
const container = document.createElement("div");
|
|
63
|
+
// Create vnode with mixed children
|
|
64
|
+
const vnode = jsx("div", {
|
|
65
|
+
children: [
|
|
66
|
+
"Text before",
|
|
67
|
+
jsx("span", { textContent: "middle" }),
|
|
68
|
+
"Text after",
|
|
69
|
+
42,
|
|
70
|
+
null,
|
|
71
|
+
jsx("div", { textContent: "end" }),
|
|
72
|
+
],
|
|
73
|
+
});
|
|
74
|
+
// Render the vnode
|
|
75
|
+
render(vnode, container);
|
|
76
|
+
// Verify mixed children were rendered correctly
|
|
77
|
+
const div = container.children[0];
|
|
78
|
+
// Should have: text node, span, text node, text node (42), div
|
|
79
|
+
// null should be skipped
|
|
80
|
+
expect(div.childNodes.length).toBe(5);
|
|
81
|
+
expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
|
|
82
|
+
expect(div.childNodes[0].textContent).toBe("Text before");
|
|
83
|
+
expect(div.childNodes[1].tagName).toBe("SPAN");
|
|
84
|
+
expect(div.childNodes[2].textContent).toBe("Text after");
|
|
85
|
+
expect(div.childNodes[3].textContent).toBe("42");
|
|
86
|
+
expect(div.childNodes[4].tagName).toBe("DIV");
|
|
87
|
+
});
|
|
88
|
+
it("should render element with boolean children", () => {
|
|
89
|
+
// Create a container element
|
|
90
|
+
const container = document.createElement("div");
|
|
91
|
+
// Create vnode with boolean children
|
|
92
|
+
const vnode = jsx("div", {
|
|
93
|
+
children: [true, false, "visible"],
|
|
94
|
+
});
|
|
95
|
+
// Render the vnode
|
|
96
|
+
render(vnode, container);
|
|
97
|
+
// Verify booleans render as empty (like React)
|
|
98
|
+
const div = container.children[0];
|
|
99
|
+
// Should only have the text node "visible", booleans should be skipped
|
|
100
|
+
expect(div.childNodes.length).toBe(1);
|
|
101
|
+
expect(div.childNodes[0].textContent).toBe("visible");
|
|
102
|
+
});
|
|
103
|
+
it("should render element with zero as child", () => {
|
|
104
|
+
// Create a container element
|
|
105
|
+
const container = document.createElement("div");
|
|
106
|
+
// Create vnode with zero as child (should render, not be treated as falsy)
|
|
107
|
+
const vnode = jsx("div", {
|
|
108
|
+
children: 0,
|
|
109
|
+
});
|
|
110
|
+
// Render the vnode
|
|
111
|
+
render(vnode, container);
|
|
112
|
+
// Verify zero is rendered
|
|
113
|
+
const div = container.children[0];
|
|
114
|
+
expect(div.textContent).toBe("0");
|
|
115
|
+
expect(div.childNodes.length).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
it("should render element with empty string as child", () => {
|
|
118
|
+
// Create a container element
|
|
119
|
+
const container = document.createElement("div");
|
|
120
|
+
// Create vnode with empty string as child
|
|
121
|
+
const vnode = jsx("div", {
|
|
122
|
+
children: "",
|
|
123
|
+
});
|
|
124
|
+
// Render the vnode
|
|
125
|
+
render(vnode, container);
|
|
126
|
+
// Verify empty string renders as empty text node
|
|
127
|
+
const div = container.children[0];
|
|
128
|
+
expect(div.textContent).toBe("");
|
|
129
|
+
expect(div.childNodes.length).toBe(1);
|
|
130
|
+
expect(div.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RootVNode } from "./RootVNode";
|
|
2
|
+
import { VNode } from "./types";
|
|
3
|
+
export declare abstract class AbstractVNode {
|
|
4
|
+
key?: string;
|
|
5
|
+
parent?: VNode;
|
|
6
|
+
root?: RootVNode;
|
|
7
|
+
elm?: Node;
|
|
8
|
+
children?: VNode[];
|
|
9
|
+
abstract mount(parent?: VNode): Node | Node[];
|
|
10
|
+
abstract patch(oldNode: VNode): void;
|
|
11
|
+
abstract unmount(): void;
|
|
12
|
+
abstract rerender(): void;
|
|
13
|
+
protected getHTMLElement(): HTMLElement;
|
|
14
|
+
/**
|
|
15
|
+
* A VNode can represent multiple elements (fragment of component)
|
|
16
|
+
*/
|
|
17
|
+
getElements(): Node[];
|
|
18
|
+
getParentElement(): HTMLElement;
|
|
19
|
+
protected canPatch(oldNode: VNode, newNode: VNode): boolean;
|
|
20
|
+
patchChildren(newChildren: VNode[]): VNode[];
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=AbstractVNode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;CA2D7C"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class AbstractVNode {
|
|
2
|
+
key;
|
|
3
|
+
parent;
|
|
4
|
+
root;
|
|
5
|
+
elm;
|
|
6
|
+
children;
|
|
7
|
+
getHTMLElement() {
|
|
8
|
+
if (!this.elm || !(this.elm instanceof HTMLElement)) {
|
|
9
|
+
throw new Error("This VNode does not have an HTMLElement");
|
|
10
|
+
}
|
|
11
|
+
return this.elm;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A VNode can represent multiple elements (fragment of component)
|
|
15
|
+
*/
|
|
16
|
+
getElements() {
|
|
17
|
+
if (this.elm) {
|
|
18
|
+
return [this.elm];
|
|
19
|
+
}
|
|
20
|
+
if (!this.children) {
|
|
21
|
+
throw new Error("This VNode has no element or children");
|
|
22
|
+
}
|
|
23
|
+
return this.children.map((child) => child.getElements()).flat();
|
|
24
|
+
}
|
|
25
|
+
getParentElement() {
|
|
26
|
+
let parent = this.parent;
|
|
27
|
+
// This VNode might not have an element, but relies
|
|
28
|
+
// on a parent for it. So we make sure that we get
|
|
29
|
+
// the actual parent of the element related to this VNode
|
|
30
|
+
while (parent) {
|
|
31
|
+
if (parent.elm instanceof HTMLElement) {
|
|
32
|
+
// This will always be an HTMLElement as text nodes has no children
|
|
33
|
+
return parent.elm;
|
|
34
|
+
}
|
|
35
|
+
parent = parent.parent;
|
|
36
|
+
}
|
|
37
|
+
throw new Error("There is no parent element for this VNode");
|
|
38
|
+
}
|
|
39
|
+
canPatch(oldNode, newNode) {
|
|
40
|
+
// Must be same constructor type
|
|
41
|
+
if (oldNode.constructor !== newNode.constructor) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// For ElementVNodes, must have same tag
|
|
45
|
+
if ("tag" in oldNode && "tag" in newNode) {
|
|
46
|
+
return oldNode.tag === newNode.tag;
|
|
47
|
+
}
|
|
48
|
+
// For ComponentVNodes, must have same component function
|
|
49
|
+
if ("component" in oldNode && "component" in newNode) {
|
|
50
|
+
return oldNode.component === newNode.component;
|
|
51
|
+
}
|
|
52
|
+
// TextVNodes and FragmentVNodes can always patch
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
patchChildren(newChildren) {
|
|
56
|
+
const prevChildren = this.children;
|
|
57
|
+
// When there are only new children, we just mount them
|
|
58
|
+
if (newChildren && prevChildren.length === 0) {
|
|
59
|
+
newChildren.forEach((child) => child.mount(this));
|
|
60
|
+
return newChildren;
|
|
61
|
+
}
|
|
62
|
+
// If we want to remove all children, we just unmount the previous ones
|
|
63
|
+
if (!newChildren.length && prevChildren.length) {
|
|
64
|
+
prevChildren.forEach((child) => child.unmount());
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
const oldKeys = {};
|
|
68
|
+
prevChildren.forEach((prevChild, index) => {
|
|
69
|
+
oldKeys[prevChild.key || index] = prevChild;
|
|
70
|
+
});
|
|
71
|
+
// Build result array in the NEW order
|
|
72
|
+
const result = [];
|
|
73
|
+
newChildren.forEach((newChild, index) => {
|
|
74
|
+
const key = newChild.key || index;
|
|
75
|
+
const prevChild = oldKeys[key];
|
|
76
|
+
if (!prevChild) {
|
|
77
|
+
// New child - mount and add to result
|
|
78
|
+
newChild.mount(this);
|
|
79
|
+
result.push(newChild);
|
|
80
|
+
}
|
|
81
|
+
else if (prevChild === newChild) {
|
|
82
|
+
// Same instance - no patching needed, just reuse
|
|
83
|
+
result.push(prevChild);
|
|
84
|
+
delete oldKeys[key];
|
|
85
|
+
}
|
|
86
|
+
else if (this.canPatch(prevChild, newChild)) {
|
|
87
|
+
// Compatible types - patch and reuse old VNode
|
|
88
|
+
prevChild.patch(newChild);
|
|
89
|
+
result.push(prevChild);
|
|
90
|
+
delete oldKeys[key];
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Incompatible types - replace completely
|
|
94
|
+
newChild.mount(this);
|
|
95
|
+
prevChild.unmount();
|
|
96
|
+
result.push(newChild);
|
|
97
|
+
delete oldKeys[key];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
// Unmount any old children that weren't reused
|
|
101
|
+
for (const key in oldKeys) {
|
|
102
|
+
oldKeys[key].unmount();
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
}
|