rask-ui 0.2.4 → 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.
@@ -46,7 +46,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
46
46
  const newChild1 = new MockVNode("a");
47
47
  const newChild2 = new MockVNode("b");
48
48
  const parent = new MockParentVNode([]);
49
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
49
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
50
50
  // New children should be mounted
51
51
  expect(newChild1.mountCalls).toBe(1);
52
52
  expect(newChild2.mountCalls).toBe(1);
@@ -54,17 +54,19 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
54
54
  expect(newChild2.parent).toBe(parent);
55
55
  // Result should be the new children (since old was empty)
56
56
  expect(result).toEqual([newChild1, newChild2]);
57
+ expect(hasChangedStructure).toBe(true);
57
58
  });
58
59
  it("should unmount all old children when new is empty", () => {
59
60
  const oldChild1 = new MockVNode("a");
60
61
  const oldChild2 = new MockVNode("b");
61
62
  const parent = new MockParentVNode([oldChild1, oldChild2]);
62
- const result = parent.patchChildren(toVNodes([]));
63
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
63
64
  // Old children should be unmounted
64
65
  expect(oldChild1.unmountCalls).toBe(1);
65
66
  expect(oldChild2.unmountCalls).toBe(1);
66
67
  // Result should be empty
67
68
  expect(result).toEqual([]);
69
+ expect(hasChangedStructure).toBe(true);
68
70
  });
69
71
  });
70
72
  describe("Patching with keys", () => {
@@ -76,7 +78,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
76
78
  const newChild1 = new MockVNode("a");
77
79
  const newChild2 = new MockVNode("b");
78
80
  const newChild3 = new MockVNode("c");
79
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
81
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
80
82
  // OLD children should be patched with new children
81
83
  expect(oldChild1.patchCalls).toBe(1);
82
84
  expect(oldChild2.patchCalls).toBe(1);
@@ -94,6 +96,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
94
96
  expect(oldChild3.unmountCalls).toBe(0);
95
97
  // Result should still be the OLD children (reused)
96
98
  expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
99
+ expect(hasChangedStructure).toBe(false);
97
100
  });
98
101
  it("should handle reordered children with keys", () => {
99
102
  const oldChild1 = new MockVNode("a");
@@ -104,7 +107,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
104
107
  const newChild1 = new MockVNode("c");
105
108
  const newChild2 = new MockVNode("a");
106
109
  const newChild3 = new MockVNode("b");
107
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
110
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
108
111
  // Old nodes should be patched with corresponding new nodes by key
109
112
  expect(oldChild1.patchedWith).toBe(newChild2); // a->a
110
113
  expect(oldChild2.patchedWith).toBe(newChild3); // b->b
@@ -119,6 +122,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
119
122
  expect(result[0].key).toBe("c");
120
123
  expect(result[1].key).toBe("a");
121
124
  expect(result[2].key).toBe("b");
125
+ expect(hasChangedStructure).toBe(true);
122
126
  });
123
127
  it("should mount new children and unmount removed children", () => {
124
128
  const oldChild1 = new MockVNode("a");
@@ -129,7 +133,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
129
133
  const newChild1 = new MockVNode("a");
130
134
  const newChild2 = new MockVNode("c");
131
135
  const newChild3 = new MockVNode("d");
132
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
136
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
133
137
  // a and c should be patched (reused)
134
138
  expect(oldChild1.patchedWith).toBe(newChild1);
135
139
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -143,6 +147,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
143
147
  expect(result).toContain(newChild3);
144
148
  expect(result).not.toContain(oldChild2);
145
149
  expect(result.length).toBe(3);
150
+ expect(hasChangedStructure).toBe(true);
146
151
  });
147
152
  it("should replace all children when all keys change", () => {
148
153
  const oldChild1 = new MockVNode("a");
@@ -150,7 +155,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
150
155
  const parent = new MockParentVNode([oldChild1, oldChild2]);
151
156
  const newChild1 = new MockVNode("x");
152
157
  const newChild2 = new MockVNode("y");
153
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
158
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
154
159
  // All new children should be mounted
155
160
  expect(newChild1.mountCalls).toBe(1);
156
161
  expect(newChild2.mountCalls).toBe(1);
@@ -159,6 +164,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
159
164
  expect(oldChild2.unmountCalls).toBe(1);
160
165
  // Result should be the new children
161
166
  expect(result).toEqual([newChild1, newChild2]);
167
+ expect(hasChangedStructure).toBe(true);
162
168
  });
163
169
  });
164
170
  describe("Patching without keys (index-based)", () => {
@@ -170,7 +176,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
170
176
  const newChild1 = new MockVNode();
171
177
  const newChild2 = new MockVNode();
172
178
  const newChild3 = new MockVNode();
173
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
179
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
174
180
  // Should patch by index: 0->0, 1->1, 2->2
175
181
  expect(oldChild1.patchedWith).toBe(newChild1);
176
182
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -181,6 +187,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
181
187
  expect(oldChild3.unmountCalls).toBe(0);
182
188
  // Result should be old children (reused)
183
189
  expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
190
+ expect(hasChangedStructure).toBe(false);
184
191
  });
185
192
  it("should mount new children when growing without keys", () => {
186
193
  const oldChild1 = new MockVNode();
@@ -190,7 +197,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
190
197
  const newChild2 = new MockVNode();
191
198
  const newChild3 = new MockVNode();
192
199
  const newChild4 = new MockVNode();
193
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
200
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
194
201
  // First two should be patched (reused)
195
202
  expect(oldChild1.patchedWith).toBe(newChild1);
196
203
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -202,6 +209,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
202
209
  expect(oldChild2.unmountCalls).toBe(0);
203
210
  // Result should be [oldChild1, oldChild2, newChild3, newChild4]
204
211
  expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
212
+ expect(hasChangedStructure).toBe(true);
205
213
  });
206
214
  it("should unmount old children when shrinking without keys", () => {
207
215
  const oldChild1 = new MockVNode();
@@ -216,7 +224,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
216
224
  ]);
217
225
  const newChild1 = new MockVNode();
218
226
  const newChild2 = new MockVNode();
219
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
227
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
220
228
  // First two should be patched
221
229
  expect(oldChild1.patchedWith).toBe(newChild1);
222
230
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -228,6 +236,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
228
236
  expect(oldChild2.unmountCalls).toBe(0);
229
237
  // Result should be [oldChild1, oldChild2]
230
238
  expect(result).toEqual([oldChild1, oldChild2]);
239
+ expect(hasChangedStructure).toBe(true);
231
240
  });
232
241
  });
233
242
  describe("Mixed keys and indices", () => {
@@ -239,7 +248,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
239
248
  const newChild1 = new MockVNode("a"); // key: "a"
240
249
  const newChild2 = new MockVNode(); // key: undefined -> index 1
241
250
  const newChild3 = new MockVNode("c"); // key: "c"
242
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
251
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
243
252
  // Keyed children should patch by key
244
253
  expect(oldChild1.patchedWith).toBe(newChild1);
245
254
  expect(oldChild3.patchedWith).toBe(newChild3);
@@ -251,6 +260,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
251
260
  expect(oldChild3.unmountCalls).toBe(0);
252
261
  // Result should be old children (reused)
253
262
  expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
263
+ expect(hasChangedStructure).toBe(false);
254
264
  });
255
265
  });
256
266
  describe("Real-world scenarios", () => {
@@ -259,7 +269,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
259
269
  const parent = new MockParentVNode([oldChild1]);
260
270
  const newChild1 = new MockVNode("title");
261
271
  const newChild2 = new MockVNode("details");
262
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
272
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
263
273
  // Title should be patched (reused)
264
274
  expect(oldChild1.patchedWith).toBe(newChild1);
265
275
  // Details should be mounted
@@ -268,13 +278,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
268
278
  expect(oldChild1.unmountCalls).toBe(0);
269
279
  // Result should be [oldChild1, newChild2]
270
280
  expect(result).toEqual([oldChild1, newChild2]);
281
+ expect(hasChangedStructure).toBe(true);
271
282
  });
272
283
  it("should handle conditional rendering (component -> null)", () => {
273
284
  const oldChild1 = new MockVNode("title");
274
285
  const oldChild2 = new MockVNode("details");
275
286
  const parent = new MockParentVNode([oldChild1, oldChild2]);
276
287
  const newChild1 = new MockVNode("title");
277
- const result = parent.patchChildren(toVNodes([newChild1]));
288
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1]));
278
289
  // Title should be patched (reused)
279
290
  expect(oldChild1.patchedWith).toBe(newChild1);
280
291
  // Details should be unmounted
@@ -283,6 +294,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
283
294
  expect(oldChild1.unmountCalls).toBe(0);
284
295
  // Result should be [oldChild1]
285
296
  expect(result).toEqual([oldChild1]);
297
+ expect(hasChangedStructure).toBe(true);
286
298
  });
287
299
  it("should handle list with items added at beginning", () => {
288
300
  const oldChild1 = new MockVNode("item-1");
@@ -291,7 +303,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
291
303
  const newChild1 = new MockVNode("item-0"); // New item at start
292
304
  const newChild2 = new MockVNode("item-1");
293
305
  const newChild3 = new MockVNode("item-2");
294
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
306
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
295
307
  // New item should be mounted
296
308
  expect(newChild1.mountCalls).toBe(1);
297
309
  // Existing items should be patched (reused)
@@ -302,6 +314,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
302
314
  expect(oldChild2.unmountCalls).toBe(0);
303
315
  // Result should be [newChild1, oldChild1, oldChild2]
304
316
  expect(result).toEqual([newChild1, oldChild1, oldChild2]);
317
+ expect(hasChangedStructure).toBe(true);
305
318
  });
306
319
  it("should handle list with items added at end", () => {
307
320
  const oldChild1 = new MockVNode("item-1");
@@ -310,7 +323,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
310
323
  const newChild1 = new MockVNode("item-1");
311
324
  const newChild2 = new MockVNode("item-2");
312
325
  const newChild3 = new MockVNode("item-3"); // New item at end
313
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
326
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
314
327
  // Existing items should be patched (reused)
315
328
  expect(oldChild1.patchedWith).toBe(newChild1);
316
329
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -321,6 +334,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
321
334
  expect(oldChild2.unmountCalls).toBe(0);
322
335
  // Result should be [oldChild1, oldChild2, newChild3]
323
336
  expect(result).toEqual([oldChild1, oldChild2, newChild3]);
337
+ expect(hasChangedStructure).toBe(true);
324
338
  });
325
339
  it("should handle list with item removed from middle", () => {
326
340
  const oldChild1 = new MockVNode("item-1");
@@ -329,7 +343,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
329
343
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
330
344
  const newChild1 = new MockVNode("item-1");
331
345
  const newChild2 = new MockVNode("item-3"); // item-2 removed
332
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
346
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
333
347
  // item-1 and item-3 should be patched (reused)
334
348
  expect(oldChild1.patchedWith).toBe(newChild1);
335
349
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -340,32 +354,35 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
340
354
  expect(oldChild3.unmountCalls).toBe(0);
341
355
  // Result should be [oldChild1, oldChild3]
342
356
  expect(result).toEqual([oldChild1, oldChild3]);
357
+ expect(hasChangedStructure).toBe(true);
343
358
  });
344
359
  it("should handle empty -> multiple children", () => {
345
360
  const parent = new MockParentVNode([]);
346
361
  const newChild1 = new MockVNode("a");
347
362
  const newChild2 = new MockVNode("b");
348
363
  const newChild3 = new MockVNode("c");
349
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
364
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
350
365
  // All should be mounted
351
366
  expect(newChild1.mountCalls).toBe(1);
352
367
  expect(newChild2.mountCalls).toBe(1);
353
368
  expect(newChild3.mountCalls).toBe(1);
354
369
  // Result should be the new children
355
370
  expect(result).toEqual([newChild1, newChild2, newChild3]);
371
+ expect(hasChangedStructure).toBe(true);
356
372
  });
357
373
  it("should handle multiple children -> empty", () => {
358
374
  const oldChild1 = new MockVNode("a");
359
375
  const oldChild2 = new MockVNode("b");
360
376
  const oldChild3 = new MockVNode("c");
361
377
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
362
- const result = parent.patchChildren(toVNodes([]));
378
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
363
379
  // All should be unmounted
364
380
  expect(oldChild1.unmountCalls).toBe(1);
365
381
  expect(oldChild2.unmountCalls).toBe(1);
366
382
  expect(oldChild3.unmountCalls).toBe(1);
367
383
  // Result should be empty
368
384
  expect(result).toEqual([]);
385
+ expect(hasChangedStructure).toBe(true);
369
386
  });
370
387
  });
371
388
  describe("Object reference preservation", () => {
@@ -375,13 +392,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
375
392
  const parent = new MockParentVNode([oldChild1, oldChild2]);
376
393
  const newChild1 = new MockVNode("a");
377
394
  const newChild2 = new MockVNode("b");
378
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
395
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
379
396
  // The result should contain the EXACT SAME object references as the old children
380
397
  expect(result[0]).toBe(oldChild1); // Same object reference
381
398
  expect(result[1]).toBe(oldChild2); // Same object reference
382
399
  // NOT the new children
383
400
  expect(result[0]).not.toBe(newChild1);
384
401
  expect(result[1]).not.toBe(newChild2);
402
+ expect(hasChangedStructure).toBe(false);
385
403
  });
386
404
  });
387
405
  });
@@ -17,6 +17,9 @@ export declare abstract class AbstractVNode {
17
17
  getElements(): Node[];
18
18
  getParentElement(): HTMLElement;
19
19
  protected canPatch(oldNode: VNode, newNode: VNode): boolean;
20
- patchChildren(newChildren: VNode[]): VNode[];
20
+ patchChildren(newChildren: VNode[]): {
21
+ children: VNode[];
22
+ hasChangedStructure: boolean;
23
+ };
21
24
  }
22
25
  //# sourceMappingURL=AbstractVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;CA2D7C"}
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"}
@@ -57,19 +57,23 @@ export class AbstractVNode {
57
57
  // When there are only new children, we just mount them
58
58
  if (newChildren && prevChildren.length === 0) {
59
59
  newChildren.forEach((child) => child.mount(this));
60
- return newChildren;
60
+ return { children: newChildren, hasChangedStructure: true };
61
61
  }
62
62
  // If we want to remove all children, we just unmount the previous ones
63
63
  if (!newChildren.length && prevChildren.length) {
64
64
  prevChildren.forEach((child) => child.unmount());
65
- return [];
65
+ return { children: [], hasChangedStructure: true };
66
66
  }
67
67
  const oldKeys = {};
68
68
  prevChildren.forEach((prevChild, index) => {
69
- oldKeys[prevChild.key || index] = prevChild;
69
+ oldKeys[prevChild.key || index] = {
70
+ vnode: prevChild,
71
+ index,
72
+ };
70
73
  });
71
74
  // Build result array in the NEW order
72
75
  const result = [];
76
+ let hasChangedStructure = false;
73
77
  newChildren.forEach((newChild, index) => {
74
78
  const key = newChild.key || index;
75
79
  const prevChild = oldKeys[key];
@@ -77,30 +81,35 @@ export class AbstractVNode {
77
81
  // New child - mount and add to result
78
82
  newChild.mount(this);
79
83
  result.push(newChild);
84
+ hasChangedStructure = true;
80
85
  }
81
- else if (prevChild === newChild) {
86
+ else if (prevChild?.vnode === newChild) {
82
87
  // Same instance - no patching needed, just reuse
83
- result.push(prevChild);
88
+ result.push(prevChild.vnode);
84
89
  delete oldKeys[key];
90
+ hasChangedStructure = hasChangedStructure || prevChild.index !== index;
85
91
  }
86
- else if (this.canPatch(prevChild, newChild)) {
92
+ else if (this.canPatch(prevChild.vnode, newChild)) {
87
93
  // Compatible types - patch and reuse old VNode
88
- prevChild.patch(newChild);
89
- result.push(prevChild);
94
+ prevChild.vnode.patch(newChild);
95
+ result.push(prevChild.vnode);
90
96
  delete oldKeys[key];
97
+ hasChangedStructure = hasChangedStructure || prevChild.index !== index;
91
98
  }
92
99
  else {
93
100
  // Incompatible types - replace completely
94
101
  newChild.mount(this);
95
- prevChild.unmount();
102
+ prevChild.vnode.unmount();
96
103
  result.push(newChild);
97
104
  delete oldKeys[key];
105
+ hasChangedStructure = true;
98
106
  }
99
107
  });
100
108
  // Unmount any old children that weren't reused
101
109
  for (const key in oldKeys) {
102
- oldKeys[key].unmount();
110
+ oldKeys[key].vnode.unmount();
111
+ hasChangedStructure = true;
103
112
  }
104
- return result;
113
+ return { children: result, hasChangedStructure };
105
114
  }
106
115
  }
@@ -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,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"}
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");
@@ -97,15 +96,9 @@ export class ComponentVNode extends AbstractVNode {
97
96
  this.root?.setAsCurrent();
98
97
  const newChildren = executeRender();
99
98
  const prevChildren = this.children;
100
- this.children = this.patchChildren(newChildren);
101
- // Typically components return a single element, which does
102
- // not require the parent to apply elements to the DOM again
103
- const canSelfUpdate = prevChildren.length === 1 &&
104
- this.children.length === 1 &&
105
- prevChildren[0] instanceof ElementVNode &&
106
- this.children[0] instanceof ElementVNode &&
107
- this.canPatch(prevChildren[0], this.children[0]);
108
- if (!canSelfUpdate) {
99
+ const { children, hasChangedStructure } = this.patchChildren(newChildren);
100
+ this.children = children;
101
+ if (hasChangedStructure) {
109
102
  this.parent?.rerender();
110
103
  }
111
104
  this.root?.clearCurrent();
@@ -1 +1 @@
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"}
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"}
@@ -57,8 +57,11 @@ export class ElementVNode extends AbstractVNode {
57
57
  patch(newNode) {
58
58
  this.patchProps(newNode.props);
59
59
  this.props = newNode.props;
60
- this.children = this.patchChildren(newNode.children);
61
- this.syncDOMChildren();
60
+ const { children, hasChangedStructure } = this.patchChildren(newNode.children);
61
+ this.children = children;
62
+ if (hasChangedStructure) {
63
+ this.syncDOMChildren();
64
+ }
62
65
  }
63
66
  unmount() {
64
67
  this.children.forEach((child) => child.unmount());
@@ -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,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,OAAO,EAAE,aAAa;IAG5B,OAAO;CAMR"}
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"}
@@ -23,7 +23,11 @@ export class FragmentVNode extends AbstractVNode {
23
23
  this.parent?.rerender();
24
24
  }
25
25
  patch(newNode) {
26
- this.children = this.patchChildren(newNode.children);
26
+ const { children, hasChangedStructure } = this.patchChildren(newNode.children);
27
+ this.children = children;
28
+ if (hasChangedStructure) {
29
+ this.rerender();
30
+ }
27
31
  }
28
32
  unmount() {
29
33
  this.children.forEach((child) => child.unmount());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",