rask-ui 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,150 +1,113 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { Signal, Observer, getCurrentObserver } from './observation';
3
- describe('Signal', () => {
4
- it('should allow subscribing to notifications', () => {
5
- const signal = new Signal();
6
- const callback = vi.fn();
7
- signal.subscribe(callback);
8
- signal.notify();
9
- expect(callback).toHaveBeenCalledTimes(1);
10
- });
11
- it('should return a disposer function', () => {
12
- const signal = new Signal();
13
- const callback = vi.fn();
14
- const dispose = signal.subscribe(callback);
15
- dispose();
16
- signal.notify();
17
- expect(callback).not.toHaveBeenCalled();
18
- });
19
- it('should handle multiple subscribers', () => {
20
- const signal = new Signal();
21
- const callback1 = vi.fn();
22
- const callback2 = vi.fn();
23
- signal.subscribe(callback1);
24
- signal.subscribe(callback2);
25
- signal.notify();
26
- expect(callback1).toHaveBeenCalledTimes(1);
27
- expect(callback2).toHaveBeenCalledTimes(1);
28
- });
29
- it('should allow unsubscribing individual callbacks', () => {
30
- const signal = new Signal();
31
- const callback1 = vi.fn();
32
- const callback2 = vi.fn();
33
- const dispose1 = signal.subscribe(callback1);
34
- signal.subscribe(callback2);
35
- dispose1();
36
- signal.notify();
37
- expect(callback1).not.toHaveBeenCalled();
38
- expect(callback2).toHaveBeenCalledTimes(1);
39
- });
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { Signal, Observer, getCurrentObserver } from "./observation";
3
+ describe("Signal", () => {
4
+ it("should allow subscribing to notifications", () => {
5
+ const signal = new Signal();
6
+ const callback = vi.fn();
7
+ signal.subscribe(callback);
8
+ signal.notify();
9
+ expect(callback).toHaveBeenCalledTimes(1);
10
+ });
11
+ it("should return a disposer function", () => {
12
+ const signal = new Signal();
13
+ const callback = vi.fn();
14
+ const dispose = signal.subscribe(callback);
15
+ dispose();
16
+ signal.notify();
17
+ expect(callback).not.toHaveBeenCalled();
18
+ });
19
+ it("should handle multiple subscribers", () => {
20
+ const signal = new Signal();
21
+ const callback1 = vi.fn();
22
+ const callback2 = vi.fn();
23
+ signal.subscribe(callback1);
24
+ signal.subscribe(callback2);
25
+ signal.notify();
26
+ expect(callback1).toHaveBeenCalledTimes(1);
27
+ expect(callback2).toHaveBeenCalledTimes(1);
28
+ });
29
+ it("should allow unsubscribing individual callbacks", () => {
30
+ const signal = new Signal();
31
+ const callback1 = vi.fn();
32
+ const callback2 = vi.fn();
33
+ const dispose1 = signal.subscribe(callback1);
34
+ signal.subscribe(callback2);
35
+ dispose1();
36
+ signal.notify();
37
+ expect(callback1).not.toHaveBeenCalled();
38
+ expect(callback2).toHaveBeenCalledTimes(1);
39
+ });
40
40
  });
41
- describe('Observer', () => {
42
- it('should queue notifications in microtasks', async () => {
43
- let callCount = 0;
44
- const observer = new Observer(() => {
45
- callCount++;
46
- });
47
- const signal = new Signal();
48
- const dispose = observer.observe();
49
- observer.subscribeSignal(signal);
50
- dispose();
51
- // Trigger multiple notifications
52
- signal.notify();
53
- signal.notify();
54
- signal.notify();
55
- // Should not be called synchronously
56
- expect(callCount).toBe(0);
57
- // Wait for microtask
58
- await new Promise((resolve) => queueMicrotask(() => resolve()));
59
- // Should be called only once due to queuing
60
- expect(callCount).toBe(1);
61
- });
62
- it('should track signals during observation', () => {
63
- const callback = vi.fn();
64
- const observer = new Observer(callback);
65
- const signal = new Signal();
66
- const dispose = observer.observe();
67
- observer.subscribeSignal(signal);
68
- dispose();
69
- signal.notify();
70
- return new Promise((resolve) => {
71
- queueMicrotask(() => {
72
- expect(callback).toHaveBeenCalledTimes(1);
73
- resolve(undefined);
74
- });
75
- });
76
- });
77
- it('should clear signals when observing again', async () => {
78
- let callCount = 0;
79
- const observer = new Observer(() => {
80
- callCount++;
81
- });
82
- const signal1 = new Signal();
83
- const signal2 = new Signal();
84
- // First observation
85
- let dispose = observer.observe();
86
- observer.subscribeSignal(signal1);
87
- dispose();
88
- // Second observation - should clear previous signals
89
- dispose = observer.observe();
90
- observer.subscribeSignal(signal2);
91
- dispose();
92
- // Notify first signal - should not trigger observer
93
- signal1.notify();
94
- await new Promise((resolve) => queueMicrotask(() => resolve()));
95
- expect(callCount).toBe(0);
96
- // Notify second signal - should trigger observer
97
- signal2.notify();
98
- await new Promise((resolve) => queueMicrotask(() => resolve()));
99
- expect(callCount).toBe(1);
100
- });
101
- it('should dispose of all signal subscriptions', async () => {
102
- const callback = vi.fn();
103
- const observer = new Observer(callback);
104
- const signal = new Signal();
105
- const dispose = observer.observe();
106
- observer.subscribeSignal(signal);
107
- dispose();
108
- observer.dispose();
109
- signal.notify();
110
- await new Promise((resolve) => queueMicrotask(() => resolve()));
111
- expect(callback).not.toHaveBeenCalled();
112
- });
113
- it('should set current observer during observation', () => {
114
- const observer = new Observer(() => { });
115
- expect(getCurrentObserver()).toBeUndefined();
116
- const dispose = observer.observe();
117
- expect(getCurrentObserver()).toBe(observer);
118
- dispose();
119
- expect(getCurrentObserver()).toBeUndefined();
120
- });
121
- it('should handle nested observations with stack', () => {
122
- const observer1 = new Observer(() => { });
123
- const observer2 = new Observer(() => { });
124
- const dispose1 = observer1.observe();
125
- expect(getCurrentObserver()).toBe(observer1);
126
- const dispose2 = observer2.observe();
127
- expect(getCurrentObserver()).toBe(observer2);
128
- dispose2();
129
- expect(getCurrentObserver()).toBe(observer1);
130
- dispose1();
131
- expect(getCurrentObserver()).toBeUndefined();
41
+ describe("Observer", () => {
42
+ it("should track signals during observation", () => {
43
+ const callback = vi.fn();
44
+ const observer = new Observer(callback);
45
+ const signal = new Signal();
46
+ const dispose = observer.observe();
47
+ observer.subscribeSignal(signal);
48
+ dispose();
49
+ signal.notify();
50
+ return new Promise((resolve) => {
51
+ queueMicrotask(() => {
52
+ expect(callback).toHaveBeenCalledTimes(1);
53
+ resolve(undefined);
54
+ });
132
55
  });
133
- it('should prevent duplicate notifications while queued', async () => {
134
- let callCount = 0;
135
- const observer = new Observer(() => {
136
- callCount++;
137
- });
138
- const signal = new Signal();
139
- const dispose = observer.observe();
140
- observer.subscribeSignal(signal);
141
- dispose();
142
- // Rapid-fire notifications
143
- for (let i = 0; i < 100; i++) {
144
- signal.notify();
145
- }
146
- await new Promise((resolve) => queueMicrotask(() => resolve()));
147
- // Should only be called once
148
- expect(callCount).toBe(1);
56
+ });
57
+ it("should clear signals when observing again", async () => {
58
+ let callCount = 0;
59
+ const observer = new Observer(() => {
60
+ callCount++;
149
61
  });
62
+ const signal1 = new Signal();
63
+ const signal2 = new Signal();
64
+ // First observation
65
+ let dispose = observer.observe();
66
+ observer.subscribeSignal(signal1);
67
+ dispose();
68
+ // Second observation - should clear previous signals
69
+ dispose = observer.observe();
70
+ observer.subscribeSignal(signal2);
71
+ dispose();
72
+ // Notify first signal - should not trigger observer
73
+ signal1.notify();
74
+ await new Promise((resolve) => queueMicrotask(() => resolve()));
75
+ expect(callCount).toBe(0);
76
+ // Notify second signal - should trigger observer
77
+ signal2.notify();
78
+ await new Promise((resolve) => queueMicrotask(() => resolve()));
79
+ expect(callCount).toBe(1);
80
+ });
81
+ it("should dispose of all signal subscriptions", async () => {
82
+ const callback = vi.fn();
83
+ const observer = new Observer(callback);
84
+ const signal = new Signal();
85
+ const dispose = observer.observe();
86
+ observer.subscribeSignal(signal);
87
+ dispose();
88
+ observer.dispose();
89
+ signal.notify();
90
+ await new Promise((resolve) => queueMicrotask(() => resolve()));
91
+ expect(callback).not.toHaveBeenCalled();
92
+ });
93
+ it("should set current observer during observation", () => {
94
+ const observer = new Observer(() => {});
95
+ expect(getCurrentObserver()).toBeUndefined();
96
+ const dispose = observer.observe();
97
+ expect(getCurrentObserver()).toBe(observer);
98
+ dispose();
99
+ expect(getCurrentObserver()).toBeUndefined();
100
+ });
101
+ it("should handle nested observations with stack", () => {
102
+ const observer1 = new Observer(() => {});
103
+ const observer2 = new Observer(() => {});
104
+ const dispose1 = observer1.observe();
105
+ expect(getCurrentObserver()).toBe(observer1);
106
+ const dispose2 = observer2.observe();
107
+ expect(getCurrentObserver()).toBe(observer2);
108
+ dispose2();
109
+ expect(getCurrentObserver()).toBe(observer1);
110
+ dispose1();
111
+ expect(getCurrentObserver()).toBeUndefined();
112
+ });
150
113
  });
@@ -11,11 +11,13 @@ class MockVNode extends AbstractVNode {
11
11
  constructor(key) {
12
12
  super();
13
13
  this.key = key;
14
+ // Initialize elm in constructor so getElements() always works
15
+ this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
14
16
  }
15
17
  mount(parent) {
16
18
  this.mountCalls++;
17
19
  this.parent = parent;
18
- this.elm = document.createTextNode(`mock-${this.key || "no-key"}`);
20
+ // elm already exists from constructor
19
21
  return this.elm;
20
22
  }
21
23
  patch(newNode) {
@@ -26,7 +28,8 @@ class MockVNode extends AbstractVNode {
26
28
  }
27
29
  unmount() {
28
30
  this.unmountCalls++;
29
- delete this.elm;
31
+ // Don't delete elm - it's needed for getElements() when building operations
32
+ // delete this.elm;
30
33
  delete this.parent;
31
34
  }
32
35
  rerender() {
@@ -37,6 +40,8 @@ class MockVNode extends AbstractVNode {
37
40
  class MockParentVNode extends MockVNode {
38
41
  constructor(initialChildren, key) {
39
42
  super(key);
43
+ // Override elm with an HTMLElement since parents need to contain children
44
+ this.elm = document.createElement("div");
40
45
  this.children = (initialChildren || []);
41
46
  }
42
47
  }
@@ -46,25 +51,33 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
46
51
  const newChild1 = new MockVNode("a");
47
52
  const newChild2 = new MockVNode("b");
48
53
  const parent = new MockParentVNode([]);
49
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
54
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
50
55
  // New children should be mounted
51
56
  expect(newChild1.mountCalls).toBe(1);
52
57
  expect(newChild2.mountCalls).toBe(1);
53
58
  expect(newChild1.parent).toBe(parent);
54
59
  expect(newChild2.parent).toBe(parent);
55
60
  // Result should be the new children (since old was empty)
56
- expect(result).toEqual([newChild1, newChild2]);
61
+ expect(children).toEqual([newChild1, newChild2]);
62
+ // Should return insert operation
63
+ expect(operations).toHaveLength(1);
64
+ expect(operations[0].type).toBe("insert");
65
+ expect(operations[0]).toHaveProperty("elms");
57
66
  });
58
67
  it("should unmount all old children when new is empty", () => {
59
68
  const oldChild1 = new MockVNode("a");
60
69
  const oldChild2 = new MockVNode("b");
61
70
  const parent = new MockParentVNode([oldChild1, oldChild2]);
62
- const result = parent.patchChildren(toVNodes([]));
71
+ const { children, operations } = parent.patchChildren(toVNodes([]));
63
72
  // Old children should be unmounted
64
73
  expect(oldChild1.unmountCalls).toBe(1);
65
74
  expect(oldChild2.unmountCalls).toBe(1);
66
75
  // Result should be empty
67
- expect(result).toEqual([]);
76
+ expect(children).toEqual([]);
77
+ // Should return remove operation
78
+ expect(operations).toHaveLength(1);
79
+ expect(operations[0].type).toBe("remove");
80
+ expect(operations[0]).toHaveProperty("elms");
68
81
  });
69
82
  });
70
83
  describe("Patching with keys", () => {
@@ -76,7 +89,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
76
89
  const newChild1 = new MockVNode("a");
77
90
  const newChild2 = new MockVNode("b");
78
91
  const newChild3 = new MockVNode("c");
79
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
92
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
80
93
  // OLD children should be patched with new children
81
94
  expect(oldChild1.patchCalls).toBe(1);
82
95
  expect(oldChild2.patchCalls).toBe(1);
@@ -93,7 +106,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
93
106
  expect(oldChild2.unmountCalls).toBe(0);
94
107
  expect(oldChild3.unmountCalls).toBe(0);
95
108
  // Result should still be the OLD children (reused)
96
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
109
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
110
+ // Should have no operations (just patching)
111
+ expect(operations).toHaveLength(0);
97
112
  });
98
113
  it("should handle reordered children with keys", () => {
99
114
  const oldChild1 = new MockVNode("a");
@@ -104,7 +119,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
104
119
  const newChild1 = new MockVNode("c");
105
120
  const newChild2 = new MockVNode("a");
106
121
  const newChild3 = new MockVNode("b");
107
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
122
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
108
123
  // Old nodes should be patched with corresponding new nodes by key
109
124
  expect(oldChild1.patchedWith).toBe(newChild2); // a->a
110
125
  expect(oldChild2.patchedWith).toBe(newChild3); // b->b
@@ -114,11 +129,14 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
114
129
  expect(oldChild2.unmountCalls).toBe(0);
115
130
  expect(oldChild3.unmountCalls).toBe(0);
116
131
  // Result should be old children in NEW order (c, a, b)
117
- expect(result).toEqual([oldChild3, oldChild1, oldChild2]);
132
+ expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
118
133
  // Verify correct keys
119
- expect(result[0].key).toBe("c");
120
- expect(result[1].key).toBe("a");
121
- expect(result[2].key).toBe("b");
134
+ expect(children[0].key).toBe("c");
135
+ expect(children[1].key).toBe("a");
136
+ expect(children[2].key).toBe("b");
137
+ // Should have move operations for reordered children
138
+ expect(operations.length).toBeGreaterThan(0);
139
+ expect(operations.some((op) => op.type === "move" || op.type === "insert")).toBe(true);
122
140
  });
123
141
  it("should mount new children and unmount removed children", () => {
124
142
  const oldChild1 = new MockVNode("a");
@@ -129,7 +147,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
129
147
  const newChild1 = new MockVNode("a");
130
148
  const newChild2 = new MockVNode("c");
131
149
  const newChild3 = new MockVNode("d");
132
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
150
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
133
151
  // a and c should be patched (reused)
134
152
  expect(oldChild1.patchedWith).toBe(newChild1);
135
153
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -138,11 +156,44 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
138
156
  // b should be unmounted
139
157
  expect(oldChild2.unmountCalls).toBe(1);
140
158
  // Result should contain old a, old c, and new d
141
- expect(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);
159
+ expect(children).toContain(oldChild1);
160
+ expect(children).toContain(oldChild3);
161
+ expect(children).toContain(newChild3);
162
+ expect(children).not.toContain(oldChild2);
163
+ expect(children.length).toBe(3);
164
+ // Should have operations for insert and remove
165
+ expect(operations.length).toBeGreaterThan(0);
166
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
167
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
168
+ });
169
+ it("should generate move operations when children are reordered", () => {
170
+ const oldChild1 = new MockVNode("a");
171
+ const oldChild2 = new MockVNode("b");
172
+ const parent = new MockParentVNode([oldChild1, oldChild2]);
173
+ // Swap order: b, a
174
+ const newChild1 = new MockVNode("b");
175
+ const newChild2 = new MockVNode("a");
176
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
177
+ // Both should be patched
178
+ expect(oldChild1.patchedWith).toBe(newChild2);
179
+ expect(oldChild2.patchedWith).toBe(newChild1);
180
+ // No unmounts
181
+ expect(oldChild1.unmountCalls).toBe(0);
182
+ expect(oldChild2.unmountCalls).toBe(0);
183
+ // Result should be old children in new order [b, a]
184
+ expect(children).toEqual([oldChild2, oldChild1]);
185
+ // Should have move or insert operations for repositioning
186
+ expect(operations.length).toBeGreaterThan(0);
187
+ const hasMoveOrInsert = operations.some((op) => op.type === "move" || op.type === "insert");
188
+ expect(hasMoveOrInsert).toBe(true);
189
+ // If there's a move operation, it should have the required properties
190
+ const moveOps = operations.filter((op) => op.type === "move");
191
+ moveOps.forEach((op) => {
192
+ if (op.type === "move") {
193
+ expect(op).toHaveProperty("elms");
194
+ expect(op).toHaveProperty("afterElm");
195
+ }
196
+ });
146
197
  });
147
198
  it("should replace all children when all keys change", () => {
148
199
  const oldChild1 = new MockVNode("a");
@@ -150,7 +201,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
150
201
  const parent = new MockParentVNode([oldChild1, oldChild2]);
151
202
  const newChild1 = new MockVNode("x");
152
203
  const newChild2 = new MockVNode("y");
153
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
204
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
154
205
  // All new children should be mounted
155
206
  expect(newChild1.mountCalls).toBe(1);
156
207
  expect(newChild2.mountCalls).toBe(1);
@@ -158,7 +209,11 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
158
209
  expect(oldChild1.unmountCalls).toBe(1);
159
210
  expect(oldChild2.unmountCalls).toBe(1);
160
211
  // Result should be the new children
161
- expect(result).toEqual([newChild1, newChild2]);
212
+ expect(children).toEqual([newChild1, newChild2]);
213
+ // Should have operations for both insert and remove
214
+ expect(operations.length).toBeGreaterThan(0);
215
+ expect(operations.some((op) => op.type === "insert")).toBe(true);
216
+ expect(operations.some((op) => op.type === "remove")).toBe(true);
162
217
  });
163
218
  });
164
219
  describe("Patching without keys (index-based)", () => {
@@ -170,7 +225,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
170
225
  const newChild1 = new MockVNode();
171
226
  const newChild2 = new MockVNode();
172
227
  const newChild3 = new MockVNode();
173
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
228
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
174
229
  // Should patch by index: 0->0, 1->1, 2->2
175
230
  expect(oldChild1.patchedWith).toBe(newChild1);
176
231
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -180,7 +235,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
180
235
  expect(oldChild2.unmountCalls).toBe(0);
181
236
  expect(oldChild3.unmountCalls).toBe(0);
182
237
  // Result should be old children (reused)
183
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
238
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
239
+ // Should have no operations (just patching)
240
+ expect(operations).toHaveLength(0);
184
241
  });
185
242
  it("should mount new children when growing without keys", () => {
186
243
  const oldChild1 = new MockVNode();
@@ -190,7 +247,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
190
247
  const newChild2 = new MockVNode();
191
248
  const newChild3 = new MockVNode();
192
249
  const newChild4 = new MockVNode();
193
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
250
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3, newChild4]));
194
251
  // First two should be patched (reused)
195
252
  expect(oldChild1.patchedWith).toBe(newChild1);
196
253
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -201,7 +258,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
201
258
  expect(oldChild1.unmountCalls).toBe(0);
202
259
  expect(oldChild2.unmountCalls).toBe(0);
203
260
  // Result should be [oldChild1, oldChild2, newChild3, newChild4]
204
- expect(result).toEqual([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);
205
265
  });
206
266
  it("should unmount old children when shrinking without keys", () => {
207
267
  const oldChild1 = new MockVNode();
@@ -216,7 +276,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
216
276
  ]);
217
277
  const newChild1 = new MockVNode();
218
278
  const newChild2 = new MockVNode();
219
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
279
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
220
280
  // First two should be patched
221
281
  expect(oldChild1.patchedWith).toBe(newChild1);
222
282
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -227,7 +287,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
227
287
  expect(oldChild1.unmountCalls).toBe(0);
228
288
  expect(oldChild2.unmountCalls).toBe(0);
229
289
  // Result should be [oldChild1, oldChild2]
230
- expect(result).toEqual([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);
231
294
  });
232
295
  });
233
296
  describe("Mixed keys and indices", () => {
@@ -239,7 +302,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
239
302
  const newChild1 = new MockVNode("a"); // key: "a"
240
303
  const newChild2 = new MockVNode(); // key: undefined -> index 1
241
304
  const newChild3 = new MockVNode("c"); // key: "c"
242
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
305
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
243
306
  // Keyed children should patch by key
244
307
  expect(oldChild1.patchedWith).toBe(newChild1);
245
308
  expect(oldChild3.patchedWith).toBe(newChild3);
@@ -250,7 +313,9 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
250
313
  expect(oldChild2.unmountCalls).toBe(0);
251
314
  expect(oldChild3.unmountCalls).toBe(0);
252
315
  // Result should be old children (reused)
253
- expect(result).toEqual([oldChild1, oldChild2, oldChild3]);
316
+ expect(children).toEqual([oldChild1, oldChild2, oldChild3]);
317
+ // Should have no operations (just patching)
318
+ expect(operations).toHaveLength(0);
254
319
  });
255
320
  });
256
321
  describe("Real-world scenarios", () => {
@@ -259,7 +324,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
259
324
  const parent = new MockParentVNode([oldChild1]);
260
325
  const newChild1 = new MockVNode("title");
261
326
  const newChild2 = new MockVNode("details");
262
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
327
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
263
328
  // Title should be patched (reused)
264
329
  expect(oldChild1.patchedWith).toBe(newChild1);
265
330
  // Details should be mounted
@@ -267,14 +332,17 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
267
332
  // No unmounts
268
333
  expect(oldChild1.unmountCalls).toBe(0);
269
334
  // Result should be [oldChild1, newChild2]
270
- expect(result).toEqual([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);
271
339
  });
272
340
  it("should handle conditional rendering (component -> null)", () => {
273
341
  const oldChild1 = new MockVNode("title");
274
342
  const oldChild2 = new MockVNode("details");
275
343
  const parent = new MockParentVNode([oldChild1, oldChild2]);
276
344
  const newChild1 = new MockVNode("title");
277
- const result = parent.patchChildren(toVNodes([newChild1]));
345
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1]));
278
346
  // Title should be patched (reused)
279
347
  expect(oldChild1.patchedWith).toBe(newChild1);
280
348
  // Details should be unmounted
@@ -282,7 +350,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
282
350
  // Title should not be unmounted
283
351
  expect(oldChild1.unmountCalls).toBe(0);
284
352
  // Result should be [oldChild1]
285
- expect(result).toEqual([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);
286
357
  });
287
358
  it("should handle list with items added at beginning", () => {
288
359
  const oldChild1 = new MockVNode("item-1");
@@ -291,7 +362,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
291
362
  const newChild1 = new MockVNode("item-0"); // New item at start
292
363
  const newChild2 = new MockVNode("item-1");
293
364
  const newChild3 = new MockVNode("item-2");
294
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
365
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
295
366
  // New item should be mounted
296
367
  expect(newChild1.mountCalls).toBe(1);
297
368
  // Existing items should be patched (reused)
@@ -301,7 +372,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
301
372
  expect(oldChild1.unmountCalls).toBe(0);
302
373
  expect(oldChild2.unmountCalls).toBe(0);
303
374
  // Result should be [newChild1, oldChild1, oldChild2]
304
- expect(result).toEqual([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);
305
379
  });
306
380
  it("should handle list with items added at end", () => {
307
381
  const oldChild1 = new MockVNode("item-1");
@@ -310,7 +384,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
310
384
  const newChild1 = new MockVNode("item-1");
311
385
  const newChild2 = new MockVNode("item-2");
312
386
  const newChild3 = new MockVNode("item-3"); // New item at end
313
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
387
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
314
388
  // Existing items should be patched (reused)
315
389
  expect(oldChild1.patchedWith).toBe(newChild1);
316
390
  expect(oldChild2.patchedWith).toBe(newChild2);
@@ -320,7 +394,10 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
320
394
  expect(oldChild1.unmountCalls).toBe(0);
321
395
  expect(oldChild2.unmountCalls).toBe(0);
322
396
  // Result should be [oldChild1, oldChild2, newChild3]
323
- expect(result).toEqual([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);
324
401
  });
325
402
  it("should handle list with item removed from middle", () => {
326
403
  const oldChild1 = new MockVNode("item-1");
@@ -329,7 +406,7 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
329
406
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
330
407
  const newChild1 = new MockVNode("item-1");
331
408
  const newChild2 = new MockVNode("item-3"); // item-2 removed
332
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
409
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
333
410
  // item-1 and item-3 should be patched (reused)
334
411
  expect(oldChild1.patchedWith).toBe(newChild1);
335
412
  expect(oldChild3.patchedWith).toBe(newChild2);
@@ -339,33 +416,42 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
339
416
  expect(oldChild1.unmountCalls).toBe(0);
340
417
  expect(oldChild3.unmountCalls).toBe(0);
341
418
  // Result should be [oldChild1, oldChild3]
342
- expect(result).toEqual([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);
343
423
  });
344
424
  it("should handle empty -> multiple children", () => {
345
425
  const parent = new MockParentVNode([]);
346
426
  const newChild1 = new MockVNode("a");
347
427
  const newChild2 = new MockVNode("b");
348
428
  const newChild3 = new MockVNode("c");
349
- const result = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
429
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
350
430
  // All should be mounted
351
431
  expect(newChild1.mountCalls).toBe(1);
352
432
  expect(newChild2.mountCalls).toBe(1);
353
433
  expect(newChild3.mountCalls).toBe(1);
354
434
  // Result should be the new children
355
- expect(result).toEqual([newChild1, newChild2, newChild3]);
435
+ expect(children).toEqual([newChild1, newChild2, newChild3]);
436
+ // Should have insert operation
437
+ expect(operations).toHaveLength(1);
438
+ expect(operations[0].type).toBe("insert");
356
439
  });
357
440
  it("should handle multiple children -> empty", () => {
358
441
  const oldChild1 = new MockVNode("a");
359
442
  const oldChild2 = new MockVNode("b");
360
443
  const oldChild3 = new MockVNode("c");
361
444
  const parent = new MockParentVNode([oldChild1, oldChild2, oldChild3]);
362
- const result = parent.patchChildren(toVNodes([]));
445
+ const { children, operations } = parent.patchChildren(toVNodes([]));
363
446
  // All should be unmounted
364
447
  expect(oldChild1.unmountCalls).toBe(1);
365
448
  expect(oldChild2.unmountCalls).toBe(1);
366
449
  expect(oldChild3.unmountCalls).toBe(1);
367
450
  // Result should be empty
368
- expect(result).toEqual([]);
451
+ expect(children).toEqual([]);
452
+ // Should have remove operation
453
+ expect(operations).toHaveLength(1);
454
+ expect(operations[0].type).toBe("remove");
369
455
  });
370
456
  });
371
457
  describe("Object reference preservation", () => {
@@ -375,13 +461,66 @@ describe("patchChildren (new approach: keep old, patch in new)", () => {
375
461
  const parent = new MockParentVNode([oldChild1, oldChild2]);
376
462
  const newChild1 = new MockVNode("a");
377
463
  const newChild2 = new MockVNode("b");
378
- const result = parent.patchChildren(toVNodes([newChild1, newChild2]));
464
+ const { children, operations } = parent.patchChildren(toVNodes([newChild1, newChild2]));
379
465
  // The result should contain the EXACT SAME object references as the old children
380
- expect(result[0]).toBe(oldChild1); // Same object reference
381
- expect(result[1]).toBe(oldChild2); // Same object reference
466
+ expect(children[0]).toBe(oldChild1); // Same object reference
467
+ expect(children[1]).toBe(oldChild2); // Same object reference
382
468
  // NOT the new children
383
- expect(result[0]).not.toBe(newChild1);
384
- expect(result[1]).not.toBe(newChild2);
469
+ expect(children[0]).not.toBe(newChild1);
470
+ expect(children[1]).not.toBe(newChild2);
471
+ // Should have no operations (just patching)
472
+ expect(operations).toHaveLength(0);
473
+ });
474
+ });
475
+ describe("Move operations - minimal reproduction", () => {
476
+ it("should correctly reorder [A,B,C] to [C,A,B] in actual DOM", () => {
477
+ // This test uses actual DOM to verify move operations work correctly
478
+ const parent = document.createElement("div");
479
+ // Create initial DOM elements
480
+ const elmA = document.createTextNode("A");
481
+ const elmB = document.createTextNode("B");
482
+ const elmC = document.createTextNode("C");
483
+ parent.appendChild(elmA);
484
+ parent.appendChild(elmB);
485
+ parent.appendChild(elmC);
486
+ // Create VNodes that represent these elements
487
+ const oldChild1 = new MockVNode("a");
488
+ const oldChild2 = new MockVNode("b");
489
+ const oldChild3 = new MockVNode("c");
490
+ oldChild1.elm = elmA;
491
+ oldChild2.elm = elmB;
492
+ oldChild3.elm = elmC;
493
+ const parentVNode = new MockParentVNode([
494
+ oldChild1,
495
+ oldChild2,
496
+ oldChild3,
497
+ ]);
498
+ parentVNode.elm = parent;
499
+ // Verify initial DOM order
500
+ expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
501
+ "A",
502
+ "B",
503
+ "C",
504
+ ]);
505
+ // Reorder to C, A, B
506
+ const newChild1 = new MockVNode("c");
507
+ const newChild2 = new MockVNode("a");
508
+ const newChild3 = new MockVNode("b");
509
+ const { children, operations } = parentVNode.patchChildren(toVNodes([newChild1, newChild2, newChild3]));
510
+ // Result should be old children in new order
511
+ expect(children).toEqual([oldChild3, oldChild1, oldChild2]);
512
+ // Apply the operations to the actual DOM
513
+ parentVNode.applyDOMOperations(operations);
514
+ // Verify DOM is correctly reordered
515
+ expect(Array.from(parent.childNodes).map((n) => n.textContent)).toEqual([
516
+ "C",
517
+ "A",
518
+ "B",
519
+ ]);
520
+ // Verify same DOM elements were moved, not recreated
521
+ expect(parent.childNodes[0]).toBe(elmC);
522
+ expect(parent.childNodes[1]).toBe(elmA);
523
+ expect(parent.childNodes[2]).toBe(elmB);
385
524
  });
386
525
  });
387
526
  });
@@ -1,5 +1,21 @@
1
1
  import { RootVNode } from "./RootVNode";
2
2
  import { VNode } from "./types";
3
+ export type DOMOperation = {
4
+ type: "replace";
5
+ oldElms: Node[];
6
+ newElms: Node[];
7
+ } | {
8
+ type: "remove";
9
+ elms: Node[];
10
+ } | {
11
+ type: "insert";
12
+ elms: Node[];
13
+ afterElm?: Node;
14
+ } | {
15
+ type: "move";
16
+ elms: Node[];
17
+ afterElm?: Node;
18
+ };
3
19
  export declare abstract class AbstractVNode {
4
20
  key?: string;
5
21
  parent?: VNode;
@@ -9,7 +25,7 @@ export declare abstract class AbstractVNode {
9
25
  abstract mount(parent?: VNode): Node | Node[];
10
26
  abstract patch(oldNode: VNode): void;
11
27
  abstract unmount(): void;
12
- abstract rerender(): void;
28
+ applyDOMOperations(operations: DOMOperation[], atVNode?: VNode): void;
13
29
  protected getHTMLElement(): HTMLElement;
14
30
  /**
15
31
  * A VNode can represent multiple elements (fragment of component)
@@ -17,6 +33,9 @@ export declare abstract class AbstractVNode {
17
33
  getElements(): Node[];
18
34
  getParentElement(): HTMLElement;
19
35
  protected canPatch(oldNode: VNode, newNode: VNode): boolean;
20
- patchChildren(newChildren: VNode[]): VNode[];
36
+ patchChildren(newChildren: VNode[]): {
37
+ children: VNode[];
38
+ operations: DOMOperation[];
39
+ };
21
40
  }
22
41
  //# sourceMappingURL=AbstractVNode.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,8BAAsB,aAAa;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,GAAG,CAAC,EAAE,IAAI,CAAC;IACX,QAAQ,CAAC,EAAE,KAAK,EAAE,CAAC;IACnB,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,EAAE;IAC7C,QAAQ,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,GAAG,IAAI;IACpC,QAAQ,CAAC,OAAO,IAAI,IAAI;IACxB,QAAQ,CAAC,QAAQ,IAAI,IAAI;IACzB,SAAS,CAAC,cAAc;IAOxB;;OAEG;IACH,WAAW,IAAI,IAAI,EAAE;IAWrB,gBAAgB,IAAI,WAAW;IAiB/B,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAG,OAAO;IAoB3D,aAAa,CAAC,WAAW,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE;CA2D7C"}
1
+ {"version":3,"file":"AbstractVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/AbstractVNode.ts"],"names":[],"mappings":"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,9 +1,77 @@
1
+ import { elementsToFragment } from "./dom-utils";
1
2
  export class AbstractVNode {
2
3
  key;
3
4
  parent;
4
5
  root;
5
6
  elm;
6
7
  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
+ }
7
75
  getHTMLElement() {
8
76
  if (!this.elm || !(this.elm instanceof HTMLElement)) {
9
77
  throw new Error("This VNode does not have an HTMLElement");
@@ -18,6 +86,7 @@ export class AbstractVNode {
18
86
  return [this.elm];
19
87
  }
20
88
  if (!this.children) {
89
+ console.log("WTF", this);
21
90
  throw new Error("This VNode has no element or children");
22
91
  }
23
92
  return this.children.map((child) => child.getElements()).flat();
@@ -55,52 +124,132 @@ export class AbstractVNode {
55
124
  patchChildren(newChildren) {
56
125
  const prevChildren = this.children;
57
126
  // When there are only new children, we just mount them
58
- if (newChildren && prevChildren.length === 0) {
127
+ if (prevChildren.length === 0) {
59
128
  newChildren.forEach((child) => child.mount(this));
60
- return newChildren;
129
+ return {
130
+ children: newChildren,
131
+ operations: [
132
+ {
133
+ type: "insert",
134
+ elms: newChildren.map((child) => child.getElements()).flat(),
135
+ },
136
+ ],
137
+ };
61
138
  }
62
139
  // If we want to remove all children, we just unmount the previous ones
63
140
  if (!newChildren.length && prevChildren.length) {
64
141
  prevChildren.forEach((child) => child.unmount());
65
- return [];
142
+ return {
143
+ children: [],
144
+ operations: [
145
+ {
146
+ type: "remove",
147
+ elms: prevChildren.map((child) => child.getElements()).flat(),
148
+ },
149
+ ],
150
+ };
66
151
  }
152
+ const operations = [];
67
153
  const oldKeys = {};
154
+ // Build oldKeys map and handle duplicate keys
68
155
  prevChildren.forEach((prevChild, index) => {
69
- oldKeys[prevChild.key || index] = prevChild;
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;
70
163
  });
71
- // Build result array in the NEW order
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) ===
72
180
  const result = [];
181
+ const newChildrenMeta = [];
73
182
  newChildren.forEach((newChild, index) => {
74
183
  const key = newChild.key || index;
75
- const prevChild = oldKeys[key];
76
- if (!prevChild) {
184
+ const oldChild = oldKeys[key];
185
+ if (!oldChild) {
77
186
  // New child - mount and add to result
78
187
  newChild.mount(this);
79
188
  result.push(newChild);
189
+ newChildrenMeta.push({ isNew: true });
80
190
  }
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)) {
191
+ else if (this.canPatch(oldChild, newChild)) {
87
192
  // Compatible types - patch and reuse old VNode
88
- prevChild.patch(newChild);
89
- result.push(prevChild);
193
+ if (oldChild !== newChild) {
194
+ oldChild.patch(newChild);
195
+ }
196
+ result.push(oldChild);
90
197
  delete oldKeys[key];
198
+ newChildrenMeta.push({ isNew: false });
91
199
  }
92
200
  else {
93
201
  // Incompatible types - replace completely
94
202
  newChild.mount(this);
95
- prevChild.unmount();
203
+ oldChild.unmount();
96
204
  result.push(newChild);
97
205
  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
+ });
98
242
  }
243
+ // else: child is in same position, no operation needed
99
244
  });
100
245
  // Unmount any old children that weren't reused
101
246
  for (const key in oldKeys) {
102
247
  oldKeys[key].unmount();
248
+ operations.push({ type: "remove", elms: oldKeys[key].getElements() });
103
249
  }
104
- return result;
250
+ return {
251
+ children: result,
252
+ operations,
253
+ };
105
254
  }
106
255
  }
@@ -40,7 +40,6 @@ 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;
44
43
  mount(parent?: VNode): Node[];
45
44
  patch(newNode: ComponentVNode): void;
46
45
  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,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGvC,MAAM,MAAM,cAAc,GACtB,KAAK,GACL,MAAM,GACN,IAAI,GACJ,MAAM,GACN,SAAS,GACT,OAAO,CAAC;AACZ,MAAM,MAAM,iBAAiB,GAAG,cAAc,GAAG,cAAc,EAAE,CAAC;AAElE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,KAAK,IACjC,CAAC,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,iBAAiB,CAAC,GACvC,CAAC,MAAM,MAAM,iBAAiB,CAAC,CAAC;AAEpC,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC;IACf,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC5B,UAAU,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC,CAAC;AAKF,wBAAgB,mBAAmB,sBAYlC;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,IAAI,QAYrC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI,QAYvC;AAED,qBAAa,cAAe,SAAQ,aAAa;IAC/C,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IAC1B,KAAK,EAAE,KAAK,CAAC;IAEb,QAAQ,EAAE,KAAK,EAAE,CAAM;IACvB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;gBAE3B,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,EACzB,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IAWd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAyI7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
1
+ {"version":3,"file":"ComponentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ComponentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,aAAa,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"}
@@ -51,9 +51,6 @@ export class ComponentVNode extends AbstractVNode {
51
51
  this.children = [];
52
52
  this.key = key;
53
53
  }
54
- rerender() {
55
- this.parent?.rerender();
56
- }
57
54
  mount(parent) {
58
55
  this.parent = parent;
59
56
  if (parent instanceof RootVNode) {
@@ -97,7 +94,8 @@ export class ComponentVNode extends AbstractVNode {
97
94
  this.root?.setAsCurrent();
98
95
  const newChildren = executeRender();
99
96
  const prevChildren = this.children;
100
- this.children = this.patchChildren(newChildren);
97
+ const { children, operations } = this.patchChildren(newChildren);
98
+ this.children = children;
101
99
  // Typically components return a single element, which does
102
100
  // not require the parent to apply elements to the DOM again
103
101
  const canSelfUpdate = prevChildren.length === 1 &&
@@ -106,7 +104,7 @@ export class ComponentVNode extends AbstractVNode {
106
104
  this.children[0] instanceof ElementVNode &&
107
105
  this.canPatch(prevChildren[0], this.children[0]);
108
106
  if (!canSelfUpdate) {
109
- this.parent?.rerender();
107
+ this.parent?.applyDOMOperations(operations, this);
110
108
  }
111
109
  this.root?.clearCurrent();
112
110
  });
@@ -8,7 +8,6 @@ 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;
12
11
  mount(parent?: VNode): Node;
13
12
  /**
14
13
  * An ELEMENT patch goes through three operations
@@ -20,11 +19,5 @@ export declare class ElementVNode extends AbstractVNode {
20
19
  private setProp;
21
20
  private patchProps;
22
21
  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;
29
22
  }
30
23
  //# 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,EAAE,MAAM,iBAAiB,CAAC;AAGhD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAUvC,qBAAa,YAAa,SAAQ,aAAa;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,GAAG,CAAC,CAA0D;IACtE,OAAO,CAAC,cAAc,CAAC,CAA6B;gBAElD,GAAG,EAAE,MAAM,EACX,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE,EAAE,KAAK,EACxB,QAAQ,EAAE,KAAK,EAAE,EACjB,GAAG,CAAC,EAAE,MAAM;IASd,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI;IAkC3B;;;;OAIG;IACH,KAAK,CAAC,OAAO,EAAE,YAAY;IAM3B,OAAO;IAYP,OAAO,CAAC,OAAO,CAoCb;IACF,OAAO,CAAC,UAAU;IAGlB,OAAO,CAAC,gBAAgB;IAgBxB;;;;OAIG;IACH,OAAO,CAAC,eAAe;CAyBxB"}
1
+ {"version":3,"file":"ElementVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/ElementVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,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"}
@@ -17,9 +17,6 @@ export class ElementVNode extends AbstractVNode {
17
17
  this.key = key;
18
18
  this.ref = ref;
19
19
  }
20
- rerender() {
21
- this.syncDOMChildren();
22
- }
23
20
  mount(parent) {
24
21
  this.parent = parent;
25
22
  if (parent instanceof RootVNode) {
@@ -57,8 +54,9 @@ export class ElementVNode extends AbstractVNode {
57
54
  patch(newNode) {
58
55
  this.patchProps(newNode.props);
59
56
  this.props = newNode.props;
60
- this.children = this.patchChildren(newNode.children);
61
- this.syncDOMChildren();
57
+ const { children, operations } = this.patchChildren(newNode.children);
58
+ this.children = children;
59
+ this.applyDOMOperations(operations);
62
60
  }
63
61
  unmount() {
64
62
  this.children.forEach((child) => child.unmount());
@@ -117,32 +115,4 @@ export class ElementVNode extends AbstractVNode {
117
115
  delete this.eventListeners[type];
118
116
  }
119
117
  }
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
- }
148
118
  }
@@ -6,7 +6,6 @@ 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;
10
9
  patch(newNode: FragmentVNode): void;
11
10
  unmount(): void;
12
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,QAAQ,IAAI,IAAI;IAGhB,KAAK,CAAC,OAAO,EAAE,aAAa;IAG5B,OAAO;CAMR"}
1
+ {"version":3,"file":"FragmentVNode.d.ts","sourceRoot":"","sources":["../../src/vdom/FragmentVNode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAKhD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,eAAO,MAAM,QAAQ,eAAqB,CAAC;AAE3C,qBAAa,aAAc,SAAQ,aAAa;IAC9C,QAAQ,EAAE,KAAK,EAAE,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;gBAED,QAAQ,EAAE,KAAK,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM;IAK3C,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE;IAW7B,KAAK,CAAC,OAAO,EAAE,aAAa;IAK5B,OAAO;CAMR"}
@@ -19,11 +19,10 @@ 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
- }
25
22
  patch(newNode) {
26
- this.children = this.patchChildren(newNode.children);
23
+ const { children, operations } = this.patchChildren(newNode.children);
24
+ this.children = children;
25
+ this.applyDOMOperations(operations);
27
26
  }
28
27
  unmount() {
29
28
  this.children.forEach((child) => child.unmount());
@@ -1,4 +1,4 @@
1
- import { AbstractVNode } from "./AbstractVNode";
1
+ import { AbstractVNode, DOMOperation } 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
- rerender(): void;
22
+ applyDOMOperations(operations: DOMOperation[], atVNode: VNode): 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,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"}
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"}
@@ -59,11 +59,8 @@ export class RootVNode extends AbstractVNode {
59
59
  return this.children.map((childNode) => childNode.mount(this)).flat();
60
60
  }
61
61
  patch() { }
62
- rerender() {
63
- const childrenElms = this.children
64
- .map((child) => child.getElements())
65
- .flat();
66
- this.elm.replaceChildren(...childrenElms);
62
+ applyDOMOperations(operations, atVNode) {
63
+ super.applyDOMOperations(operations, atVNode);
67
64
  this.flushLifecycle();
68
65
  }
69
66
  unmount() { }
@@ -5,7 +5,6 @@ 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;
9
8
  unmount(): void;
10
9
  }
11
10
  //# 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,QAAQ,IAAI,IAAI;IAChB,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,OAAO;CAMR"}
@@ -25,7 +25,6 @@ export class TextVNode extends AbstractVNode {
25
25
  this.text = newNode.text;
26
26
  this.elm.textContent = this.text;
27
27
  }
28
- rerender() { }
29
28
  unmount() {
30
29
  this.root?.queueUnmount(() => {
31
30
  delete this.elm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rask-ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",