rask-ui 0.2.3 → 0.2.5
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/tests/patchChildren.test.js +66 -187
- package/dist/vdom/AbstractVNode.d.ts +2 -18
- package/dist/vdom/AbstractVNode.d.ts.map +1 -1
- package/dist/vdom/AbstractVNode.js +27 -167
- package/dist/vdom/ComponentVNode.d.ts +1 -0
- package/dist/vdom/ComponentVNode.d.ts.map +1 -1
- package/dist/vdom/ComponentVNode.js +6 -11
- package/dist/vdom/ElementVNode.d.ts +7 -0
- package/dist/vdom/ElementVNode.d.ts.map +1 -1
- package/dist/vdom/ElementVNode.js +35 -2
- package/dist/vdom/FragmentVNode.d.ts +1 -0
- package/dist/vdom/FragmentVNode.d.ts.map +1 -1
- package/dist/vdom/FragmentVNode.js +7 -2
- package/dist/vdom/RootVNode.d.ts +2 -2
- package/dist/vdom/RootVNode.d.ts.map +1 -1
- package/dist/vdom/RootVNode.js +5 -2
- package/dist/vdom/TextVNode.d.ts +1 -0
- package/dist/vdom/TextVNode.d.ts.map +1 -1
- package/dist/vdom/TextVNode.js +1 -0
- package/package.json +1 -1
|
@@ -11,13 +11,11 @@ class MockVNode extends AbstractVNode {
|
|
|
11
11
|
constructor(key) {
|
|
12
12
|
super();
|
|
13
13
|
this.key = key;
|
|
14
|
-
// Initialize elm in constructor so getElements() always works
|
|
15
|
-
this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
|
|
16
14
|
}
|
|
17
15
|
mount(parent) {
|
|
18
16
|
this.mountCalls++;
|
|
19
17
|
this.parent = parent;
|
|
20
|
-
|
|
18
|
+
this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
|
|
21
19
|
return this.elm;
|
|
22
20
|
}
|
|
23
21
|
patch(newNode) {
|
|
@@ -28,8 +26,7 @@ class MockVNode extends AbstractVNode {
|
|
|
28
26
|
}
|
|
29
27
|
unmount() {
|
|
30
28
|
this.unmountCalls++;
|
|
31
|
-
|
|
32
|
-
// delete this.elm;
|
|
29
|
+
delete this.elm;
|
|
33
30
|
delete this.parent;
|
|
34
31
|
}
|
|
35
32
|
rerender() {
|
|
@@ -40,8 +37,6 @@ class MockVNode extends AbstractVNode {
|
|
|
40
37
|
class MockParentVNode extends MockVNode {
|
|
41
38
|
constructor(initialChildren, key) {
|
|
42
39
|
super(key);
|
|
43
|
-
// Override elm with an HTMLElement since parents need to contain children
|
|
44
|
-
this.elm = document.createElement("div");
|
|
45
40
|
this.children = (initialChildren || []);
|
|
46
41
|
}
|
|
47
42
|
}
|
|
@@ -51,33 +46,27 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
51
46
|
const newChild1 = new MockVNode("a");
|
|
52
47
|
const newChild2 = new MockVNode("b");
|
|
53
48
|
const parent = new MockParentVNode([]);
|
|
54
|
-
const { children,
|
|
49
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
55
50
|
// New children should be mounted
|
|
56
51
|
expect(newChild1.mountCalls).toBe(1);
|
|
57
52
|
expect(newChild2.mountCalls).toBe(1);
|
|
58
53
|
expect(newChild1.parent).toBe(parent);
|
|
59
54
|
expect(newChild2.parent).toBe(parent);
|
|
60
55
|
// Result should be the new children (since old was empty)
|
|
61
|
-
expect(
|
|
62
|
-
|
|
63
|
-
expect(operations).toHaveLength(1);
|
|
64
|
-
expect(operations[0].type).toBe("insert");
|
|
65
|
-
expect(operations[0]).toHaveProperty("elms");
|
|
56
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
57
|
+
expect(hasChangedStructure).toBe(true);
|
|
66
58
|
});
|
|
67
59
|
it("should unmount all old children when new is empty", () => {
|
|
68
60
|
const oldChild1 = new MockVNode("a");
|
|
69
61
|
const oldChild2 = new MockVNode("b");
|
|
70
62
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
71
|
-
const { children,
|
|
63
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
|
|
72
64
|
// Old children should be unmounted
|
|
73
65
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
74
66
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
75
67
|
// Result should be empty
|
|
76
|
-
expect(
|
|
77
|
-
|
|
78
|
-
expect(operations).toHaveLength(1);
|
|
79
|
-
expect(operations[0].type).toBe("remove");
|
|
80
|
-
expect(operations[0]).toHaveProperty("elms");
|
|
68
|
+
expect(result).toEqual([]);
|
|
69
|
+
expect(hasChangedStructure).toBe(true);
|
|
81
70
|
});
|
|
82
71
|
});
|
|
83
72
|
describe("Patching with keys", () => {
|
|
@@ -89,7 +78,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
89
78
|
const newChild1 = new MockVNode("a");
|
|
90
79
|
const newChild2 = new MockVNode("b");
|
|
91
80
|
const newChild3 = new MockVNode("c");
|
|
92
|
-
const { children,
|
|
81
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
93
82
|
// OLD children should be patched with new children
|
|
94
83
|
expect(oldChild1.patchCalls).toBe(1);
|
|
95
84
|
expect(oldChild2.patchCalls).toBe(1);
|
|
@@ -106,9 +95,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
106
95
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
107
96
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
108
97
|
// Result should still be the OLD children (reused)
|
|
109
|
-
expect(
|
|
110
|
-
|
|
111
|
-
expect(operations).toHaveLength(0);
|
|
98
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
99
|
+
expect(hasChangedStructure).toBe(false);
|
|
112
100
|
});
|
|
113
101
|
it("should handle reordered children with keys", () => {
|
|
114
102
|
const oldChild1 = new MockVNode("a");
|
|
@@ -119,7 +107,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
119
107
|
const newChild1 = new MockVNode("c");
|
|
120
108
|
const newChild2 = new MockVNode("a");
|
|
121
109
|
const newChild3 = new MockVNode("b");
|
|
122
|
-
const { children,
|
|
110
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
123
111
|
// Old nodes should be patched with corresponding new nodes by key
|
|
124
112
|
expect(oldChild1.patchedWith).toBe(newChild2); // a->a
|
|
125
113
|
expect(oldChild2.patchedWith).toBe(newChild3); // b->b
|
|
@@ -129,14 +117,12 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
129
117
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
130
118
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
131
119
|
// Result should be old children in NEW order (c, a, b)
|
|
132
|
-
expect(
|
|
120
|
+
expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
|
|
133
121
|
// Verify correct keys
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
136
|
-
expect(
|
|
137
|
-
|
|
138
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
139
|
-
expect(operations.some((op) => op.type === "move" || op.type === "insert")).toBe(true);
|
|
122
|
+
expect(result[0].key).toBe("c");
|
|
123
|
+
expect(result[1].key).toBe("a");
|
|
124
|
+
expect(result[2].key).toBe("b");
|
|
125
|
+
expect(hasChangedStructure).toBe(true);
|
|
140
126
|
});
|
|
141
127
|
it("should mount new children and unmount removed children", () => {
|
|
142
128
|
const oldChild1 = new MockVNode("a");
|
|
@@ -147,7 +133,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
147
133
|
const newChild1 = new MockVNode("a");
|
|
148
134
|
const newChild2 = new MockVNode("c");
|
|
149
135
|
const newChild3 = new MockVNode("d");
|
|
150
|
-
const { children,
|
|
136
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
151
137
|
// a and c should be patched (reused)
|
|
152
138
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
153
139
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -156,44 +142,12 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
156
142
|
// b should be unmounted
|
|
157
143
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
158
144
|
// Result should contain old a, old c, and new d
|
|
159
|
-
expect(
|
|
160
|
-
expect(
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
163
|
-
expect(
|
|
164
|
-
|
|
165
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
166
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
167
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
it("should generate move operations when children are reordered", () => {
|
|
170
|
-
const oldChild1 = new MockVNode("a");
|
|
171
|
-
const oldChild2 = new MockVNode("b");
|
|
172
|
-
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
173
|
-
// Swap order: b, a
|
|
174
|
-
const newChild1 = new MockVNode("b");
|
|
175
|
-
const newChild2 = new MockVNode("a");
|
|
176
|
-
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
177
|
-
// Both should be patched
|
|
178
|
-
expect(oldChild1.patchedWith).toBe(newChild2);
|
|
179
|
-
expect(oldChild2.patchedWith).toBe(newChild1);
|
|
180
|
-
// No unmounts
|
|
181
|
-
expect(oldChild1.unmountCalls).toBe(0);
|
|
182
|
-
expect(oldChild2.unmountCalls).toBe(0);
|
|
183
|
-
// Result should be old children in new order [b, a]
|
|
184
|
-
expect(children).toEqual([oldChild2, oldChild1]);
|
|
185
|
-
// Should have move or insert operations for repositioning
|
|
186
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
187
|
-
const hasMoveOrInsert = operations.some((op) => op.type === "move" || op.type === "insert");
|
|
188
|
-
expect(hasMoveOrInsert).toBe(true);
|
|
189
|
-
// If there's a move operation, it should have the required properties
|
|
190
|
-
const moveOps = operations.filter((op) => op.type === "move");
|
|
191
|
-
moveOps.forEach((op) => {
|
|
192
|
-
if (op.type === "move") {
|
|
193
|
-
expect(op).toHaveProperty("elms");
|
|
194
|
-
expect(op).toHaveProperty("afterElm");
|
|
195
|
-
}
|
|
196
|
-
});
|
|
145
|
+
expect(result).toContain(oldChild1);
|
|
146
|
+
expect(result).toContain(oldChild3);
|
|
147
|
+
expect(result).toContain(newChild3);
|
|
148
|
+
expect(result).not.toContain(oldChild2);
|
|
149
|
+
expect(result.length).toBe(3);
|
|
150
|
+
expect(hasChangedStructure).toBe(true);
|
|
197
151
|
});
|
|
198
152
|
it("should replace all children when all keys change", () => {
|
|
199
153
|
const oldChild1 = new MockVNode("a");
|
|
@@ -201,7 +155,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
201
155
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
202
156
|
const newChild1 = new MockVNode("x");
|
|
203
157
|
const newChild2 = new MockVNode("y");
|
|
204
|
-
const { children,
|
|
158
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
205
159
|
// All new children should be mounted
|
|
206
160
|
expect(newChild1.mountCalls).toBe(1);
|
|
207
161
|
expect(newChild2.mountCalls).toBe(1);
|
|
@@ -209,11 +163,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
209
163
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
210
164
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
211
165
|
// Result should be the new children
|
|
212
|
-
expect(
|
|
213
|
-
|
|
214
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
215
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
216
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
166
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
167
|
+
expect(hasChangedStructure).toBe(true);
|
|
217
168
|
});
|
|
218
169
|
});
|
|
219
170
|
describe("Patching without keys (index-based)", () => {
|
|
@@ -225,7 +176,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
225
176
|
const newChild1 = new MockVNode();
|
|
226
177
|
const newChild2 = new MockVNode();
|
|
227
178
|
const newChild3 = new MockVNode();
|
|
228
|
-
const { children,
|
|
179
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
229
180
|
// Should patch by index: 0->0, 1->1, 2->2
|
|
230
181
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
231
182
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -235,9 +186,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
235
186
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
236
187
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
237
188
|
// Result should be old children (reused)
|
|
238
|
-
expect(
|
|
239
|
-
|
|
240
|
-
expect(operations).toHaveLength(0);
|
|
189
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
190
|
+
expect(hasChangedStructure).toBe(false);
|
|
241
191
|
});
|
|
242
192
|
it("should mount new children when growing without keys", () => {
|
|
243
193
|
const oldChild1 = new MockVNode();
|
|
@@ -247,7 +197,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
247
197
|
const newChild2 = new MockVNode();
|
|
248
198
|
const newChild3 = new MockVNode();
|
|
249
199
|
const newChild4 = new MockVNode();
|
|
250
|
-
const { children,
|
|
200
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
|
|
251
201
|
// First two should be patched (reused)
|
|
252
202
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
253
203
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -258,10 +208,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
258
208
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
259
209
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
260
210
|
// Result should be [oldChild1, oldChild2, newChild3, newChild4]
|
|
261
|
-
expect(
|
|
262
|
-
|
|
263
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
264
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
211
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
|
|
212
|
+
expect(hasChangedStructure).toBe(true);
|
|
265
213
|
});
|
|
266
214
|
it("should unmount old children when shrinking without keys", () => {
|
|
267
215
|
const oldChild1 = new MockVNode();
|
|
@@ -276,7 +224,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
276
224
|
]);
|
|
277
225
|
const newChild1 = new MockVNode();
|
|
278
226
|
const newChild2 = new MockVNode();
|
|
279
|
-
const { children,
|
|
227
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
280
228
|
// First two should be patched
|
|
281
229
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
282
230
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -287,10 +235,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
287
235
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
288
236
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
289
237
|
// Result should be [oldChild1, oldChild2]
|
|
290
|
-
expect(
|
|
291
|
-
|
|
292
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
293
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
238
|
+
expect(result).toEqual([oldChild1, oldChild2]);
|
|
239
|
+
expect(hasChangedStructure).toBe(true);
|
|
294
240
|
});
|
|
295
241
|
});
|
|
296
242
|
describe("Mixed keys and indices", () => {
|
|
@@ -302,7 +248,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
302
248
|
const newChild1 = new MockVNode("a"); // key: "a"
|
|
303
249
|
const newChild2 = new MockVNode(); // key: undefined -> index 1
|
|
304
250
|
const newChild3 = new MockVNode("c"); // key: "c"
|
|
305
|
-
const { children,
|
|
251
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
306
252
|
// Keyed children should patch by key
|
|
307
253
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
308
254
|
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
@@ -313,9 +259,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
313
259
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
314
260
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
315
261
|
// Result should be old children (reused)
|
|
316
|
-
expect(
|
|
317
|
-
|
|
318
|
-
expect(operations).toHaveLength(0);
|
|
262
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
263
|
+
expect(hasChangedStructure).toBe(false);
|
|
319
264
|
});
|
|
320
265
|
});
|
|
321
266
|
describe("Real-world scenarios", () => {
|
|
@@ -324,7 +269,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
324
269
|
const parent = new MockParentVNode([oldChild1]);
|
|
325
270
|
const newChild1 = new MockVNode("title");
|
|
326
271
|
const newChild2 = new MockVNode("details");
|
|
327
|
-
const { children,
|
|
272
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
328
273
|
// Title should be patched (reused)
|
|
329
274
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
330
275
|
// Details should be mounted
|
|
@@ -332,17 +277,15 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
332
277
|
// No unmounts
|
|
333
278
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
334
279
|
// Result should be [oldChild1, newChild2]
|
|
335
|
-
expect(
|
|
336
|
-
|
|
337
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
338
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
280
|
+
expect(result).toEqual([oldChild1, newChild2]);
|
|
281
|
+
expect(hasChangedStructure).toBe(true);
|
|
339
282
|
});
|
|
340
283
|
it("should handle conditional rendering (component -> null)", () => {
|
|
341
284
|
const oldChild1 = new MockVNode("title");
|
|
342
285
|
const oldChild2 = new MockVNode("details");
|
|
343
286
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
344
287
|
const newChild1 = new MockVNode("title");
|
|
345
|
-
const { children,
|
|
288
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1]));
|
|
346
289
|
// Title should be patched (reused)
|
|
347
290
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
348
291
|
// Details should be unmounted
|
|
@@ -350,10 +293,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
350
293
|
// Title should not be unmounted
|
|
351
294
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
352
295
|
// Result should be [oldChild1]
|
|
353
|
-
expect(
|
|
354
|
-
|
|
355
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
356
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
296
|
+
expect(result).toEqual([oldChild1]);
|
|
297
|
+
expect(hasChangedStructure).toBe(true);
|
|
357
298
|
});
|
|
358
299
|
it("should handle list with items added at beginning", () => {
|
|
359
300
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -362,7 +303,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
362
303
|
const newChild1 = new MockVNode("item-0"); // New item at start
|
|
363
304
|
const newChild2 = new MockVNode("item-1");
|
|
364
305
|
const newChild3 = new MockVNode("item-2");
|
|
365
|
-
const { children,
|
|
306
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
366
307
|
// New item should be mounted
|
|
367
308
|
expect(newChild1.mountCalls).toBe(1);
|
|
368
309
|
// Existing items should be patched (reused)
|
|
@@ -372,10 +313,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
372
313
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
373
314
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
374
315
|
// Result should be [newChild1, oldChild1, oldChild2]
|
|
375
|
-
expect(
|
|
376
|
-
|
|
377
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
378
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
316
|
+
expect(result).toEqual([newChild1, oldChild1, oldChild2]);
|
|
317
|
+
expect(hasChangedStructure).toBe(true);
|
|
379
318
|
});
|
|
380
319
|
it("should handle list with items added at end", () => {
|
|
381
320
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -384,7 +323,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
384
323
|
const newChild1 = new MockVNode("item-1");
|
|
385
324
|
const newChild2 = new MockVNode("item-2");
|
|
386
325
|
const newChild3 = new MockVNode("item-3"); // New item at end
|
|
387
|
-
const { children,
|
|
326
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
388
327
|
// Existing items should be patched (reused)
|
|
389
328
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
390
329
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -394,10 +333,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
394
333
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
395
334
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
396
335
|
// Result should be [oldChild1, oldChild2, newChild3]
|
|
397
|
-
expect(
|
|
398
|
-
|
|
399
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
400
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
336
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3]);
|
|
337
|
+
expect(hasChangedStructure).toBe(true);
|
|
401
338
|
});
|
|
402
339
|
it("should handle list with item removed from middle", () => {
|
|
403
340
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -406,7 +343,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
406
343
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
407
344
|
const newChild1 = new MockVNode("item-1");
|
|
408
345
|
const newChild2 = new MockVNode("item-3"); // item-2 removed
|
|
409
|
-
const { children,
|
|
346
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
410
347
|
// item-1 and item-3 should be patched (reused)
|
|
411
348
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
412
349
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -416,42 +353,36 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
416
353
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
417
354
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
418
355
|
// Result should be [oldChild1, oldChild3]
|
|
419
|
-
expect(
|
|
420
|
-
|
|
421
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
422
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
356
|
+
expect(result).toEqual([oldChild1, oldChild3]);
|
|
357
|
+
expect(hasChangedStructure).toBe(true);
|
|
423
358
|
});
|
|
424
359
|
it("should handle empty -> multiple children", () => {
|
|
425
360
|
const parent = new MockParentVNode([]);
|
|
426
361
|
const newChild1 = new MockVNode("a");
|
|
427
362
|
const newChild2 = new MockVNode("b");
|
|
428
363
|
const newChild3 = new MockVNode("c");
|
|
429
|
-
const { children,
|
|
364
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
430
365
|
// All should be mounted
|
|
431
366
|
expect(newChild1.mountCalls).toBe(1);
|
|
432
367
|
expect(newChild2.mountCalls).toBe(1);
|
|
433
368
|
expect(newChild3.mountCalls).toBe(1);
|
|
434
369
|
// Result should be the new children
|
|
435
|
-
expect(
|
|
436
|
-
|
|
437
|
-
expect(operations).toHaveLength(1);
|
|
438
|
-
expect(operations[0].type).toBe("insert");
|
|
370
|
+
expect(result).toEqual([newChild1, newChild2, newChild3]);
|
|
371
|
+
expect(hasChangedStructure).toBe(true);
|
|
439
372
|
});
|
|
440
373
|
it("should handle multiple children -> empty", () => {
|
|
441
374
|
const oldChild1 = new MockVNode("a");
|
|
442
375
|
const oldChild2 = new MockVNode("b");
|
|
443
376
|
const oldChild3 = new MockVNode("c");
|
|
444
377
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
445
|
-
const { children,
|
|
378
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
|
|
446
379
|
// All should be unmounted
|
|
447
380
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
448
381
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
449
382
|
expect(oldChild3.unmountCalls).toBe(1);
|
|
450
383
|
// Result should be empty
|
|
451
|
-
expect(
|
|
452
|
-
|
|
453
|
-
expect(operations).toHaveLength(1);
|
|
454
|
-
expect(operations[0].type).toBe("remove");
|
|
384
|
+
expect(result).toEqual([]);
|
|
385
|
+
expect(hasChangedStructure).toBe(true);
|
|
455
386
|
});
|
|
456
387
|
});
|
|
457
388
|
describe("Object reference preservation", () => {
|
|
@@ -461,66 +392,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
461
392
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
462
393
|
const newChild1 = new MockVNode("a");
|
|
463
394
|
const newChild2 = new MockVNode("b");
|
|
464
|
-
const { children,
|
|
395
|
+
const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
465
396
|
// The result should contain the EXACT SAME object references as the old children
|
|
466
|
-
expect(
|
|
467
|
-
expect(
|
|
397
|
+
expect(result[0]).toBe(oldChild1); // Same object reference
|
|
398
|
+
expect(result[1]).toBe(oldChild2); // Same object reference
|
|
468
399
|
// NOT the new children
|
|
469
|
-
expect(
|
|
470
|
-
expect(
|
|
471
|
-
|
|
472
|
-
expect(operations).toHaveLength(0);
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
describe("Move operations - minimal reproduction", () => {
|
|
476
|
-
it("should correctly reorder [A,B,C] to [C,A,B] in actual DOM", () => {
|
|
477
|
-
// This test uses actual DOM to verify move operations work correctly
|
|
478
|
-
const parent = document.createElement("div");
|
|
479
|
-
// Create initial DOM elements
|
|
480
|
-
const elmA = document.createTextNode("A");
|
|
481
|
-
const elmB = document.createTextNode("B");
|
|
482
|
-
const elmC = document.createTextNode("C");
|
|
483
|
-
parent.appendChild(elmA);
|
|
484
|
-
parent.appendChild(elmB);
|
|
485
|
-
parent.appendChild(elmC);
|
|
486
|
-
// Create VNodes that represent these elements
|
|
487
|
-
const oldChild1 = new MockVNode("a");
|
|
488
|
-
const oldChild2 = new MockVNode("b");
|
|
489
|
-
const oldChild3 = new MockVNode("c");
|
|
490
|
-
oldChild1.elm = elmA;
|
|
491
|
-
oldChild2.elm = elmB;
|
|
492
|
-
oldChild3.elm = elmC;
|
|
493
|
-
const parentVNode = new MockParentVNode([
|
|
494
|
-
oldChild1,
|
|
495
|
-
oldChild2,
|
|
496
|
-
oldChild3,
|
|
497
|
-
]);
|
|
498
|
-
parentVNode.elm = parent;
|
|
499
|
-
// Verify initial DOM order
|
|
500
|
-
expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
|
|
501
|
-
"A",
|
|
502
|
-
"B",
|
|
503
|
-
"C",
|
|
504
|
-
]);
|
|
505
|
-
// Reorder to C, A, B
|
|
506
|
-
const newChild1 = new MockVNode("c");
|
|
507
|
-
const newChild2 = new MockVNode("a");
|
|
508
|
-
const newChild3 = new MockVNode("b");
|
|
509
|
-
const { children, operations } = parentVNode.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
510
|
-
// Result should be old children in new order
|
|
511
|
-
expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
|
|
512
|
-
// Apply the operations to the actual DOM
|
|
513
|
-
parentVNode.applyDOMOperations(operations);
|
|
514
|
-
// Verify DOM is correctly reordered
|
|
515
|
-
expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
|
|
516
|
-
"C",
|
|
517
|
-
"A",
|
|
518
|
-
"B",
|
|
519
|
-
]);
|
|
520
|
-
// Verify same DOM elements were moved, not recreated
|
|
521
|
-
expect(parent.childNodes[0]).toBe(elmC);
|
|
522
|
-
expect(parent.childNodes[1]).toBe(elmA);
|
|
523
|
-
expect(parent.childNodes[2]).toBe(elmB);
|
|
400
|
+
expect(result[0]).not.toBe(newChild1);
|
|
401
|
+
expect(result[1]).not.toBe(newChild2);
|
|
402
|
+
expect(hasChangedStructure).toBe(false);
|
|
524
403
|
});
|
|
525
404
|
});
|
|
526
405
|
});
|
|
@@ -1,21 +1,5 @@
|
|
|
1
1
|
import { RootVNode } from "./RootVNode";
|
|
2
2
|
import { VNode } from "./types";
|
|
3
|
-
export type DOMOperation = {
|
|
4
|
-
type: "replace";
|
|
5
|
-
oldElms: Node[];
|
|
6
|
-
newElms: Node[];
|
|
7
|
-
} | {
|
|
8
|
-
type: "remove";
|
|
9
|
-
elms: Node[];
|
|
10
|
-
} | {
|
|
11
|
-
type: "insert";
|
|
12
|
-
elms: Node[];
|
|
13
|
-
afterElm?: Node;
|
|
14
|
-
} | {
|
|
15
|
-
type: "move";
|
|
16
|
-
elms: Node[];
|
|
17
|
-
afterElm?: Node;
|
|
18
|
-
};
|
|
19
3
|
export declare abstract class AbstractVNode {
|
|
20
4
|
key?: string;
|
|
21
5
|
parent?: VNode;
|
|
@@ -25,7 +9,7 @@ export declare abstract class AbstractVNode {
|
|
|
25
9
|
abstract mount(parent?: VNode): Node | Node[];
|
|
26
10
|
abstract patch(oldNode: VNode): void;
|
|
27
11
|
abstract unmount(): void;
|
|
28
|
-
|
|
12
|
+
abstract rerender(): void;
|
|
29
13
|
protected getHTMLElement(): HTMLElement;
|
|
30
14
|
/**
|
|
31
15
|
* A VNode can represent multiple elements (fragment of component)
|
|
@@ -35,7 +19,7 @@ export declare abstract class AbstractVNode {
|
|
|
35
19
|
protected canPatch(oldNode: VNode, newNode: VNode): boolean;
|
|
36
20
|
patchChildren(newChildren: VNode[]): {
|
|
37
21
|
children: VNode[];
|
|
38
|
-
|
|
22
|
+
hasChangedStructure: boolean;
|
|
39
23
|
};
|
|
40
24
|
}
|
|
41
25
|
//# sourceMappingURL=AbstractVNode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"
|
|
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;QACnC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClB,mBAAmB,EAAE,OAAO,CAAC;KAC9B;CAqEF"}
|
|
@@ -1,77 +1,9 @@
|
|
|
1
|
-
import { elementsToFragment } from "./dom-utils";
|
|
2
1
|
export class AbstractVNode {
|
|
3
2
|
key;
|
|
4
3
|
parent;
|
|
5
4
|
root;
|
|
6
5
|
elm;
|
|
7
6
|
children;
|
|
8
|
-
applyDOMOperations(operations, atVNode) {
|
|
9
|
-
if (!this.elm || !this.children) {
|
|
10
|
-
this.parent?.applyDOMOperations(operations, this);
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
console.log(operations);
|
|
14
|
-
operations.forEach((operation) => {
|
|
15
|
-
switch (operation.type) {
|
|
16
|
-
case "insert": {
|
|
17
|
-
const fragment = elementsToFragment(operation.elms);
|
|
18
|
-
// Insert after afterElm (or at start if undefined)
|
|
19
|
-
if (operation.afterElm === undefined) {
|
|
20
|
-
// Insert at the start
|
|
21
|
-
this.elm.insertBefore(fragment, this.elm.firstChild);
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
// Insert after afterElm
|
|
25
|
-
const target = operation.afterElm.nextSibling;
|
|
26
|
-
this.elm.insertBefore(fragment, target);
|
|
27
|
-
}
|
|
28
|
-
break;
|
|
29
|
-
}
|
|
30
|
-
case "remove": {
|
|
31
|
-
const elms = operation.elms;
|
|
32
|
-
if (elms.length === 1) {
|
|
33
|
-
this.elm.removeChild(elms[0]);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
const range = new Range();
|
|
37
|
-
range.setStartBefore(elms[0]);
|
|
38
|
-
range.setEndAfter(elms[elms.length - 1]);
|
|
39
|
-
range.deleteContents();
|
|
40
|
-
}
|
|
41
|
-
break;
|
|
42
|
-
}
|
|
43
|
-
case "replace": {
|
|
44
|
-
const oldElms = operation.oldElms;
|
|
45
|
-
const newElms = operation.newElms;
|
|
46
|
-
if (oldElms.length === 1) {
|
|
47
|
-
this.elm.replaceChild(elementsToFragment(newElms), oldElms[0]);
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
const range = new Range();
|
|
51
|
-
range.setStartBefore(oldElms[0]);
|
|
52
|
-
range.setEndAfter(oldElms[oldElms.length - 1]);
|
|
53
|
-
range.deleteContents();
|
|
54
|
-
range.insertNode(elementsToFragment(newElms));
|
|
55
|
-
}
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
case "move": {
|
|
59
|
-
const fragment = elementsToFragment(operation.elms);
|
|
60
|
-
// Insert after afterElm (or at start if undefined)
|
|
61
|
-
if (operation.afterElm === undefined) {
|
|
62
|
-
// Insert at the start
|
|
63
|
-
this.elm.insertBefore(fragment, this.elm.firstChild);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
// Insert after afterElm
|
|
67
|
-
const target = operation.afterElm.nextSibling;
|
|
68
|
-
this.elm.insertBefore(fragment, target);
|
|
69
|
-
}
|
|
70
|
-
break;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
7
|
getHTMLElement() {
|
|
76
8
|
if (!this.elm || !(this.elm instanceof HTMLElement)) {
|
|
77
9
|
throw new Error("This VNode does not have an HTMLElement");
|
|
@@ -86,7 +18,6 @@ export class AbstractVNode {
|
|
|
86
18
|
return [this.elm];
|
|
87
19
|
}
|
|
88
20
|
if (!this.children) {
|
|
89
|
-
console.log("WTF", this);
|
|
90
21
|
throw new Error("This VNode has no element or children");
|
|
91
22
|
}
|
|
92
23
|
return this.children.map((child) => child.getElements()).flat();
|
|
@@ -124,132 +55,61 @@ export class AbstractVNode {
|
|
|
124
55
|
patchChildren(newChildren) {
|
|
125
56
|
const prevChildren = this.children;
|
|
126
57
|
// When there are only new children, we just mount them
|
|
127
|
-
if (prevChildren.length === 0) {
|
|
58
|
+
if (newChildren && prevChildren.length === 0) {
|
|
128
59
|
newChildren.forEach((child) => child.mount(this));
|
|
129
|
-
return {
|
|
130
|
-
children: newChildren,
|
|
131
|
-
operations: [
|
|
132
|
-
{
|
|
133
|
-
type: "insert",
|
|
134
|
-
elms: newChildren.map((child) => child.getElements()).flat(),
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
60
|
+
return { children: newChildren, hasChangedStructure: true };
|
|
138
61
|
}
|
|
139
62
|
// If we want to remove all children, we just unmount the previous ones
|
|
140
63
|
if (!newChildren.length && prevChildren.length) {
|
|
141
64
|
prevChildren.forEach((child) => child.unmount());
|
|
142
|
-
return {
|
|
143
|
-
children: [],
|
|
144
|
-
operations: [
|
|
145
|
-
{
|
|
146
|
-
type: "remove",
|
|
147
|
-
elms: prevChildren.map((child) => child.getElements()).flat(),
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
};
|
|
65
|
+
return { children: [], hasChangedStructure: true };
|
|
151
66
|
}
|
|
152
|
-
const operations = [];
|
|
153
67
|
const oldKeys = {};
|
|
154
|
-
// Build oldKeys map and handle duplicate keys
|
|
155
68
|
prevChildren.forEach((prevChild, index) => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
operations.push({ type: "remove", elms: oldKeys[key].getElements() });
|
|
161
|
-
}
|
|
162
|
-
oldKeys[key] = prevChild;
|
|
69
|
+
oldKeys[prevChild.key || index] = {
|
|
70
|
+
vnode: prevChild,
|
|
71
|
+
index,
|
|
72
|
+
};
|
|
163
73
|
});
|
|
164
|
-
//
|
|
165
|
-
const getAfterElm = (index, result) => {
|
|
166
|
-
let currentIndex = index;
|
|
167
|
-
let prevChild = result[--currentIndex];
|
|
168
|
-
let afterElm;
|
|
169
|
-
while (prevChild) {
|
|
170
|
-
const prevElms = prevChild.getElements();
|
|
171
|
-
afterElm = prevElms[prevElms.length - 1];
|
|
172
|
-
if (afterElm) {
|
|
173
|
-
break;
|
|
174
|
-
}
|
|
175
|
-
prevChild = result[--currentIndex];
|
|
176
|
-
}
|
|
177
|
-
return afterElm;
|
|
178
|
-
};
|
|
179
|
-
// === PASS 1: Build result array (mount/patch all children) ===
|
|
74
|
+
// Build result array in the NEW order
|
|
180
75
|
const result = [];
|
|
181
|
-
|
|
76
|
+
let hasChangedStructure = false;
|
|
182
77
|
newChildren.forEach((newChild, index) => {
|
|
183
78
|
const key = newChild.key || index;
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
79
|
+
const prevChild = oldKeys[key];
|
|
80
|
+
if (!prevChild) {
|
|
186
81
|
// New child - mount and add to result
|
|
187
82
|
newChild.mount(this);
|
|
188
83
|
result.push(newChild);
|
|
189
|
-
|
|
84
|
+
hasChangedStructure = true;
|
|
85
|
+
}
|
|
86
|
+
else if (prevChild?.vnode === newChild) {
|
|
87
|
+
// Same instance - no patching needed, just reuse
|
|
88
|
+
result.push(prevChild.vnode);
|
|
89
|
+
delete oldKeys[key];
|
|
90
|
+
hasChangedStructure = hasChangedStructure || prevChild.index !== index;
|
|
190
91
|
}
|
|
191
|
-
else if (this.canPatch(
|
|
92
|
+
else if (this.canPatch(prevChild.vnode, newChild)) {
|
|
192
93
|
// Compatible types - patch and reuse old VNode
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
result.push(oldChild);
|
|
94
|
+
prevChild.vnode.patch(newChild);
|
|
95
|
+
result.push(prevChild.vnode);
|
|
197
96
|
delete oldKeys[key];
|
|
198
|
-
|
|
97
|
+
hasChangedStructure = hasChangedStructure || prevChild.index !== index;
|
|
199
98
|
}
|
|
200
99
|
else {
|
|
201
100
|
// Incompatible types - replace completely
|
|
202
101
|
newChild.mount(this);
|
|
203
|
-
|
|
102
|
+
prevChild.vnode.unmount();
|
|
204
103
|
result.push(newChild);
|
|
205
104
|
delete oldKeys[key];
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
});
|
|
209
|
-
// === PASS 2: Generate operations by comparing positions ===
|
|
210
|
-
result.forEach((child, newIndex) => {
|
|
211
|
-
const oldIndex = prevChildren.indexOf(child);
|
|
212
|
-
const meta = newChildrenMeta[newIndex];
|
|
213
|
-
if (meta.isNew) {
|
|
214
|
-
// New child - generate insert operation
|
|
215
|
-
const afterElm = getAfterElm(newIndex, result);
|
|
216
|
-
if (meta.replacedOld) {
|
|
217
|
-
// This is a replacement
|
|
218
|
-
const prevElms = meta.replacedOld.getElements();
|
|
219
|
-
if (prevElms.length) {
|
|
220
|
-
operations.push({
|
|
221
|
-
type: "replace",
|
|
222
|
-
oldElms: prevElms,
|
|
223
|
-
newElms: child.getElements(),
|
|
224
|
-
});
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
operations.push({
|
|
229
|
-
type: "insert",
|
|
230
|
-
elms: child.getElements(),
|
|
231
|
-
afterElm,
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
else if (oldIndex !== newIndex) {
|
|
235
|
-
// Existing child that moved - generate move operation
|
|
236
|
-
const afterElm = getAfterElm(newIndex, result);
|
|
237
|
-
operations.push({
|
|
238
|
-
type: "move",
|
|
239
|
-
elms: child.getElements(),
|
|
240
|
-
afterElm,
|
|
241
|
-
});
|
|
105
|
+
hasChangedStructure = true;
|
|
242
106
|
}
|
|
243
|
-
// else: child is in same position, no operation needed
|
|
244
107
|
});
|
|
245
108
|
// Unmount any old children that weren't reused
|
|
246
109
|
for (const key in oldKeys) {
|
|
247
|
-
oldKeys[key].unmount();
|
|
248
|
-
|
|
110
|
+
oldKeys[key].vnode.unmount();
|
|
111
|
+
hasChangedStructure = true;
|
|
249
112
|
}
|
|
250
|
-
return {
|
|
251
|
-
children: result,
|
|
252
|
-
operations,
|
|
253
|
-
};
|
|
113
|
+
return { children: result, hasChangedStructure };
|
|
254
114
|
}
|
|
255
115
|
}
|
|
@@ -40,6 +40,7 @@ export declare class ComponentVNode extends AbstractVNode {
|
|
|
40
40
|
children: VNode[];
|
|
41
41
|
instance?: ComponentInstance;
|
|
42
42
|
constructor(component: Component<any>, props: Props, children: VNode[], key?: string);
|
|
43
|
+
rerender(): void;
|
|
43
44
|
mount(parent?: VNode): Node[];
|
|
44
45
|
patch(newNode: ComponentVNode): void;
|
|
45
46
|
unmount(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,MAAM,GACN,IAAI,GACJ,MAAM,GACN,SAAS,GACT,OAAO,CAAC;AACZ,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,cAAc,EAAE,CAAC;AAElE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,KAAK,IACjC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,iBAAiB,CAAC,GACvC,CAAC,MAAM,MAAM,iBAAiB,CAAC,CAAC;AAEpC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAKF,wBAAgB,mBAAmB,sBAYlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAYrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAYvC;AAED,qBAAa,cAAe,SAAQ,aAAa;IAC/C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;IAEb,QAAQ,EAAE,KAAK,EAAE,CAAM;IACvB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;gBAE3B,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IAWd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAmI7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
|
|
@@ -4,7 +4,6 @@ import { FragmentVNode } from "./FragmentVNode";
|
|
|
4
4
|
import { RootVNode } from "./RootVNode";
|
|
5
5
|
import { normalizeChildren } from "./utils";
|
|
6
6
|
import { currentRoot } from "./RootVNode";
|
|
7
|
-
import { ElementVNode } from "./ElementVNode";
|
|
8
7
|
export function getCurrentComponent() {
|
|
9
8
|
if (!currentRoot) {
|
|
10
9
|
throw new Error("No current root");
|
|
@@ -51,6 +50,9 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
51
50
|
this.children = [];
|
|
52
51
|
this.key = key;
|
|
53
52
|
}
|
|
53
|
+
rerender() {
|
|
54
|
+
this.parent?.rerender();
|
|
55
|
+
}
|
|
54
56
|
mount(parent) {
|
|
55
57
|
this.parent = parent;
|
|
56
58
|
if (parent instanceof RootVNode) {
|
|
@@ -94,17 +96,10 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
94
96
|
this.root?.setAsCurrent();
|
|
95
97
|
const newChildren = executeRender();
|
|
96
98
|
const prevChildren = this.children;
|
|
97
|
-
const { children,
|
|
99
|
+
const { children, hasChangedStructure } = this.patchChildren(newChildren);
|
|
98
100
|
this.children = children;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const canSelfUpdate = prevChildren.length === 1 &&
|
|
102
|
-
this.children.length === 1 &&
|
|
103
|
-
prevChildren[0] instanceof ElementVNode &&
|
|
104
|
-
this.children[0] instanceof ElementVNode &&
|
|
105
|
-
this.canPatch(prevChildren[0], this.children[0]);
|
|
106
|
-
if (!canSelfUpdate) {
|
|
107
|
-
this.parent?.applyDOMOperations(operations, this);
|
|
101
|
+
if (hasChangedStructure) {
|
|
102
|
+
this.parent?.rerender();
|
|
108
103
|
}
|
|
109
104
|
this.root?.clearCurrent();
|
|
110
105
|
});
|
|
@@ -8,6 +8,7 @@ export declare class ElementVNode extends AbstractVNode {
|
|
|
8
8
|
private ref?;
|
|
9
9
|
private eventListeners?;
|
|
10
10
|
constructor(tag: string, { ref, ...props }: Props, children: VNode[], key?: string);
|
|
11
|
+
rerender(): void;
|
|
11
12
|
mount(parent?: VNode): Node;
|
|
12
13
|
/**
|
|
13
14
|
* An ELEMENT patch goes through three operations
|
|
@@ -19,5 +20,11 @@ export declare class ElementVNode extends AbstractVNode {
|
|
|
19
20
|
private setProp;
|
|
20
21
|
private patchProps;
|
|
21
22
|
private addEventListener;
|
|
23
|
+
/**
|
|
24
|
+
* Intelligently sync DOM to match children VNode order.
|
|
25
|
+
* Only performs DOM operations when elements are out of position.
|
|
26
|
+
* This is used by both patch() and rerender() to efficiently update children.
|
|
27
|
+
*/
|
|
28
|
+
private syncDOMChildren;
|
|
22
29
|
}
|
|
23
30
|
//# sourceMappingURL=ElementVNode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUvC,qBAAa,YAAa,SAAQ,aAAa;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,CAA0D;IACtE,OAAO,CAAC,cAAc,CAAC,CAA6B;gBAElD,GAAG,EAAE,MAAM,EACX,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,EACxB,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IASd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAY3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;IAgBxB;;;;OAIG;IACH,OAAO,CAAC,eAAe;CAyBxB"}
|
|
@@ -17,6 +17,9 @@ export class ElementVNode extends AbstractVNode {
|
|
|
17
17
|
this.key = key;
|
|
18
18
|
this.ref = ref;
|
|
19
19
|
}
|
|
20
|
+
rerender() {
|
|
21
|
+
this.syncDOMChildren();
|
|
22
|
+
}
|
|
20
23
|
mount(parent) {
|
|
21
24
|
this.parent = parent;
|
|
22
25
|
if (parent instanceof RootVNode) {
|
|
@@ -54,9 +57,11 @@ export class ElementVNode extends AbstractVNode {
|
|
|
54
57
|
patch(newNode) {
|
|
55
58
|
this.patchProps(newNode.props);
|
|
56
59
|
this.props = newNode.props;
|
|
57
|
-
const { children,
|
|
60
|
+
const { children, hasChangedStructure } = this.patchChildren(newNode.children);
|
|
58
61
|
this.children = children;
|
|
59
|
-
|
|
62
|
+
if (hasChangedStructure) {
|
|
63
|
+
this.syncDOMChildren();
|
|
64
|
+
}
|
|
60
65
|
}
|
|
61
66
|
unmount() {
|
|
62
67
|
this.children.forEach((child) => child.unmount());
|
|
@@ -115,4 +120,32 @@ export class ElementVNode extends AbstractVNode {
|
|
|
115
120
|
delete this.eventListeners[type];
|
|
116
121
|
}
|
|
117
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Intelligently sync DOM to match children VNode order.
|
|
125
|
+
* Only performs DOM operations when elements are out of position.
|
|
126
|
+
* This is used by both patch() and rerender() to efficiently update children.
|
|
127
|
+
*/
|
|
128
|
+
syncDOMChildren() {
|
|
129
|
+
const elm = this.elm;
|
|
130
|
+
let currentDomChild = elm.firstChild;
|
|
131
|
+
for (const child of this.children) {
|
|
132
|
+
const childNodes = child.getElements();
|
|
133
|
+
for (const node of childNodes) {
|
|
134
|
+
if (currentDomChild === node) {
|
|
135
|
+
// Already in correct position, advance pointer
|
|
136
|
+
currentDomChild = currentDomChild.nextSibling;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Insert (or move if it exists elsewhere in DOM)
|
|
140
|
+
elm.insertBefore(node, currentDomChild);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Remove any leftover nodes (shouldn't happen if unmount works correctly)
|
|
145
|
+
while (currentDomChild) {
|
|
146
|
+
const next = currentDomChild.nextSibling;
|
|
147
|
+
elm.removeChild(currentDomChild);
|
|
148
|
+
currentDomChild = next;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
118
151
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,KAAK,CAAC,OAAO,EAAE,aAAa;
|
|
1
|
+
{"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,OAAO,EAAE,aAAa;IAS5B,OAAO;CAMR"}
|
|
@@ -19,10 +19,15 @@ export class FragmentVNode extends AbstractVNode {
|
|
|
19
19
|
}
|
|
20
20
|
return this.children.map((child) => child.mount(this)).flat();
|
|
21
21
|
}
|
|
22
|
+
rerender() {
|
|
23
|
+
this.parent?.rerender();
|
|
24
|
+
}
|
|
22
25
|
patch(newNode) {
|
|
23
|
-
const { children,
|
|
26
|
+
const { children, hasChangedStructure } = this.patchChildren(newNode.children);
|
|
24
27
|
this.children = children;
|
|
25
|
-
|
|
28
|
+
if (hasChangedStructure) {
|
|
29
|
+
this.rerender();
|
|
30
|
+
}
|
|
26
31
|
}
|
|
27
32
|
unmount() {
|
|
28
33
|
this.children.forEach((child) => child.unmount());
|
package/dist/vdom/RootVNode.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AbstractVNode
|
|
1
|
+
import { AbstractVNode } from "./AbstractVNode";
|
|
2
2
|
import { VNode } from "./types";
|
|
3
3
|
import { ComponentInstance } from "./ComponentVNode";
|
|
4
4
|
export declare let currentRoot: RootVNode | undefined;
|
|
@@ -19,7 +19,7 @@ export declare class RootVNode extends AbstractVNode {
|
|
|
19
19
|
clearCurrent(): void;
|
|
20
20
|
mount(): Node | Node[];
|
|
21
21
|
patch(): void;
|
|
22
|
-
|
|
22
|
+
rerender(): void;
|
|
23
23
|
unmount(): void;
|
|
24
24
|
}
|
|
25
25
|
//# sourceMappingURL=RootVNode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RootVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/RootVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,
|
|
1
|
+
{"version":3,"file":"RootVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/RootVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAIrD,eAAO,IAAI,WAAW,EAAE,SAAS,GAAG,SAAS,CAAC;AAE9C,qBAAa,SAAU,SAAQ,aAAa;IAC1C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,cAAc,EAAE,iBAAiB,EAAE,CAAM;IACzC,OAAO,CAAC,cAAc,CAGpB;IACF,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,gBAAgB,CAAyB;gBAErC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW;IAMnD,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI;IAIzB,YAAY,CAAC,EAAE,EAAE,MAAM,IAAI;IAG3B,aAAa,CAAC,EAAE,EAAE,MAAM,IAAI;IAa5B,cAAc;IAOd,aAAa,CAAC,QAAQ,EAAE,iBAAiB;IAIzC,YAAY;IAIZ,YAAY;IAIZ,YAAY;IAMZ,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE;IAGtB,KAAK,IAAI,IAAI;IACb,QAAQ,IAAI,IAAI;IAShB,OAAO,IAAI,IAAI;CAChB"}
|
package/dist/vdom/RootVNode.js
CHANGED
|
@@ -59,8 +59,11 @@ export class RootVNode extends AbstractVNode {
|
|
|
59
59
|
return this.children.map((childNode) => childNode.mount(this)).flat();
|
|
60
60
|
}
|
|
61
61
|
patch() { }
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
rerender() {
|
|
63
|
+
const childrenElms = this.children
|
|
64
|
+
.map((child) => child.getElements())
|
|
65
|
+
.flat();
|
|
66
|
+
this.elm.replaceChildren(...childrenElms);
|
|
64
67
|
this.flushLifecycle();
|
|
65
68
|
}
|
|
66
69
|
unmount() { }
|
package/dist/vdom/TextVNode.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/TextVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,SAAU,SAAQ,aAAa;IAC1C,IAAI,EAAE,MAAM,CAAC;gBACD,IAAI,EAAE,MAAM;IAIxB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAe3B,KAAK,CAAC,OAAO,EAAE,SAAS;IAQxB,OAAO;CAMR"}
|
|
1
|
+
{"version":3,"file":"TextVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/TextVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAEhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,qBAAa,SAAU,SAAQ,aAAa;IAC1C,IAAI,EAAE,MAAM,CAAC;gBACD,IAAI,EAAE,MAAM;IAIxB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAe3B,KAAK,CAAC,OAAO,EAAE,SAAS;IAQxB,QAAQ,IAAI,IAAI;IAChB,OAAO;CAMR"}
|
package/dist/vdom/TextVNode.js
CHANGED