rask-ui 0.2.1 → 0.2.3
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/observation.d.ts +1 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +3 -16
- package/dist/observation.test.js +109 -146
- package/dist/tests/observation.test.js +0 -37
- package/dist/tests/patchChildren.test.js +187 -48
- package/dist/vdom/AbstractVNode.d.ts +21 -2
- package/dist/vdom/AbstractVNode.d.ts.map +1 -1
- package/dist/vdom/AbstractVNode.js +166 -17
- package/dist/vdom/ComponentVNode.d.ts +0 -1
- package/dist/vdom/ComponentVNode.d.ts.map +1 -1
- package/dist/vdom/ComponentVNode.js +26 -18
- package/dist/vdom/ElementVNode.d.ts +0 -1
- package/dist/vdom/ElementVNode.d.ts.map +1 -1
- package/dist/vdom/ElementVNode.js +3 -11
- package/dist/vdom/FragmentVNode.d.ts +0 -1
- package/dist/vdom/FragmentVNode.d.ts.map +1 -1
- package/dist/vdom/FragmentVNode.js +3 -4
- package/dist/vdom/RootVNode.d.ts +5 -2
- package/dist/vdom/RootVNode.d.ts.map +1 -1
- package/dist/vdom/RootVNode.js +17 -5
- package/dist/vdom/TextVNode.d.ts +0 -1
- package/dist/vdom/TextVNode.d.ts.map +1 -1
- package/dist/vdom/TextVNode.js +3 -1
- package/package.json +1 -1
|
@@ -11,11 +11,13 @@ 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"}`);
|
|
14
16
|
}
|
|
15
17
|
mount(parent) {
|
|
16
18
|
this.mountCalls++;
|
|
17
19
|
this.parent = parent;
|
|
18
|
-
|
|
20
|
+
// elm already exists from constructor
|
|
19
21
|
return this.elm;
|
|
20
22
|
}
|
|
21
23
|
patch(newNode) {
|
|
@@ -26,7 +28,8 @@ class MockVNode extends AbstractVNode {
|
|
|
26
28
|
}
|
|
27
29
|
unmount() {
|
|
28
30
|
this.unmountCalls++;
|
|
29
|
-
delete
|
|
31
|
+
// Don't delete elm - it's needed for getElements() when building operations
|
|
32
|
+
// delete this.elm;
|
|
30
33
|
delete this.parent;
|
|
31
34
|
}
|
|
32
35
|
rerender() {
|
|
@@ -37,6 +40,8 @@ class MockVNode extends AbstractVNode {
|
|
|
37
40
|
class MockParentVNode extends MockVNode {
|
|
38
41
|
constructor(initialChildren, key) {
|
|
39
42
|
super(key);
|
|
43
|
+
// Override elm with an HTMLElement since parents need to contain children
|
|
44
|
+
this.elm = document.createElement("div");
|
|
40
45
|
this.children = (initialChildren || []);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
@@ -46,25 +51,33 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
46
51
|
const newChild1 = new MockVNode("a");
|
|
47
52
|
const newChild2 = new MockVNode("b");
|
|
48
53
|
const parent = new MockParentVNode([]);
|
|
49
|
-
const
|
|
54
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
50
55
|
// New children should be mounted
|
|
51
56
|
expect(newChild1.mountCalls).toBe(1);
|
|
52
57
|
expect(newChild2.mountCalls).toBe(1);
|
|
53
58
|
expect(newChild1.parent).toBe(parent);
|
|
54
59
|
expect(newChild2.parent).toBe(parent);
|
|
55
60
|
// Result should be the new children (since old was empty)
|
|
56
|
-
expect(
|
|
61
|
+
expect(children).toEqual([newChild1, newChild2]);
|
|
62
|
+
// Should return insert operation
|
|
63
|
+
expect(operations).toHaveLength(1);
|
|
64
|
+
expect(operations[0].type).toBe("insert");
|
|
65
|
+
expect(operations[0]).toHaveProperty("elms");
|
|
57
66
|
});
|
|
58
67
|
it("should unmount all old children when new is empty", () => {
|
|
59
68
|
const oldChild1 = new MockVNode("a");
|
|
60
69
|
const oldChild2 = new MockVNode("b");
|
|
61
70
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
62
|
-
const
|
|
71
|
+
const { children, operations } = parent.patchChildren(toVNodes([]));
|
|
63
72
|
// Old children should be unmounted
|
|
64
73
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
65
74
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
66
75
|
// Result should be empty
|
|
67
|
-
expect(
|
|
76
|
+
expect(children).toEqual([]);
|
|
77
|
+
// Should return remove operation
|
|
78
|
+
expect(operations).toHaveLength(1);
|
|
79
|
+
expect(operations[0].type).toBe("remove");
|
|
80
|
+
expect(operations[0]).toHaveProperty("elms");
|
|
68
81
|
});
|
|
69
82
|
});
|
|
70
83
|
describe("Patching with keys", () => {
|
|
@@ -76,7 +89,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
76
89
|
const newChild1 = new MockVNode("a");
|
|
77
90
|
const newChild2 = new MockVNode("b");
|
|
78
91
|
const newChild3 = new MockVNode("c");
|
|
79
|
-
const
|
|
92
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
80
93
|
// OLD children should be patched with new children
|
|
81
94
|
expect(oldChild1.patchCalls).toBe(1);
|
|
82
95
|
expect(oldChild2.patchCalls).toBe(1);
|
|
@@ -93,7 +106,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
93
106
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
94
107
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
95
108
|
// Result should still be the OLD children (reused)
|
|
96
|
-
expect(
|
|
109
|
+
expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
110
|
+
// Should have no operations (just patching)
|
|
111
|
+
expect(operations).toHaveLength(0);
|
|
97
112
|
});
|
|
98
113
|
it("should handle reordered children with keys", () => {
|
|
99
114
|
const oldChild1 = new MockVNode("a");
|
|
@@ -104,7 +119,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
104
119
|
const newChild1 = new MockVNode("c");
|
|
105
120
|
const newChild2 = new MockVNode("a");
|
|
106
121
|
const newChild3 = new MockVNode("b");
|
|
107
|
-
const
|
|
122
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
108
123
|
// Old nodes should be patched with corresponding new nodes by key
|
|
109
124
|
expect(oldChild1.patchedWith).toBe(newChild2); // a->a
|
|
110
125
|
expect(oldChild2.patchedWith).toBe(newChild3); // b->b
|
|
@@ -114,11 +129,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
114
129
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
115
130
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
116
131
|
// Result should be old children in NEW order (c, a, b)
|
|
117
|
-
expect(
|
|
132
|
+
expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
|
|
118
133
|
// Verify correct keys
|
|
119
|
-
expect(
|
|
120
|
-
expect(
|
|
121
|
-
expect(
|
|
134
|
+
expect(children[0].key).toBe("c");
|
|
135
|
+
expect(children[1].key).toBe("a");
|
|
136
|
+
expect(children[2].key).toBe("b");
|
|
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);
|
|
122
140
|
});
|
|
123
141
|
it("should mount new children and unmount removed children", () => {
|
|
124
142
|
const oldChild1 = new MockVNode("a");
|
|
@@ -129,7 +147,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
129
147
|
const newChild1 = new MockVNode("a");
|
|
130
148
|
const newChild2 = new MockVNode("c");
|
|
131
149
|
const newChild3 = new MockVNode("d");
|
|
132
|
-
const
|
|
150
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
133
151
|
// a and c should be patched (reused)
|
|
134
152
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
135
153
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -138,11 +156,44 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
138
156
|
// b should be unmounted
|
|
139
157
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
140
158
|
// Result should contain old a, old c, and new d
|
|
141
|
-
expect(
|
|
142
|
-
expect(
|
|
143
|
-
expect(
|
|
144
|
-
expect(
|
|
145
|
-
expect(
|
|
159
|
+
expect(children).toContain(oldChild1);
|
|
160
|
+
expect(children).toContain(oldChild3);
|
|
161
|
+
expect(children).toContain(newChild3);
|
|
162
|
+
expect(children).not.toContain(oldChild2);
|
|
163
|
+
expect(children.length).toBe(3);
|
|
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
|
+
});
|
|
146
197
|
});
|
|
147
198
|
it("should replace all children when all keys change", () => {
|
|
148
199
|
const oldChild1 = new MockVNode("a");
|
|
@@ -150,7 +201,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
150
201
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
151
202
|
const newChild1 = new MockVNode("x");
|
|
152
203
|
const newChild2 = new MockVNode("y");
|
|
153
|
-
const
|
|
204
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
154
205
|
// All new children should be mounted
|
|
155
206
|
expect(newChild1.mountCalls).toBe(1);
|
|
156
207
|
expect(newChild2.mountCalls).toBe(1);
|
|
@@ -158,7 +209,11 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
158
209
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
159
210
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
160
211
|
// Result should be the new children
|
|
161
|
-
expect(
|
|
212
|
+
expect(children).toEqual([newChild1, newChild2]);
|
|
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);
|
|
162
217
|
});
|
|
163
218
|
});
|
|
164
219
|
describe("Patching without keys (index-based)", () => {
|
|
@@ -170,7 +225,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
170
225
|
const newChild1 = new MockVNode();
|
|
171
226
|
const newChild2 = new MockVNode();
|
|
172
227
|
const newChild3 = new MockVNode();
|
|
173
|
-
const
|
|
228
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
174
229
|
// Should patch by index: 0->0, 1->1, 2->2
|
|
175
230
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
176
231
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -180,7 +235,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
180
235
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
181
236
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
182
237
|
// Result should be old children (reused)
|
|
183
|
-
expect(
|
|
238
|
+
expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
239
|
+
// Should have no operations (just patching)
|
|
240
|
+
expect(operations).toHaveLength(0);
|
|
184
241
|
});
|
|
185
242
|
it("should mount new children when growing without keys", () => {
|
|
186
243
|
const oldChild1 = new MockVNode();
|
|
@@ -190,7 +247,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
190
247
|
const newChild2 = new MockVNode();
|
|
191
248
|
const newChild3 = new MockVNode();
|
|
192
249
|
const newChild4 = new MockVNode();
|
|
193
|
-
const
|
|
250
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
|
|
194
251
|
// First two should be patched (reused)
|
|
195
252
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
196
253
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -201,7 +258,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
201
258
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
202
259
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
203
260
|
// Result should be [oldChild1, oldChild2, newChild3, newChild4]
|
|
204
|
-
expect(
|
|
261
|
+
expect(children).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
|
|
262
|
+
// Should have insert operation
|
|
263
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
264
|
+
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
205
265
|
});
|
|
206
266
|
it("should unmount old children when shrinking without keys", () => {
|
|
207
267
|
const oldChild1 = new MockVNode();
|
|
@@ -216,7 +276,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
216
276
|
]);
|
|
217
277
|
const newChild1 = new MockVNode();
|
|
218
278
|
const newChild2 = new MockVNode();
|
|
219
|
-
const
|
|
279
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
220
280
|
// First two should be patched
|
|
221
281
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
222
282
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -227,7 +287,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
227
287
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
228
288
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
229
289
|
// Result should be [oldChild1, oldChild2]
|
|
230
|
-
expect(
|
|
290
|
+
expect(children).toEqual([oldChild1, oldChild2]);
|
|
291
|
+
// Should have remove operations
|
|
292
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
293
|
+
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
231
294
|
});
|
|
232
295
|
});
|
|
233
296
|
describe("Mixed keys and indices", () => {
|
|
@@ -239,7 +302,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
239
302
|
const newChild1 = new MockVNode("a"); // key: "a"
|
|
240
303
|
const newChild2 = new MockVNode(); // key: undefined -> index 1
|
|
241
304
|
const newChild3 = new MockVNode("c"); // key: "c"
|
|
242
|
-
const
|
|
305
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
243
306
|
// Keyed children should patch by key
|
|
244
307
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
245
308
|
expect(oldChild3.patchedWith).toBe(newChild3);
|
|
@@ -250,7 +313,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
250
313
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
251
314
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
252
315
|
// Result should be old children (reused)
|
|
253
|
-
expect(
|
|
316
|
+
expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
|
|
317
|
+
// Should have no operations (just patching)
|
|
318
|
+
expect(operations).toHaveLength(0);
|
|
254
319
|
});
|
|
255
320
|
});
|
|
256
321
|
describe("Real-world scenarios", () => {
|
|
@@ -259,7 +324,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
259
324
|
const parent = new MockParentVNode([oldChild1]);
|
|
260
325
|
const newChild1 = new MockVNode("title");
|
|
261
326
|
const newChild2 = new MockVNode("details");
|
|
262
|
-
const
|
|
327
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
263
328
|
// Title should be patched (reused)
|
|
264
329
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
265
330
|
// Details should be mounted
|
|
@@ -267,14 +332,17 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
267
332
|
// No unmounts
|
|
268
333
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
269
334
|
// Result should be [oldChild1, newChild2]
|
|
270
|
-
expect(
|
|
335
|
+
expect(children).toEqual([oldChild1, newChild2]);
|
|
336
|
+
// Should have insert operation
|
|
337
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
338
|
+
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
271
339
|
});
|
|
272
340
|
it("should handle conditional rendering (component -> null)", () => {
|
|
273
341
|
const oldChild1 = new MockVNode("title");
|
|
274
342
|
const oldChild2 = new MockVNode("details");
|
|
275
343
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
276
344
|
const newChild1 = new MockVNode("title");
|
|
277
|
-
const
|
|
345
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1]));
|
|
278
346
|
// Title should be patched (reused)
|
|
279
347
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
280
348
|
// Details should be unmounted
|
|
@@ -282,7 +350,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
282
350
|
// Title should not be unmounted
|
|
283
351
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
284
352
|
// Result should be [oldChild1]
|
|
285
|
-
expect(
|
|
353
|
+
expect(children).toEqual([oldChild1]);
|
|
354
|
+
// Should have remove operation
|
|
355
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
356
|
+
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
286
357
|
});
|
|
287
358
|
it("should handle list with items added at beginning", () => {
|
|
288
359
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -291,7 +362,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
291
362
|
const newChild1 = new MockVNode("item-0"); // New item at start
|
|
292
363
|
const newChild2 = new MockVNode("item-1");
|
|
293
364
|
const newChild3 = new MockVNode("item-2");
|
|
294
|
-
const
|
|
365
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
295
366
|
// New item should be mounted
|
|
296
367
|
expect(newChild1.mountCalls).toBe(1);
|
|
297
368
|
// Existing items should be patched (reused)
|
|
@@ -301,7 +372,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
301
372
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
302
373
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
303
374
|
// Result should be [newChild1, oldChild1, oldChild2]
|
|
304
|
-
expect(
|
|
375
|
+
expect(children).toEqual([newChild1, oldChild1, oldChild2]);
|
|
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);
|
|
305
379
|
});
|
|
306
380
|
it("should handle list with items added at end", () => {
|
|
307
381
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -310,7 +384,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
310
384
|
const newChild1 = new MockVNode("item-1");
|
|
311
385
|
const newChild2 = new MockVNode("item-2");
|
|
312
386
|
const newChild3 = new MockVNode("item-3"); // New item at end
|
|
313
|
-
const
|
|
387
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
314
388
|
// Existing items should be patched (reused)
|
|
315
389
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
316
390
|
expect(oldChild2.patchedWith).toBe(newChild2);
|
|
@@ -320,7 +394,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
320
394
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
321
395
|
expect(oldChild2.unmountCalls).toBe(0);
|
|
322
396
|
// Result should be [oldChild1, oldChild2, newChild3]
|
|
323
|
-
expect(
|
|
397
|
+
expect(children).toEqual([oldChild1, oldChild2, newChild3]);
|
|
398
|
+
// Should have insert operation
|
|
399
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
400
|
+
expect(operations.some((op) => op.type === "insert")).toBe(true);
|
|
324
401
|
});
|
|
325
402
|
it("should handle list with item removed from middle", () => {
|
|
326
403
|
const oldChild1 = new MockVNode("item-1");
|
|
@@ -329,7 +406,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
329
406
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
330
407
|
const newChild1 = new MockVNode("item-1");
|
|
331
408
|
const newChild2 = new MockVNode("item-3"); // item-2 removed
|
|
332
|
-
const
|
|
409
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
333
410
|
// item-1 and item-3 should be patched (reused)
|
|
334
411
|
expect(oldChild1.patchedWith).toBe(newChild1);
|
|
335
412
|
expect(oldChild3.patchedWith).toBe(newChild2);
|
|
@@ -339,33 +416,42 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
339
416
|
expect(oldChild1.unmountCalls).toBe(0);
|
|
340
417
|
expect(oldChild3.unmountCalls).toBe(0);
|
|
341
418
|
// Result should be [oldChild1, oldChild3]
|
|
342
|
-
expect(
|
|
419
|
+
expect(children).toEqual([oldChild1, oldChild3]);
|
|
420
|
+
// Should have remove operation
|
|
421
|
+
expect(operations.length).toBeGreaterThan(0);
|
|
422
|
+
expect(operations.some((op) => op.type === "remove")).toBe(true);
|
|
343
423
|
});
|
|
344
424
|
it("should handle empty -> multiple children", () => {
|
|
345
425
|
const parent = new MockParentVNode([]);
|
|
346
426
|
const newChild1 = new MockVNode("a");
|
|
347
427
|
const newChild2 = new MockVNode("b");
|
|
348
428
|
const newChild3 = new MockVNode("c");
|
|
349
|
-
const
|
|
429
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
|
|
350
430
|
// All should be mounted
|
|
351
431
|
expect(newChild1.mountCalls).toBe(1);
|
|
352
432
|
expect(newChild2.mountCalls).toBe(1);
|
|
353
433
|
expect(newChild3.mountCalls).toBe(1);
|
|
354
434
|
// Result should be the new children
|
|
355
|
-
expect(
|
|
435
|
+
expect(children).toEqual([newChild1, newChild2, newChild3]);
|
|
436
|
+
// Should have insert operation
|
|
437
|
+
expect(operations).toHaveLength(1);
|
|
438
|
+
expect(operations[0].type).toBe("insert");
|
|
356
439
|
});
|
|
357
440
|
it("should handle multiple children -> empty", () => {
|
|
358
441
|
const oldChild1 = new MockVNode("a");
|
|
359
442
|
const oldChild2 = new MockVNode("b");
|
|
360
443
|
const oldChild3 = new MockVNode("c");
|
|
361
444
|
const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
|
|
362
|
-
const
|
|
445
|
+
const { children, operations } = parent.patchChildren(toVNodes([]));
|
|
363
446
|
// All should be unmounted
|
|
364
447
|
expect(oldChild1.unmountCalls).toBe(1);
|
|
365
448
|
expect(oldChild2.unmountCalls).toBe(1);
|
|
366
449
|
expect(oldChild3.unmountCalls).toBe(1);
|
|
367
450
|
// Result should be empty
|
|
368
|
-
expect(
|
|
451
|
+
expect(children).toEqual([]);
|
|
452
|
+
// Should have remove operation
|
|
453
|
+
expect(operations).toHaveLength(1);
|
|
454
|
+
expect(operations[0].type).toBe("remove");
|
|
369
455
|
});
|
|
370
456
|
});
|
|
371
457
|
describe("Object reference preservation", () => {
|
|
@@ -375,13 +461,66 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
|
|
|
375
461
|
const parent = new MockParentVNode([oldChild1, oldChild2]);
|
|
376
462
|
const newChild1 = new MockVNode("a");
|
|
377
463
|
const newChild2 = new MockVNode("b");
|
|
378
|
-
const
|
|
464
|
+
const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
|
|
379
465
|
// The result should contain the EXACT SAME object references as the old children
|
|
380
|
-
expect(
|
|
381
|
-
expect(
|
|
466
|
+
expect(children[0]).toBe(oldChild1); // Same object reference
|
|
467
|
+
expect(children[1]).toBe(oldChild2); // Same object reference
|
|
382
468
|
// NOT the new children
|
|
383
|
-
expect(
|
|
384
|
-
expect(
|
|
469
|
+
expect(children[0]).not.toBe(newChild1);
|
|
470
|
+
expect(children[1]).not.toBe(newChild2);
|
|
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);
|
|
385
524
|
});
|
|
386
525
|
});
|
|
387
526
|
});
|
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
};
|
|
3
19
|
export declare abstract class AbstractVNode {
|
|
4
20
|
key?: string;
|
|
5
21
|
parent?: VNode;
|
|
@@ -9,7 +25,7 @@ export declare abstract class AbstractVNode {
|
|
|
9
25
|
abstract mount(parent?: VNode): Node | Node[];
|
|
10
26
|
abstract patch(oldNode: VNode): void;
|
|
11
27
|
abstract unmount(): void;
|
|
12
|
-
|
|
28
|
+
applyDOMOperations(operations: DOMOperation[], atVNode?: VNode): void;
|
|
13
29
|
protected getHTMLElement(): HTMLElement;
|
|
14
30
|
/**
|
|
15
31
|
* A VNode can represent multiple elements (fragment of component)
|
|
@@ -17,6 +33,9 @@ export declare abstract class AbstractVNode {
|
|
|
17
33
|
getElements(): Node[];
|
|
18
34
|
getParentElement(): HTMLElement;
|
|
19
35
|
protected canPatch(oldNode: VNode, newNode: VNode): boolean;
|
|
20
|
-
patchChildren(newChildren: VNode[]):
|
|
36
|
+
patchChildren(newChildren: VNode[]): {
|
|
37
|
+
children: VNode[];
|
|
38
|
+
operations: DOMOperation[];
|
|
39
|
+
};
|
|
21
40
|
}
|
|
22
41
|
//# 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":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,IAAI,EAAE,CAAC;IAChB,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,IAAI,EAAE,CAAC;CACd,GACD;IACE,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,GACD;IACE,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,EAAE,CAAC;IACb,QAAQ,CAAC,EAAE,IAAI,CAAC;CACjB,CAAC;AAEN,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,kBAAkB,CAAC,UAAU,EAAE,YAAY,EAAE,EAAE,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI;IAkErE,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAarB,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,UAAU,EAAE,YAAY,EAAE,CAAC;KAC5B;CAuJF"}
|