rask-ui 0.2.3 → 0.2.4

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