rask-ui 0.2.3 → 0.2.4
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 +48 -187
- package/dist/vdom/AbstractVNode.d.ts +2 -21
- package/dist/vdom/AbstractVNode.d.ts.map +1 -1
- package/dist/vdom/AbstractVNode.js +17 -166
- package/dist/vdom/ComponentVNode.d.ts +1 -0
- package/dist/vdom/ComponentVNode.d.ts.map +1 -1
- package/dist/vdom/ComponentVNode.js +5 -3
- package/dist/vdom/ElementVNode.d.ts +7 -0
- package/dist/vdom/ElementVNode.d.ts.map +1 -1
- package/dist/vdom/ElementVNode.js +33 -3
- package/dist/vdom/FragmentVNode.d.ts +1 -0
- package/dist/vdom/FragmentVNode.d.ts.map +1 -1
- package/dist/vdom/FragmentVNode.js +4 -3
- 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,25 @@ 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
|
|
49
|
+
const result = 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
|
-
// Should return insert operation
|
|
63
|
-
expect(operations).toHaveLength(1);
|
|
64
|
-
expect(operations[0].type).toBe("insert");
|
|
65
|
-
expect(operations[0]).toHaveProperty("elms");
|
|
56
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
66
57
|
});
|
|
67
58
|
it("should unmount all old children when new is empty", () => {
|
|
68
59
|
const oldChild1 = new MockVNode("a");
|
|
69
60
|
const oldChild2 = new MockVNode("b");
|
|
70
61
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
71
|
-
const
|
|
62
|
+
const result = parent.patchChildren(toVNodes([]));
|
|
72
63
|
// Old children should be unmounted
|
|
73
64
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
74
65
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
75
66
|
// Result should be empty
|
|
76
|
-
expect(
|
|
77
|
-
// Should return remove operation
|
|
78
|
-
expect(operations).toHaveLength(1);
|
|
79
|
-
expect(operations[0].type).toBe("remove");
|
|
80
|
-
expect(operations[0]).toHaveProperty("elms");
|
|
67
|
+
expect(result).toEqual([]);
|
|
81
68
|
});
|
|
82
69
|
});
|
|
83
70
|
describe("Patching with keys", () => {
|
|
@@ -89,7 +76,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
89
76
|
const newChild1 = new MockVNode("a");
|
|
90
77
|
const newChild2 = new MockVNode("b");
|
|
91
78
|
const newChild3 = new MockVNode("c");
|
|
92
|
-
const
|
|
79
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
93
80
|
// OLD children should be patched with new children
|
|
94
81
|
expect(oldChild1.patchCalls).toBe(1);
|
|
95
82
|
expect(oldChild2.patchCalls).toBe(1);
|
|
@@ -106,9 +93,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
106
93
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
107
94
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
108
95
|
// Result should still be the OLD children (reused)
|
|
109
|
-
expect(
|
|
110
|
-
// Should have no operations (just patching)
|
|
111
|
-
expect(operations).toHaveLength(0);
|
|
96
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
112
97
|
});
|
|
113
98
|
it("should handle reordered children with keys", () => {
|
|
114
99
|
const oldChild1 = new MockVNode("a");
|
|
@@ -119,7 +104,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
119
104
|
const newChild1 = new MockVNode("c");
|
|
120
105
|
const newChild2 = new MockVNode("a");
|
|
121
106
|
const newChild3 = new MockVNode("b");
|
|
122
|
-
const
|
|
107
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
123
108
|
// Old nodes should be patched with corresponding new nodes by key
|
|
124
109
|
expect(oldChild1.patchedWith).toBe(newChild2); // a->a
|
|
125
110
|
expect(oldChild2.patchedWith).toBe(newChild3); // b->b
|
|
@@ -129,14 +114,11 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
129
114
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
130
115
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
131
116
|
// Result should be old children in NEW order (c, a, b)
|
|
132
|
-
expect(
|
|
117
|
+
expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
|
|
133
118
|
// Verify correct keys
|
|
134
|
-
expect(
|
|
135
|
-
expect(
|
|
136
|
-
expect(
|
|
137
|
-
// Should have move operations for reordered children
|
|
138
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
139
|
-
expect(operations.some((op) => op.type === "move" || op.type === "insert")).toBe(true);
|
|
119
|
+
expect(result[0].key).toBe("c");
|
|
120
|
+
expect(result[1].key).toBe("a");
|
|
121
|
+
expect(result[2].key).toBe("b");
|
|
140
122
|
});
|
|
141
123
|
it("should mount new children and unmount removed children", () => {
|
|
142
124
|
const oldChild1 = new MockVNode("a");
|
|
@@ -147,7 +129,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
147
129
|
const newChild1 = new MockVNode("a");
|
|
148
130
|
const newChild2 = new MockVNode("c");
|
|
149
131
|
const newChild3 = new MockVNode("d");
|
|
150
|
-
const
|
|
132
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
151
133
|
// a and c should be patched (reused)
|
|
152
134
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
153
135
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -156,44 +138,11 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
156
138
|
// b should be unmounted
|
|
157
139
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
158
140
|
// Result should contain old a, old c, and new d
|
|
159
|
-
expect(
|
|
160
|
-
expect(
|
|
161
|
-
expect(
|
|
162
|
-
expect(
|
|
163
|
-
expect(
|
|
164
|
-
// Should have operations for insert and remove
|
|
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
|
-
});
|
|
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);
|
|
197
146
|
});
|
|
198
147
|
it("should replace all children when all keys change", () => {
|
|
199
148
|
const oldChild1 = new MockVNode("a");
|
|
@@ -201,7 +150,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
201
150
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
202
151
|
const newChild1 = new MockVNode("x");
|
|
203
152
|
const newChild2 = new MockVNode("y");
|
|
204
|
-
const
|
|
153
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
205
154
|
// All new children should be mounted
|
|
206
155
|
expect(newChild1.mountCalls).toBe(1);
|
|
207
156
|
expect(newChild2.mountCalls).toBe(1);
|
|
@@ -209,11 +158,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
209
158
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
210
159
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
211
160
|
// Result should be the new children
|
|
212
|
-
expect(
|
|
213
|
-
// Should have operations for both insert and remove
|
|
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);
|
|
161
|
+
expect(result).toEqual([newChild1, newChild2]);
|
|
217
162
|
});
|
|
218
163
|
});
|
|
219
164
|
describe("Patching without keys (index-based)", () => {
|
|
@@ -225,7 +170,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
225
170
|
const newChild1 = new MockVNode();
|
|
226
171
|
const newChild2 = new MockVNode();
|
|
227
172
|
const newChild3 = new MockVNode();
|
|
228
|
-
const
|
|
173
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
229
174
|
// Should patch by index: 0->0, 1->1, 2->2
|
|
230
175
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
231
176
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -235,9 +180,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
235
180
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
236
181
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
237
182
|
// Result should be old children (reused)
|
|
238
|
-
expect(
|
|
239
|
-
// Should have no operations (just patching)
|
|
240
|
-
expect(operations).toHaveLength(0);
|
|
183
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
241
184
|
});
|
|
242
185
|
it("should mount new children when growing without keys", () => {
|
|
243
186
|
const oldChild1 = new MockVNode();
|
|
@@ -247,7 +190,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
247
190
|
const newChild2 = new MockVNode();
|
|
248
191
|
const newChild3 = new MockVNode();
|
|
249
192
|
const newChild4 = new MockVNode();
|
|
250
|
-
const
|
|
193
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
|
|
251
194
|
// First two should be patched (reused)
|
|
252
195
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
253
196
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -258,10 +201,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
258
201
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
259
202
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
260
203
|
// Result should be [oldChild1, oldChild2, newChild3, newChild4]
|
|
261
|
-
expect(
|
|
262
|
-
// Should have insert operation
|
|
263
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
264
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
204
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
|
|
265
205
|
});
|
|
266
206
|
it("should unmount old children when shrinking without keys", () => {
|
|
267
207
|
const oldChild1 = new MockVNode();
|
|
@@ -276,7 +216,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
276
216
|
]);
|
|
277
217
|
const newChild1 = new MockVNode();
|
|
278
218
|
const newChild2 = new MockVNode();
|
|
279
|
-
const
|
|
219
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
280
220
|
// First two should be patched
|
|
281
221
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
282
222
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -287,10 +227,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
287
227
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
288
228
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
289
229
|
// Result should be [oldChild1, oldChild2]
|
|
290
|
-
expect(
|
|
291
|
-
// Should have remove operations
|
|
292
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
293
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
230
|
+
expect(result).toEqual([oldChild1, oldChild2]);
|
|
294
231
|
});
|
|
295
232
|
});
|
|
296
233
|
describe("Mixed keys and indices", () => {
|
|
@@ -302,7 +239,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
302
239
|
const newChild1 = new MockVNode("a"); // key: "a"
|
|
303
240
|
const newChild2 = new MockVNode(); // key: undefined -> index 1
|
|
304
241
|
const newChild3 = new MockVNode("c"); // key: "c"
|
|
305
|
-
const
|
|
242
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
306
243
|
// Keyed children should patch by key
|
|
307
244
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
308
245
|
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
@@ -313,9 +250,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
313
250
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
314
251
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
315
252
|
// Result should be old children (reused)
|
|
316
|
-
expect(
|
|
317
|
-
// Should have no operations (just patching)
|
|
318
|
-
expect(operations).toHaveLength(0);
|
|
253
|
+
expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
319
254
|
});
|
|
320
255
|
});
|
|
321
256
|
describe("Real-world scenarios", () => {
|
|
@@ -324,7 +259,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
324
259
|
const parent = new MockParentVNode([oldChild1]);
|
|
325
260
|
const newChild1 = new MockVNode("title");
|
|
326
261
|
const newChild2 = new MockVNode("details");
|
|
327
|
-
const
|
|
262
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
328
263
|
// Title should be patched (reused)
|
|
329
264
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
330
265
|
// Details should be mounted
|
|
@@ -332,17 +267,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
332
267
|
// No unmounts
|
|
333
268
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
334
269
|
// Result should be [oldChild1, newChild2]
|
|
335
|
-
expect(
|
|
336
|
-
// Should have insert operation
|
|
337
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
338
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
270
|
+
expect(result).toEqual([oldChild1, newChild2]);
|
|
339
271
|
});
|
|
340
272
|
it("should handle conditional rendering (component -> null)", () => {
|
|
341
273
|
const oldChild1 = new MockVNode("title");
|
|
342
274
|
const oldChild2 = new MockVNode("details");
|
|
343
275
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
344
276
|
const newChild1 = new MockVNode("title");
|
|
345
|
-
const
|
|
277
|
+
const result = parent.patchChildren(toVNodes([newChild1]));
|
|
346
278
|
// Title should be patched (reused)
|
|
347
279
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
348
280
|
// Details should be unmounted
|
|
@@ -350,10 +282,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
350
282
|
// Title should not be unmounted
|
|
351
283
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
352
284
|
// Result should be [oldChild1]
|
|
353
|
-
expect(
|
|
354
|
-
// Should have remove operation
|
|
355
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
356
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
285
|
+
expect(result).toEqual([oldChild1]);
|
|
357
286
|
});
|
|
358
287
|
it("should handle list with items added at beginning", () => {
|
|
359
288
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -362,7 +291,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
362
291
|
const newChild1 = new MockVNode("item-0"); // New item at start
|
|
363
292
|
const newChild2 = new MockVNode("item-1");
|
|
364
293
|
const newChild3 = new MockVNode("item-2");
|
|
365
|
-
const
|
|
294
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
366
295
|
// New item should be mounted
|
|
367
296
|
expect(newChild1.mountCalls).toBe(1);
|
|
368
297
|
// Existing items should be patched (reused)
|
|
@@ -372,10 +301,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
372
301
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
373
302
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
374
303
|
// Result should be [newChild1, oldChild1, oldChild2]
|
|
375
|
-
expect(
|
|
376
|
-
// Should have insert operation and possibly move operations for shifted items
|
|
377
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
378
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
304
|
+
expect(result).toEqual([newChild1, oldChild1, oldChild2]);
|
|
379
305
|
});
|
|
380
306
|
it("should handle list with items added at end", () => {
|
|
381
307
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -384,7 +310,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
384
310
|
const newChild1 = new MockVNode("item-1");
|
|
385
311
|
const newChild2 = new MockVNode("item-2");
|
|
386
312
|
const newChild3 = new MockVNode("item-3"); // New item at end
|
|
387
|
-
const
|
|
313
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
388
314
|
// Existing items should be patched (reused)
|
|
389
315
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
390
316
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -394,10 +320,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
394
320
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
395
321
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
396
322
|
// Result should be [oldChild1, oldChild2, newChild3]
|
|
397
|
-
expect(
|
|
398
|
-
// Should have insert operation
|
|
399
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
400
|
-
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
323
|
+
expect(result).toEqual([oldChild1, oldChild2, newChild3]);
|
|
401
324
|
});
|
|
402
325
|
it("should handle list with item removed from middle", () => {
|
|
403
326
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -406,7 +329,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
406
329
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
407
330
|
const newChild1 = new MockVNode("item-1");
|
|
408
331
|
const newChild2 = new MockVNode("item-3"); // item-2 removed
|
|
409
|
-
const
|
|
332
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
410
333
|
// item-1 and item-3 should be patched (reused)
|
|
411
334
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
412
335
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -416,42 +339,33 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
416
339
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
417
340
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
418
341
|
// Result should be [oldChild1, oldChild3]
|
|
419
|
-
expect(
|
|
420
|
-
// Should have remove operation
|
|
421
|
-
expect(operations.length).toBeGreaterThan(0);
|
|
422
|
-
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
342
|
+
expect(result).toEqual([oldChild1, oldChild3]);
|
|
423
343
|
});
|
|
424
344
|
it("should handle empty -> multiple children", () => {
|
|
425
345
|
const parent = new MockParentVNode([]);
|
|
426
346
|
const newChild1 = new MockVNode("a");
|
|
427
347
|
const newChild2 = new MockVNode("b");
|
|
428
348
|
const newChild3 = new MockVNode("c");
|
|
429
|
-
const
|
|
349
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
430
350
|
// All should be mounted
|
|
431
351
|
expect(newChild1.mountCalls).toBe(1);
|
|
432
352
|
expect(newChild2.mountCalls).toBe(1);
|
|
433
353
|
expect(newChild3.mountCalls).toBe(1);
|
|
434
354
|
// Result should be the new children
|
|
435
|
-
expect(
|
|
436
|
-
// Should have insert operation
|
|
437
|
-
expect(operations).toHaveLength(1);
|
|
438
|
-
expect(operations[0].type).toBe("insert");
|
|
355
|
+
expect(result).toEqual([newChild1, newChild2, newChild3]);
|
|
439
356
|
});
|
|
440
357
|
it("should handle multiple children -> empty", () => {
|
|
441
358
|
const oldChild1 = new MockVNode("a");
|
|
442
359
|
const oldChild2 = new MockVNode("b");
|
|
443
360
|
const oldChild3 = new MockVNode("c");
|
|
444
361
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
445
|
-
const
|
|
362
|
+
const result = parent.patchChildren(toVNodes([]));
|
|
446
363
|
// All should be unmounted
|
|
447
364
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
448
365
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
449
366
|
expect(oldChild3.unmountCalls).toBe(1);
|
|
450
367
|
// Result should be empty
|
|
451
|
-
expect(
|
|
452
|
-
// Should have remove operation
|
|
453
|
-
expect(operations).toHaveLength(1);
|
|
454
|
-
expect(operations[0].type).toBe("remove");
|
|
368
|
+
expect(result).toEqual([]);
|
|
455
369
|
});
|
|
456
370
|
});
|
|
457
371
|
describe("Object reference preservation", () => {
|
|
@@ -461,66 +375,13 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
461
375
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
462
376
|
const newChild1 = new MockVNode("a");
|
|
463
377
|
const newChild2 = new MockVNode("b");
|
|
464
|
-
const
|
|
378
|
+
const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
465
379
|
// The result should contain the EXACT SAME object references as the old children
|
|
466
|
-
expect(
|
|
467
|
-
expect(
|
|
380
|
+
expect(result[0]).toBe(oldChild1); // Same object reference
|
|
381
|
+
expect(result[1]).toBe(oldChild2); // Same object reference
|
|
468
382
|
// NOT the new children
|
|
469
|
-
expect(
|
|
470
|
-
expect(
|
|
471
|
-
// Should have no operations (just patching)
|
|
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);
|
|
383
|
+
expect(result[0]).not.toBe(newChild1);
|
|
384
|
+
expect(result[1]).not.toBe(newChild2);
|
|
524
385
|
});
|
|
525
386
|
});
|
|
526
387
|
});
|
|
@@ -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)
|
|
@@ -33,9 +17,6 @@ export declare abstract class AbstractVNode {
|
|
|
33
17
|
getElements(): Node[];
|
|
34
18
|
getParentElement(): HTMLElement;
|
|
35
19
|
protected canPatch(oldNode: VNode, newNode: VNode): boolean;
|
|
36
|
-
patchChildren(newChildren: VNode[]):
|
|
37
|
-
children: VNode[];
|
|
38
|
-
operations: DOMOperation[];
|
|
39
|
-
};
|
|
20
|
+
patchChildren(newChildren: VNode[]): VNode[];
|
|
40
21
|
}
|
|
41
22
|
//# 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,KAAK,EAAE;CA2D7C"}
|
|
@@ -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,52 @@ 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 newChildren;
|
|
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 [];
|
|
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
|
-
// If key already exists, we have a duplicate - unmount the old one immediately
|
|
158
|
-
if (oldKeys[key]) {
|
|
159
|
-
oldKeys[key].unmount();
|
|
160
|
-
operations.push({ type: "remove", elms: oldKeys[key].getElements() });
|
|
161
|
-
}
|
|
162
|
-
oldKeys[key] = prevChild;
|
|
69
|
+
oldKeys[prevChild.key || index] = prevChild;
|
|
163
70
|
});
|
|
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) ===
|
|
71
|
+
// Build result array in the NEW order
|
|
180
72
|
const result = [];
|
|
181
|
-
const newChildrenMeta = [];
|
|
182
73
|
newChildren.forEach((newChild, index) => {
|
|
183
74
|
const key = newChild.key || index;
|
|
184
|
-
const
|
|
185
|
-
if (!
|
|
75
|
+
const prevChild = oldKeys[key];
|
|
76
|
+
if (!prevChild) {
|
|
186
77
|
// New child - mount and add to result
|
|
187
78
|
newChild.mount(this);
|
|
188
79
|
result.push(newChild);
|
|
189
|
-
newChildrenMeta.push({ isNew: true });
|
|
190
80
|
}
|
|
191
|
-
else if (
|
|
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)) {
|
|
192
87
|
// Compatible types - patch and reuse old VNode
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
result.push(oldChild);
|
|
88
|
+
prevChild.patch(newChild);
|
|
89
|
+
result.push(prevChild);
|
|
197
90
|
delete oldKeys[key];
|
|
198
|
-
newChildrenMeta.push({ isNew: false });
|
|
199
91
|
}
|
|
200
92
|
else {
|
|
201
93
|
// Incompatible types - replace completely
|
|
202
94
|
newChild.mount(this);
|
|
203
|
-
|
|
95
|
+
prevChild.unmount();
|
|
204
96
|
result.push(newChild);
|
|
205
97
|
delete oldKeys[key];
|
|
206
|
-
newChildrenMeta.push({ isNew: true, replacedOld: oldChild });
|
|
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
|
-
});
|
|
242
98
|
}
|
|
243
|
-
// else: child is in same position, no operation needed
|
|
244
99
|
});
|
|
245
100
|
// Unmount any old children that weren't reused
|
|
246
101
|
for (const key in oldKeys) {
|
|
247
102
|
oldKeys[key].unmount();
|
|
248
|
-
operations.push({ type: "remove", elms: oldKeys[key].getElements() });
|
|
249
103
|
}
|
|
250
|
-
return
|
|
251
|
-
children: result,
|
|
252
|
-
operations,
|
|
253
|
-
};
|
|
104
|
+
return result;
|
|
254
105
|
}
|
|
255
106
|
}
|
|
@@ -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;IAyI7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
|
|
@@ -51,6 +51,9 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
51
51
|
this.children = [];
|
|
52
52
|
this.key = key;
|
|
53
53
|
}
|
|
54
|
+
rerender() {
|
|
55
|
+
this.parent?.rerender();
|
|
56
|
+
}
|
|
54
57
|
mount(parent) {
|
|
55
58
|
this.parent = parent;
|
|
56
59
|
if (parent instanceof RootVNode) {
|
|
@@ -94,8 +97,7 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
94
97
|
this.root?.setAsCurrent();
|
|
95
98
|
const newChildren = executeRender();
|
|
96
99
|
const prevChildren = this.children;
|
|
97
|
-
|
|
98
|
-
this.children = children;
|
|
100
|
+
this.children = this.patchChildren(newChildren);
|
|
99
101
|
// Typically components return a single element, which does
|
|
100
102
|
// not require the parent to apply elements to the DOM again
|
|
101
103
|
const canSelfUpdate = prevChildren.length === 1 &&
|
|
@@ -104,7 +106,7 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
104
106
|
this.children[0] instanceof ElementVNode &&
|
|
105
107
|
this.canPatch(prevChildren[0], this.children[0]);
|
|
106
108
|
if (!canSelfUpdate) {
|
|
107
|
-
this.parent?.
|
|
109
|
+
this.parent?.rerender();
|
|
108
110
|
}
|
|
109
111
|
this.root?.clearCurrent();
|
|
110
112
|
});
|
|
@@ -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;IAM3B,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,8 @@ export class ElementVNode extends AbstractVNode {
|
|
|
54
57
|
patch(newNode) {
|
|
55
58
|
this.patchProps(newNode.props);
|
|
56
59
|
this.props = newNode.props;
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
this.applyDOMOperations(operations);
|
|
60
|
+
this.children = this.patchChildren(newNode.children);
|
|
61
|
+
this.syncDOMChildren();
|
|
60
62
|
}
|
|
61
63
|
unmount() {
|
|
62
64
|
this.children.forEach((child) => child.unmount());
|
|
@@ -115,4 +117,32 @@ export class ElementVNode extends AbstractVNode {
|
|
|
115
117
|
delete this.eventListeners[type];
|
|
116
118
|
}
|
|
117
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* Intelligently sync DOM to match children VNode order.
|
|
122
|
+
* Only performs DOM operations when elements are out of position.
|
|
123
|
+
* This is used by both patch() and rerender() to efficiently update children.
|
|
124
|
+
*/
|
|
125
|
+
syncDOMChildren() {
|
|
126
|
+
const elm = this.elm;
|
|
127
|
+
let currentDomChild = elm.firstChild;
|
|
128
|
+
for (const child of this.children) {
|
|
129
|
+
const childNodes = child.getElements();
|
|
130
|
+
for (const node of childNodes) {
|
|
131
|
+
if (currentDomChild === node) {
|
|
132
|
+
// Already in correct position, advance pointer
|
|
133
|
+
currentDomChild = currentDomChild.nextSibling;
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Insert (or move if it exists elsewhere in DOM)
|
|
137
|
+
elm.insertBefore(node, currentDomChild);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Remove any leftover nodes (shouldn't happen if unmount works correctly)
|
|
142
|
+
while (currentDomChild) {
|
|
143
|
+
const next = currentDomChild.nextSibling;
|
|
144
|
+
elm.removeChild(currentDomChild);
|
|
145
|
+
currentDomChild = next;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
118
148
|
}
|
|
@@ -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;IAG5B,OAAO;CAMR"}
|
|
@@ -19,10 +19,11 @@ 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
|
-
|
|
24
|
-
this.children = children;
|
|
25
|
-
this.applyDOMOperations(operations);
|
|
26
|
+
this.children = this.patchChildren(newNode.children);
|
|
26
27
|
}
|
|
27
28
|
unmount() {
|
|
28
29
|
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