rask-ui 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- // elm already exists from constructor
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
- // Don't delete elm - it's needed for getElements() when building operations
32
- // delete this.elm;
29
+ delete this.elm;
33
30
  delete this.parent;
34
31
  }
35
32
  rerender() {
@@ -40,8 +37,6 @@ class MockVNode extends AbstractVNode {
40
37
  class MockParentVNode extends MockVNode {
41
38
  constructor(initialChildren, key) {
42
39
  super(key);
43
- // Override elm with an HTMLElement since parents need to contain children
44
- this.elm = document.createElement("div");
45
40
  this.children = (initialChildren || []);
46
41
  }
47
42
  }
@@ -51,33 +46,27 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
51
46
  const newChild1 = new MockVNode("a");
52
47
  const newChild2 = new MockVNode("b");
53
48
  const parent = new MockParentVNode([]);
54
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
49
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
55
50
  // New children should be mounted
56
51
  expect(newChild1.mountCalls).toBe(1);
57
52
  expect(newChild2.mountCalls).toBe(1);
58
53
  expect(newChild1.parent).toBe(parent);
59
54
  expect(newChild2.parent).toBe(parent);
60
55
  // Result should be the new children (since old was empty)
61
- expect(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");
56
+ expect(result).toEqual([newChild1, newChild2]);
57
+ expect(hasChangedStructure).toBe(true);
66
58
  });
67
59
  it("should unmount all old children when new is empty", () => {
68
60
  const oldChild1 = new MockVNode("a");
69
61
  const oldChild2 = new MockVNode("b");
70
62
  const parent = new MockParentVNode([oldChild1, oldChild2]);
71
- const { children, operations } = parent.patchChildren(toVNodes([]));
63
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
72
64
  // Old children should be unmounted
73
65
  expect(oldChild1.unmountCalls).toBe(1);
74
66
  expect(oldChild2.unmountCalls).toBe(1);
75
67
  // Result should be empty
76
- expect(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
+ expect(result).toEqual([]);
69
+ expect(hasChangedStructure).toBe(true);
81
70
  });
82
71
  });
83
72
  describe("Patching with keys", () => {
@@ -89,7 +78,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
89
78
  const newChild1 = new MockVNode("a");
90
79
  const newChild2 = new MockVNode("b");
91
80
  const newChild3 = new MockVNode("c");
92
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
81
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
93
82
  // OLD children should be patched with new children
94
83
  expect(oldChild1.patchCalls).toBe(1);
95
84
  expect(oldChild2.patchCalls).toBe(1);
@@ -106,9 +95,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
106
95
  expect(oldChild2.unmountCalls).toBe(0);
107
96
  expect(oldChild3.unmountCalls).toBe(0);
108
97
  // Result should still be the OLD children (reused)
109
- expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
110
- // Should have no operations (just patching)
111
- expect(operations).toHaveLength(0);
98
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
99
+ expect(hasChangedStructure).toBe(false);
112
100
  });
113
101
  it("should handle reordered children with keys", () => {
114
102
  const oldChild1 = new MockVNode("a");
@@ -119,7 +107,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
119
107
  const newChild1 = new MockVNode("c");
120
108
  const newChild2 = new MockVNode("a");
121
109
  const newChild3 = new MockVNode("b");
122
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
110
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
123
111
  // Old nodes should be patched with corresponding new nodes by key
124
112
  expect(oldChild1.patchedWith).toBe(newChild2); // a->a
125
113
  expect(oldChild2.patchedWith).toBe(newChild3); // b->b
@@ -129,14 +117,12 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
129
117
  expect(oldChild2.unmountCalls).toBe(0);
130
118
  expect(oldChild3.unmountCalls).toBe(0);
131
119
  // Result should be old children in NEW order (c, a, b)
132
- expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
120
+ expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
133
121
  // Verify correct keys
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
+ expect(result[0].key).toBe("c");
123
+ expect(result[1].key).toBe("a");
124
+ expect(result[2].key).toBe("b");
125
+ expect(hasChangedStructure).toBe(true);
140
126
  });
141
127
  it("should mount new children and unmount removed children", () => {
142
128
  const oldChild1 = new MockVNode("a");
@@ -147,7 +133,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
147
133
  const newChild1 = new MockVNode("a");
148
134
  const newChild2 = new MockVNode("c");
149
135
  const newChild3 = new MockVNode("d");
150
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
136
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
151
137
  // a and c should be patched (reused)
152
138
  expect(oldChild1.patchedWith).toBe(newChild1);
153
139
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -156,44 +142,12 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
156
142
  // b should be unmounted
157
143
  expect(oldChild2.unmountCalls).toBe(1);
158
144
  // Result should contain old a, old c, and new d
159
- expect(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
- });
145
+ expect(result).toContain(oldChild1);
146
+ expect(result).toContain(oldChild3);
147
+ expect(result).toContain(newChild3);
148
+ expect(result).not.toContain(oldChild2);
149
+ expect(result.length).toBe(3);
150
+ expect(hasChangedStructure).toBe(true);
197
151
  });
198
152
  it("should replace all children when all keys change", () => {
199
153
  const oldChild1 = new MockVNode("a");
@@ -201,7 +155,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
201
155
  const parent = new MockParentVNode([oldChild1, oldChild2]);
202
156
  const newChild1 = new MockVNode("x");
203
157
  const newChild2 = new MockVNode("y");
204
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
158
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
205
159
  // All new children should be mounted
206
160
  expect(newChild1.mountCalls).toBe(1);
207
161
  expect(newChild2.mountCalls).toBe(1);
@@ -209,11 +163,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
209
163
  expect(oldChild1.unmountCalls).toBe(1);
210
164
  expect(oldChild2.unmountCalls).toBe(1);
211
165
  // Result should be the new children
212
- expect(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);
166
+ expect(result).toEqual([newChild1, newChild2]);
167
+ expect(hasChangedStructure).toBe(true);
217
168
  });
218
169
  });
219
170
  describe("Patching without keys (index-based)", () => {
@@ -225,7 +176,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
225
176
  const newChild1 = new MockVNode();
226
177
  const newChild2 = new MockVNode();
227
178
  const newChild3 = new MockVNode();
228
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
179
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
229
180
  // Should patch by index: 0->0, 1->1, 2->2
230
181
  expect(oldChild1.patchedWith).toBe(newChild1);
231
182
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -235,9 +186,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
235
186
  expect(oldChild2.unmountCalls).toBe(0);
236
187
  expect(oldChild3.unmountCalls).toBe(0);
237
188
  // Result should be old children (reused)
238
- expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
239
- // Should have no operations (just patching)
240
- expect(operations).toHaveLength(0);
189
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
190
+ expect(hasChangedStructure).toBe(false);
241
191
  });
242
192
  it("should mount new children when growing without keys", () => {
243
193
  const oldChild1 = new MockVNode();
@@ -247,7 +197,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
247
197
  const newChild2 = new MockVNode();
248
198
  const newChild3 = new MockVNode();
249
199
  const newChild4 = new MockVNode();
250
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
200
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
251
201
  // First two should be patched (reused)
252
202
  expect(oldChild1.patchedWith).toBe(newChild1);
253
203
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -258,10 +208,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
258
208
  expect(oldChild1.unmountCalls).toBe(0);
259
209
  expect(oldChild2.unmountCalls).toBe(0);
260
210
  // Result should be [oldChild1, oldChild2, newChild3, newChild4]
261
- expect(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);
211
+ expect(result).toEqual([oldChild1, oldChild2, newChild3, newChild4]);
212
+ expect(hasChangedStructure).toBe(true);
265
213
  });
266
214
  it("should unmount old children when shrinking without keys", () => {
267
215
  const oldChild1 = new MockVNode();
@@ -276,7 +224,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
276
224
  ]);
277
225
  const newChild1 = new MockVNode();
278
226
  const newChild2 = new MockVNode();
279
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
227
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
280
228
  // First two should be patched
281
229
  expect(oldChild1.patchedWith).toBe(newChild1);
282
230
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -287,10 +235,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
287
235
  expect(oldChild1.unmountCalls).toBe(0);
288
236
  expect(oldChild2.unmountCalls).toBe(0);
289
237
  // Result should be [oldChild1, oldChild2]
290
- expect(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);
238
+ expect(result).toEqual([oldChild1, oldChild2]);
239
+ expect(hasChangedStructure).toBe(true);
294
240
  });
295
241
  });
296
242
  describe("Mixed keys and indices", () => {
@@ -302,7 +248,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
302
248
  const newChild1 = new MockVNode("a"); // key: "a"
303
249
  const newChild2 = new MockVNode(); // key: undefined -> index 1
304
250
  const newChild3 = new MockVNode("c"); // key: "c"
305
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
251
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
306
252
  // Keyed children should patch by key
307
253
  expect(oldChild1.patchedWith).toBe(newChild1);
308
254
  expect(oldChild3.patchedWith).toBe(newChild3);
@@ -313,9 +259,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
313
259
  expect(oldChild2.unmountCalls).toBe(0);
314
260
  expect(oldChild3.unmountCalls).toBe(0);
315
261
  // Result should be old children (reused)
316
- expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
317
- // Should have no operations (just patching)
318
- expect(operations).toHaveLength(0);
262
+ expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
263
+ expect(hasChangedStructure).toBe(false);
319
264
  });
320
265
  });
321
266
  describe("Real-world scenarios", () => {
@@ -324,7 +269,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
324
269
  const parent = new MockParentVNode([oldChild1]);
325
270
  const newChild1 = new MockVNode("title");
326
271
  const newChild2 = new MockVNode("details");
327
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
272
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
328
273
  // Title should be patched (reused)
329
274
  expect(oldChild1.patchedWith).toBe(newChild1);
330
275
  // Details should be mounted
@@ -332,17 +277,15 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
332
277
  // No unmounts
333
278
  expect(oldChild1.unmountCalls).toBe(0);
334
279
  // Result should be [oldChild1, newChild2]
335
- expect(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);
280
+ expect(result).toEqual([oldChild1, newChild2]);
281
+ expect(hasChangedStructure).toBe(true);
339
282
  });
340
283
  it("should handle conditional rendering (component -> null)", () => {
341
284
  const oldChild1 = new MockVNode("title");
342
285
  const oldChild2 = new MockVNode("details");
343
286
  const parent = new MockParentVNode([oldChild1, oldChild2]);
344
287
  const newChild1 = new MockVNode("title");
345
- const { children, operations } = parent.patchChildren(toVNodes([newChild1]));
288
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1]));
346
289
  // Title should be patched (reused)
347
290
  expect(oldChild1.patchedWith).toBe(newChild1);
348
291
  // Details should be unmounted
@@ -350,10 +293,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
350
293
  // Title should not be unmounted
351
294
  expect(oldChild1.unmountCalls).toBe(0);
352
295
  // Result should be [oldChild1]
353
- expect(children).toEqual([oldChild1]);
354
- // Should have remove operation
355
- expect(operations.length).toBeGreaterThan(0);
356
- expect(operations.some((op) => op.type === "remove")).toBe(true);
296
+ expect(result).toEqual([oldChild1]);
297
+ expect(hasChangedStructure).toBe(true);
357
298
  });
358
299
  it("should handle list with items added at beginning", () => {
359
300
  const oldChild1 = new MockVNode("item-1");
@@ -362,7 +303,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
362
303
  const newChild1 = new MockVNode("item-0"); // New item at start
363
304
  const newChild2 = new MockVNode("item-1");
364
305
  const newChild3 = new MockVNode("item-2");
365
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
306
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
366
307
  // New item should be mounted
367
308
  expect(newChild1.mountCalls).toBe(1);
368
309
  // Existing items should be patched (reused)
@@ -372,10 +313,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
372
313
  expect(oldChild1.unmountCalls).toBe(0);
373
314
  expect(oldChild2.unmountCalls).toBe(0);
374
315
  // Result should be [newChild1, oldChild1, oldChild2]
375
- expect(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);
316
+ expect(result).toEqual([newChild1, oldChild1, oldChild2]);
317
+ expect(hasChangedStructure).toBe(true);
379
318
  });
380
319
  it("should handle list with items added at end", () => {
381
320
  const oldChild1 = new MockVNode("item-1");
@@ -384,7 +323,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
384
323
  const newChild1 = new MockVNode("item-1");
385
324
  const newChild2 = new MockVNode("item-2");
386
325
  const newChild3 = new MockVNode("item-3"); // New item at end
387
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
326
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
388
327
  // Existing items should be patched (reused)
389
328
  expect(oldChild1.patchedWith).toBe(newChild1);
390
329
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -394,10 +333,8 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
394
333
  expect(oldChild1.unmountCalls).toBe(0);
395
334
  expect(oldChild2.unmountCalls).toBe(0);
396
335
  // Result should be [oldChild1, oldChild2, newChild3]
397
- expect(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);
336
+ expect(result).toEqual([oldChild1, oldChild2, newChild3]);
337
+ expect(hasChangedStructure).toBe(true);
401
338
  });
402
339
  it("should handle list with item removed from middle", () => {
403
340
  const oldChild1 = new MockVNode("item-1");
@@ -406,7 +343,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
406
343
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
407
344
  const newChild1 = new MockVNode("item-1");
408
345
  const newChild2 = new MockVNode("item-3"); // item-2 removed
409
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
346
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
410
347
  // item-1 and item-3 should be patched (reused)
411
348
  expect(oldChild1.patchedWith).toBe(newChild1);
412
349
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -416,42 +353,36 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
416
353
  expect(oldChild1.unmountCalls).toBe(0);
417
354
  expect(oldChild3.unmountCalls).toBe(0);
418
355
  // Result should be [oldChild1, oldChild3]
419
- expect(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);
356
+ expect(result).toEqual([oldChild1, oldChild3]);
357
+ expect(hasChangedStructure).toBe(true);
423
358
  });
424
359
  it("should handle empty -> multiple children", () => {
425
360
  const parent = new MockParentVNode([]);
426
361
  const newChild1 = new MockVNode("a");
427
362
  const newChild2 = new MockVNode("b");
428
363
  const newChild3 = new MockVNode("c");
429
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
364
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
430
365
  // All should be mounted
431
366
  expect(newChild1.mountCalls).toBe(1);
432
367
  expect(newChild2.mountCalls).toBe(1);
433
368
  expect(newChild3.mountCalls).toBe(1);
434
369
  // Result should be the new children
435
- expect(children).toEqual([newChild1, newChild2, newChild3]);
436
- // Should have insert operation
437
- expect(operations).toHaveLength(1);
438
- expect(operations[0].type).toBe("insert");
370
+ expect(result).toEqual([newChild1, newChild2, newChild3]);
371
+ expect(hasChangedStructure).toBe(true);
439
372
  });
440
373
  it("should handle multiple children -> empty", () => {
441
374
  const oldChild1 = new MockVNode("a");
442
375
  const oldChild2 = new MockVNode("b");
443
376
  const oldChild3 = new MockVNode("c");
444
377
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
445
- const { children, operations } = parent.patchChildren(toVNodes([]));
378
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([]));
446
379
  // All should be unmounted
447
380
  expect(oldChild1.unmountCalls).toBe(1);
448
381
  expect(oldChild2.unmountCalls).toBe(1);
449
382
  expect(oldChild3.unmountCalls).toBe(1);
450
383
  // Result should be empty
451
- expect(children).toEqual([]);
452
- // Should have remove operation
453
- expect(operations).toHaveLength(1);
454
- expect(operations[0].type).toBe("remove");
384
+ expect(result).toEqual([]);
385
+ expect(hasChangedStructure).toBe(true);
455
386
  });
456
387
  });
457
388
  describe("Object reference preservation", () => {
@@ -461,66 +392,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
461
392
  const parent = new MockParentVNode([oldChild1, oldChild2]);
462
393
  const newChild1 = new MockVNode("a");
463
394
  const newChild2 = new MockVNode("b");
464
- const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
395
+ const { children: result, hasChangedStructure } = parent.patchChildren(toVNodes([newChild1, newChild2]));
465
396
  // The result should contain the EXACT SAME object references as the old children
466
- expect(children[0]).toBe(oldChild1); // Same object reference
467
- expect(children[1]).toBe(oldChild2); // Same object reference
397
+ expect(result[0]).toBe(oldChild1); // Same object reference
398
+ expect(result[1]).toBe(oldChild2); // Same object reference
468
399
  // NOT the new children
469
- expect(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);
400
+ expect(result[0]).not.toBe(newChild1);
401
+ expect(result[1]).not.toBe(newChild2);
402
+ expect(hasChangedStructure).toBe(false);
524
403
  });
525
404
  });
526
405
  });
@@ -1,21 +1,5 @@
1
1
  import { RootVNode } from "./RootVNode";
2
2
  import { VNode } from "./types";
3
- export type DOMOperation = {
4
- type: "replace";
5
- oldElms: Node[];
6
- newElms: Node[];
7
- } | {
8
- type: "remove";
9
- elms: Node[];
10
- } | {
11
- type: "insert";
12
- elms: Node[];
13
- afterElm?: Node;
14
- } | {
15
- type: "move";
16
- elms: Node[];
17
- afterElm?: Node;
18
- };
19
3
  export declare abstract class AbstractVNode {
20
4
  key?: string;
21
5
  parent?: VNode;
@@ -25,7 +9,7 @@ export declare abstract class AbstractVNode {
25
9
  abstract mount(parent?: VNode): Node | Node[];
26
10
  abstract patch(oldNode: VNode): void;
27
11
  abstract unmount(): void;
28
- applyDOMOperations(operations: DOMOperation[], atVNode?: VNode): void;
12
+ abstract rerender(): void;
29
13
  protected getHTMLElement(): HTMLElement;
30
14
  /**
31
15
  * A VNode can represent multiple elements (fragment of component)
@@ -35,7 +19,7 @@ export declare abstract class AbstractVNode {
35
19
  protected canPatch(oldNode: VNode, newNode: VNode): boolean;
36
20
  patchChildren(newChildren: VNode[]): {
37
21
  children: VNode[];
38
- operations: DOMOperation[];
22
+ hasChangedStructure: boolean;
39
23
  };
40
24
  }
41
25
  //# sourceMappingURL=AbstractVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG;QACnC,QAAQ,EAAE,KAAK,EAAE,CAAC;QAClB,mBAAmB,EAAE,OAAO,CAAC;KAC9B;CAqEF"}
@@ -1,77 +1,9 @@
1
- import { elementsToFragment } from "./dom-utils";
2
1
  export class AbstractVNode {
3
2
  key;
4
3
  parent;
5
4
  root;
6
5
  elm;
7
6
  children;
8
- applyDOMOperations(operations, atVNode) {
9
- if (!this.elm || !this.children) {
10
- this.parent?.applyDOMOperations(operations, this);
11
- return;
12
- }
13
- console.log(operations);
14
- operations.forEach((operation) => {
15
- switch (operation.type) {
16
- case "insert": {
17
- const fragment = elementsToFragment(operation.elms);
18
- // Insert after afterElm (or at start if undefined)
19
- if (operation.afterElm === undefined) {
20
- // Insert at the start
21
- this.elm.insertBefore(fragment, this.elm.firstChild);
22
- }
23
- else {
24
- // Insert after afterElm
25
- const target = operation.afterElm.nextSibling;
26
- this.elm.insertBefore(fragment, target);
27
- }
28
- break;
29
- }
30
- case "remove": {
31
- const elms = operation.elms;
32
- if (elms.length === 1) {
33
- this.elm.removeChild(elms[0]);
34
- }
35
- else {
36
- const range = new Range();
37
- range.setStartBefore(elms[0]);
38
- range.setEndAfter(elms[elms.length - 1]);
39
- range.deleteContents();
40
- }
41
- break;
42
- }
43
- case "replace": {
44
- const oldElms = operation.oldElms;
45
- const newElms = operation.newElms;
46
- if (oldElms.length === 1) {
47
- this.elm.replaceChild(elementsToFragment(newElms), oldElms[0]);
48
- }
49
- else {
50
- const range = new Range();
51
- range.setStartBefore(oldElms[0]);
52
- range.setEndAfter(oldElms[oldElms.length - 1]);
53
- range.deleteContents();
54
- range.insertNode(elementsToFragment(newElms));
55
- }
56
- break;
57
- }
58
- case "move": {
59
- const fragment = elementsToFragment(operation.elms);
60
- // Insert after afterElm (or at start if undefined)
61
- if (operation.afterElm === undefined) {
62
- // Insert at the start
63
- this.elm.insertBefore(fragment, this.elm.firstChild);
64
- }
65
- else {
66
- // Insert after afterElm
67
- const target = operation.afterElm.nextSibling;
68
- this.elm.insertBefore(fragment, target);
69
- }
70
- break;
71
- }
72
- }
73
- });
74
- }
75
7
  getHTMLElement() {
76
8
  if (!this.elm || !(this.elm instanceof HTMLElement)) {
77
9
  throw new Error("This VNode does not have an HTMLElement");
@@ -86,7 +18,6 @@ export class AbstractVNode {
86
18
  return [this.elm];
87
19
  }
88
20
  if (!this.children) {
89
- console.log("WTF", this);
90
21
  throw new Error("This VNode has no element or children");
91
22
  }
92
23
  return this.children.map((child) => child.getElements()).flat();
@@ -124,132 +55,61 @@ export class AbstractVNode {
124
55
  patchChildren(newChildren) {
125
56
  const prevChildren = this.children;
126
57
  // When there are only new children, we just mount them
127
- if (prevChildren.length === 0) {
58
+ if (newChildren && prevChildren.length === 0) {
128
59
  newChildren.forEach((child) => child.mount(this));
129
- return {
130
- children: newChildren,
131
- operations: [
132
- {
133
- type: "insert",
134
- elms: newChildren.map((child) => child.getElements()).flat(),
135
- },
136
- ],
137
- };
60
+ return { children: newChildren, hasChangedStructure: true };
138
61
  }
139
62
  // If we want to remove all children, we just unmount the previous ones
140
63
  if (!newChildren.length && prevChildren.length) {
141
64
  prevChildren.forEach((child) => child.unmount());
142
- return {
143
- children: [],
144
- operations: [
145
- {
146
- type: "remove",
147
- elms: prevChildren.map((child) => child.getElements()).flat(),
148
- },
149
- ],
150
- };
65
+ return { children: [], hasChangedStructure: true };
151
66
  }
152
- const operations = [];
153
67
  const oldKeys = {};
154
- // Build oldKeys map and handle duplicate keys
155
68
  prevChildren.forEach((prevChild, index) => {
156
- const key = prevChild.key || index;
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] = {
70
+ vnode: prevChild,
71
+ index,
72
+ };
163
73
  });
164
- // Helper to get afterElm for a position in result array
165
- const getAfterElm = (index, result) => {
166
- let currentIndex = index;
167
- let prevChild = result[--currentIndex];
168
- let afterElm;
169
- while (prevChild) {
170
- const prevElms = prevChild.getElements();
171
- afterElm = prevElms[prevElms.length - 1];
172
- if (afterElm) {
173
- break;
174
- }
175
- prevChild = result[--currentIndex];
176
- }
177
- return afterElm;
178
- };
179
- // === PASS 1: Build result array (mount/patch all children) ===
74
+ // Build result array in the NEW order
180
75
  const result = [];
181
- const newChildrenMeta = [];
76
+ let hasChangedStructure = false;
182
77
  newChildren.forEach((newChild, index) => {
183
78
  const key = newChild.key || index;
184
- const oldChild = oldKeys[key];
185
- if (!oldChild) {
79
+ const prevChild = oldKeys[key];
80
+ if (!prevChild) {
186
81
  // New child - mount and add to result
187
82
  newChild.mount(this);
188
83
  result.push(newChild);
189
- newChildrenMeta.push({ isNew: true });
84
+ hasChangedStructure = true;
85
+ }
86
+ else if (prevChild?.vnode === newChild) {
87
+ // Same instance - no patching needed, just reuse
88
+ result.push(prevChild.vnode);
89
+ delete oldKeys[key];
90
+ hasChangedStructure = hasChangedStructure || prevChild.index !== index;
190
91
  }
191
- else if (this.canPatch(oldChild, newChild)) {
92
+ else if (this.canPatch(prevChild.vnode, newChild)) {
192
93
  // Compatible types - patch and reuse old VNode
193
- if (oldChild !== newChild) {
194
- oldChild.patch(newChild);
195
- }
196
- result.push(oldChild);
94
+ prevChild.vnode.patch(newChild);
95
+ result.push(prevChild.vnode);
197
96
  delete oldKeys[key];
198
- newChildrenMeta.push({ isNew: false });
97
+ hasChangedStructure = hasChangedStructure || prevChild.index !== index;
199
98
  }
200
99
  else {
201
100
  // Incompatible types - replace completely
202
101
  newChild.mount(this);
203
- oldChild.unmount();
102
+ prevChild.vnode.unmount();
204
103
  result.push(newChild);
205
104
  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
- });
105
+ hasChangedStructure = true;
242
106
  }
243
- // else: child is in same position, no operation needed
244
107
  });
245
108
  // Unmount any old children that weren't reused
246
109
  for (const key in oldKeys) {
247
- oldKeys[key].unmount();
248
- operations.push({ type: "remove", elms: oldKeys[key].getElements() });
110
+ oldKeys[key].vnode.unmount();
111
+ hasChangedStructure = true;
249
112
  }
250
- return {
251
- children: result,
252
- operations,
253
- };
113
+ return { children: result, hasChangedStructure };
254
114
  }
255
115
  }
@@ -40,6 +40,7 @@ export declare class ComponentVNode extends AbstractVNode {
40
40
  children: VNode[];
41
41
  instance?: ComponentInstance;
42
42
  constructor(component: Component<any>, props: Props, children: VNode[], key?: string);
43
+ rerender(): void;
43
44
  mount(parent?: VNode): Node[];
44
45
  patch(newNode: ComponentVNode): void;
45
46
  unmount(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAG9D,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,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IA0I7B,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");
@@ -51,6 +50,9 @@ export class ComponentVNode extends AbstractVNode {
51
50
  this.children = [];
52
51
  this.key = key;
53
52
  }
53
+ rerender() {
54
+ this.parent?.rerender();
55
+ }
54
56
  mount(parent) {
55
57
  this.parent = parent;
56
58
  if (parent instanceof RootVNode) {
@@ -94,17 +96,10 @@ export class ComponentVNode extends AbstractVNode {
94
96
  this.root?.setAsCurrent();
95
97
  const newChildren = executeRender();
96
98
  const prevChildren = this.children;
97
- const { children, operations } = this.patchChildren(newChildren);
99
+ const { children, hasChangedStructure } = this.patchChildren(newChildren);
98
100
  this.children = children;
99
- // Typically components return a single element, which does
100
- // not require the parent to apply elements to the DOM again
101
- const canSelfUpdate = prevChildren.length === 1 &&
102
- this.children.length === 1 &&
103
- prevChildren[0] instanceof ElementVNode &&
104
- this.children[0] instanceof ElementVNode &&
105
- this.canPatch(prevChildren[0], this.children[0]);
106
- if (!canSelfUpdate) {
107
- this.parent?.applyDOMOperations(operations, this);
101
+ if (hasChangedStructure) {
102
+ this.parent?.rerender();
108
103
  }
109
104
  this.root?.clearCurrent();
110
105
  });
@@ -8,6 +8,7 @@ export declare class ElementVNode extends AbstractVNode {
8
8
  private ref?;
9
9
  private eventListeners?;
10
10
  constructor(tag: string, { ref, ...props }: Props, children: VNode[], key?: string);
11
+ rerender(): void;
11
12
  mount(parent?: VNode): Node;
12
13
  /**
13
14
  * An ELEMENT patch goes through three operations
@@ -19,5 +20,11 @@ export declare class ElementVNode extends AbstractVNode {
19
20
  private setProp;
20
21
  private patchProps;
21
22
  private addEventListener;
23
+ /**
24
+ * Intelligently sync DOM to match children VNode order.
25
+ * Only performs DOM operations when elements are out of position.
26
+ * This is used by both patch() and rerender() to efficiently update children.
27
+ */
28
+ private syncDOMChildren;
22
29
  }
23
30
  //# sourceMappingURL=ElementVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAgB,MAAM,iBAAiB,CAAC;AAG9D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAWvC,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,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAO3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;CAgBzB"}
1
+ {"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUvC,qBAAa,YAAa,SAAQ,aAAa;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,CAA0D;IACtE,OAAO,CAAC,cAAc,CAAC,CAA6B;gBAElD,GAAG,EAAE,MAAM,EACX,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,EACxB,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IASd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAY3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;IAgBxB;;;;OAIG;IACH,OAAO,CAAC,eAAe;CAyBxB"}
@@ -17,6 +17,9 @@ export class ElementVNode extends AbstractVNode {
17
17
  this.key = key;
18
18
  this.ref = ref;
19
19
  }
20
+ rerender() {
21
+ this.syncDOMChildren();
22
+ }
20
23
  mount(parent) {
21
24
  this.parent = parent;
22
25
  if (parent instanceof RootVNode) {
@@ -54,9 +57,11 @@ export class ElementVNode extends AbstractVNode {
54
57
  patch(newNode) {
55
58
  this.patchProps(newNode.props);
56
59
  this.props = newNode.props;
57
- const { children, operations } = this.patchChildren(newNode.children);
60
+ const { children, hasChangedStructure } = this.patchChildren(newNode.children);
58
61
  this.children = children;
59
- this.applyDOMOperations(operations);
62
+ if (hasChangedStructure) {
63
+ this.syncDOMChildren();
64
+ }
60
65
  }
61
66
  unmount() {
62
67
  this.children.forEach((child) => child.unmount());
@@ -115,4 +120,32 @@ export class ElementVNode extends AbstractVNode {
115
120
  delete this.eventListeners[type];
116
121
  }
117
122
  }
123
+ /**
124
+ * Intelligently sync DOM to match children VNode order.
125
+ * Only performs DOM operations when elements are out of position.
126
+ * This is used by both patch() and rerender() to efficiently update children.
127
+ */
128
+ syncDOMChildren() {
129
+ const elm = this.elm;
130
+ let currentDomChild = elm.firstChild;
131
+ for (const child of this.children) {
132
+ const childNodes = child.getElements();
133
+ for (const node of childNodes) {
134
+ if (currentDomChild === node) {
135
+ // Already in correct position, advance pointer
136
+ currentDomChild = currentDomChild.nextSibling;
137
+ }
138
+ else {
139
+ // Insert (or move if it exists elsewhere in DOM)
140
+ elm.insertBefore(node, currentDomChild);
141
+ }
142
+ }
143
+ }
144
+ // Remove any leftover nodes (shouldn't happen if unmount works correctly)
145
+ while (currentDomChild) {
146
+ const next = currentDomChild.nextSibling;
147
+ elm.removeChild(currentDomChild);
148
+ currentDomChild = next;
149
+ }
150
+ }
118
151
  }
@@ -6,6 +6,7 @@ export declare class FragmentVNode extends AbstractVNode {
6
6
  key?: string;
7
7
  constructor(children: VNode[], key?: string);
8
8
  mount(parent?: VNode): Node[];
9
+ rerender(): void;
9
10
  patch(newNode: FragmentVNode): void;
10
11
  unmount(): void;
11
12
  }
@@ -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;IAK5B,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"}
@@ -19,10 +19,15 @@ export class FragmentVNode extends AbstractVNode {
19
19
  }
20
20
  return this.children.map((child) => child.mount(this)).flat();
21
21
  }
22
+ rerender() {
23
+ this.parent?.rerender();
24
+ }
22
25
  patch(newNode) {
23
- const { children, operations } = this.patchChildren(newNode.children);
26
+ const { children, hasChangedStructure } = this.patchChildren(newNode.children);
24
27
  this.children = children;
25
- this.applyDOMOperations(operations);
28
+ if (hasChangedStructure) {
29
+ this.rerender();
30
+ }
26
31
  }
27
32
  unmount() {
28
33
  this.children.forEach((child) => child.unmount());
@@ -1,4 +1,4 @@
1
- import { AbstractVNode, DOMOperation } from "./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
- applyDOMOperations(operations: DOMOperation[], atVNode: VNode): void;
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,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC9D,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,kBAAkB,CAAC,UAAU,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,KAAK,GAAG,IAAI;IAIpE,OAAO,IAAI,IAAI;CAChB"}
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"}
@@ -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
- applyDOMOperations(operations, atVNode) {
63
- super.applyDOMOperations(operations, atVNode);
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() { }
@@ -5,6 +5,7 @@ export declare class TextVNode extends AbstractVNode {
5
5
  constructor(text: string);
6
6
  mount(parent?: VNode): Node;
7
7
  patch(newNode: TextVNode): void;
8
+ rerender(): void;
8
9
  unmount(): void;
9
10
  }
10
11
  //# sourceMappingURL=TextVNode.d.ts.map
@@ -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"}
@@ -25,6 +25,7 @@ export class TextVNode extends AbstractVNode {
25
25
  this.text = newNode.text;
26
26
  this.elm.textContent = this.text;
27
27
  }
28
+ rerender() { }
28
29
  unmount() {
29
30
  this.root?.queueUnmount(() => {
30
31
  delete this.elm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",