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