rask-ui 0.2.0 → 0.2.2
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/README.md +923 -185
- package/dist/observation.d.ts +1 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +3 -16
- package/dist/tests/class.test.js +42 -0
- package/dist/tests/component.props.test.js +252 -6
- package/dist/tests/component.state.test.js +21 -15
- package/dist/tests/createState.test.js +8 -16
- package/dist/tests/observation.test.js +0 -37
- package/dist/vdom/ComponentVNode.d.ts.map +1 -1
- package/dist/vdom/ComponentVNode.js +26 -16
- package/dist/vdom/ElementVNode.d.ts +6 -0
- package/dist/vdom/ElementVNode.d.ts.map +1 -1
- package/dist/vdom/ElementVNode.js +30 -8
- package/dist/vdom/RootVNode.d.ts +3 -0
- package/dist/vdom/RootVNode.d.ts.map +1 -1
- package/dist/vdom/RootVNode.js +15 -0
- package/dist/vdom/TextVNode.d.ts.map +1 -1
- package/dist/vdom/TextVNode.js +3 -0
- package/dist/vdom/dom-utils.d.ts.map +1 -1
- package/dist/vdom/dom-utils.js +13 -3
- package/package.json +4 -3
package/dist/observation.d.ts
CHANGED
|
@@ -5,11 +5,10 @@ export declare class Signal {
|
|
|
5
5
|
notify(): void;
|
|
6
6
|
}
|
|
7
7
|
export declare class Observer {
|
|
8
|
-
|
|
8
|
+
isDisposed: boolean;
|
|
9
9
|
private signalDisposers;
|
|
10
10
|
private clearSignals;
|
|
11
11
|
private onNotify;
|
|
12
|
-
private isQueued;
|
|
13
12
|
constructor(onNotify: () => void);
|
|
14
13
|
subscribeSignal(signal: Signal): void;
|
|
15
14
|
observe(): () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,aAEjC;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAyB;IAC5C,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI;IAOxB,MAAM;CAGP;AAED,qBAAa,QAAQ;IACnB,
|
|
1
|
+
{"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,aAEjC;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,WAAW,CAAyB;IAC5C,SAAS,CAAC,EAAE,EAAE,MAAM,IAAI;IAOxB,MAAM;CAGP;AAED,qBAAa,QAAQ;IACnB,UAAU,UAAS;IACnB,OAAO,CAAC,eAAe,CAAyB;IAChD,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,QAAQ,CAAa;gBACjB,QAAQ,EAAE,MAAM,IAAI;IAGhC,eAAe,CAAC,MAAM,EAAE,MAAM;IAG9B,OAAO;IAOP,OAAO;CAIR"}
|
package/dist/observation.js
CHANGED
|
@@ -15,28 +15,15 @@ export class Signal {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
export class Observer {
|
|
18
|
-
|
|
18
|
+
isDisposed = false;
|
|
19
19
|
signalDisposers = new Set();
|
|
20
20
|
clearSignals() {
|
|
21
21
|
this.signalDisposers.forEach((dispose) => dispose());
|
|
22
22
|
this.signalDisposers.clear();
|
|
23
23
|
}
|
|
24
24
|
onNotify;
|
|
25
|
-
isQueued = false;
|
|
26
25
|
constructor(onNotify) {
|
|
27
|
-
this.onNotify =
|
|
28
|
-
if (this.isQueued) {
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
queueMicrotask(() => {
|
|
32
|
-
this.isQueued = false;
|
|
33
|
-
if (this._isDisposed) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
onNotify();
|
|
37
|
-
});
|
|
38
|
-
this.isQueued = true;
|
|
39
|
-
};
|
|
26
|
+
this.onNotify = onNotify;
|
|
40
27
|
}
|
|
41
28
|
subscribeSignal(signal) {
|
|
42
29
|
this.signalDisposers.add(signal.subscribe(this.onNotify));
|
|
@@ -50,6 +37,6 @@ export class Observer {
|
|
|
50
37
|
}
|
|
51
38
|
dispose() {
|
|
52
39
|
this.clearSignals();
|
|
53
|
-
this.
|
|
40
|
+
this.isDisposed = true;
|
|
54
41
|
}
|
|
55
42
|
}
|
package/dist/tests/class.test.js
CHANGED
|
@@ -46,6 +46,7 @@ describe("Class Property Support", () => {
|
|
|
46
46
|
render(jsx("div", { class: {} }), container);
|
|
47
47
|
const div = container.querySelector("div");
|
|
48
48
|
expect(div?.className).toBe("");
|
|
49
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
49
50
|
});
|
|
50
51
|
it("should handle all false object notation", () => {
|
|
51
52
|
const container = document.createElement("div");
|
|
@@ -57,6 +58,7 @@ describe("Class Property Support", () => {
|
|
|
57
58
|
}), container);
|
|
58
59
|
const div = container.querySelector("div");
|
|
59
60
|
expect(div?.className).toBe("");
|
|
61
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
60
62
|
});
|
|
61
63
|
it("should update classes when object notation changes", async () => {
|
|
62
64
|
const container = document.createElement("div");
|
|
@@ -124,6 +126,46 @@ describe("Class Property Support", () => {
|
|
|
124
126
|
render(jsx("div", { class: undefined }), container);
|
|
125
127
|
const div = container.querySelector("div");
|
|
126
128
|
expect(div?.className).toBe("");
|
|
129
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
it("should remove class attribute when empty string is provided", () => {
|
|
132
|
+
const container = document.createElement("div");
|
|
133
|
+
render(jsx("div", { class: "" }), container);
|
|
134
|
+
const div = container.querySelector("div");
|
|
135
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
it("should remove class attribute when null is provided", () => {
|
|
138
|
+
const container = document.createElement("div");
|
|
139
|
+
render(jsx("div", { class: null }), container);
|
|
140
|
+
const div = container.querySelector("div");
|
|
141
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
it("should remove class attribute when object notation results in empty string", () => {
|
|
144
|
+
const container = document.createElement("div");
|
|
145
|
+
render(jsx("div", {
|
|
146
|
+
class: {
|
|
147
|
+
active: false,
|
|
148
|
+
visible: false,
|
|
149
|
+
},
|
|
150
|
+
}), container);
|
|
151
|
+
const div = container.querySelector("div");
|
|
152
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
it("should remove class attribute when updating from non-empty to empty string", async () => {
|
|
155
|
+
const container = document.createElement("div");
|
|
156
|
+
let stateFn;
|
|
157
|
+
const App = () => {
|
|
158
|
+
const state = createState({ className: "initial" });
|
|
159
|
+
stateFn = state;
|
|
160
|
+
return () => jsx("div", { class: state.className });
|
|
161
|
+
};
|
|
162
|
+
render(jsx(App, {}), container);
|
|
163
|
+
const div = container.querySelector("div");
|
|
164
|
+
expect(div?.className).toBe("initial");
|
|
165
|
+
expect(div?.hasAttribute("class")).toBe(true);
|
|
166
|
+
stateFn.className = "";
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
168
|
+
expect(div?.hasAttribute("class")).toBe(false);
|
|
127
169
|
});
|
|
128
170
|
it("should handle dynamic string class updates", async () => {
|
|
129
171
|
const container = document.createElement("div");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { jsx, render } from "../vdom";
|
|
3
|
+
import { createState } from "../createState";
|
|
3
4
|
describe("Component Props", () => {
|
|
4
5
|
it("should pass string props to component", () => {
|
|
5
6
|
const container = document.createElement("div");
|
|
@@ -75,14 +76,259 @@ describe("Component Props", () => {
|
|
|
75
76
|
render(jsx(MyComponent, { message: "Custom message" }), container2);
|
|
76
77
|
expect(container2.children[0].textContent).toBe("Custom message");
|
|
77
78
|
});
|
|
78
|
-
it("should
|
|
79
|
+
it("should pass props from parent to child component", () => {
|
|
79
80
|
const container = document.createElement("div");
|
|
80
|
-
const
|
|
81
|
+
const ChildComponent = (props) => {
|
|
81
82
|
return () => jsx("div", { children: props.message });
|
|
82
83
|
};
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const ParentComponent = (props) => {
|
|
85
|
+
return () => jsx("div", {
|
|
86
|
+
children: jsx(ChildComponent, { message: props.text }),
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
render(jsx(ParentComponent, { text: "Hello from parent" }), container);
|
|
90
|
+
expect(container.children[0].children[0].textContent).toBe("Hello from parent");
|
|
91
|
+
});
|
|
92
|
+
it("should pass props through multiple levels of nesting", () => {
|
|
93
|
+
const container = document.createElement("div");
|
|
94
|
+
const GrandchildComponent = (props) => {
|
|
95
|
+
return () => jsx("span", { children: props.value });
|
|
96
|
+
};
|
|
97
|
+
const ChildComponent = (props) => {
|
|
98
|
+
return () => jsx("div", {
|
|
99
|
+
children: jsx(GrandchildComponent, { value: props.data }),
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const ParentComponent = (props) => {
|
|
103
|
+
return () => jsx("div", {
|
|
104
|
+
children: jsx(ChildComponent, { data: props.info }),
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
render(jsx(ParentComponent, { info: "Deep nested value" }), container);
|
|
108
|
+
const span = container.querySelector("span");
|
|
109
|
+
expect(span?.textContent).toBe("Deep nested value");
|
|
110
|
+
});
|
|
111
|
+
it("should update nested component when parent props change", async () => {
|
|
112
|
+
const container = document.createElement("div");
|
|
113
|
+
const ChildComponent = (props) => {
|
|
114
|
+
return () => jsx("div", { children: String(props.count) });
|
|
115
|
+
};
|
|
116
|
+
const ParentComponent = () => {
|
|
117
|
+
const state = createState({ value: 5 });
|
|
118
|
+
return () => jsx("div", {
|
|
119
|
+
children: [
|
|
120
|
+
jsx(ChildComponent, { count: state.value }),
|
|
121
|
+
jsx("button", {
|
|
122
|
+
onclick: () => (state.value = 10),
|
|
123
|
+
children: "Update",
|
|
124
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
render(jsx(ParentComponent, {}), container);
|
|
129
|
+
expect(container.children[0].children[0].textContent).toBe("5");
|
|
130
|
+
// Trigger update
|
|
131
|
+
container.querySelector("button").click();
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
133
|
+
expect(container.children[0].children[0].textContent).toBe("10");
|
|
134
|
+
});
|
|
135
|
+
it("should correctly update props in nested components with state changes", async () => {
|
|
136
|
+
const container = document.createElement("div");
|
|
137
|
+
const ChildComponent = (props) => {
|
|
138
|
+
return () => jsx("div", { children: `${props.message}: ${props.count}` });
|
|
139
|
+
};
|
|
140
|
+
const ParentComponent = () => {
|
|
141
|
+
const state = createState({ count: 0 });
|
|
142
|
+
return () => jsx("div", {
|
|
143
|
+
children: [
|
|
144
|
+
jsx(ChildComponent, { message: "Count", count: state.count }),
|
|
145
|
+
jsx("button", {
|
|
146
|
+
onclick: () => state.count++,
|
|
147
|
+
children: "Increment",
|
|
148
|
+
}),
|
|
149
|
+
],
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
render(jsx(ParentComponent, {}), container);
|
|
153
|
+
// Check initial state
|
|
154
|
+
const childDiv = container.querySelector(":scope > div > div");
|
|
155
|
+
expect(childDiv.textContent).toBe("Count: 0");
|
|
156
|
+
// Simulate button click
|
|
157
|
+
const button = container.querySelector("button");
|
|
158
|
+
button.click();
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
160
|
+
// Check updated state - THIS is where the bug would show
|
|
161
|
+
// If old props are used instead of new props, this will fail
|
|
162
|
+
expect(childDiv.textContent).toBe("Count: 1");
|
|
163
|
+
});
|
|
164
|
+
it("should pass updated object props to nested components", async () => {
|
|
165
|
+
const container = document.createElement("div");
|
|
166
|
+
const ChildComponent = (props) => {
|
|
167
|
+
return () => jsx("div", { children: `${props.user.name} - ${props.user.age}` });
|
|
168
|
+
};
|
|
169
|
+
const ParentComponent = () => {
|
|
170
|
+
const state = createState({
|
|
171
|
+
userData: { name: "Alice", age: 25 },
|
|
172
|
+
});
|
|
173
|
+
return () => jsx("div", {
|
|
174
|
+
children: [
|
|
175
|
+
jsx(ChildComponent, { user: state.userData }),
|
|
176
|
+
jsx("button", {
|
|
177
|
+
onclick: () => {
|
|
178
|
+
state.userData.name = "Bob";
|
|
179
|
+
state.userData.age = 30;
|
|
180
|
+
},
|
|
181
|
+
children: "Update",
|
|
182
|
+
}),
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
render(jsx(ParentComponent, {}), container);
|
|
187
|
+
expect(container.children[0].children[0].textContent).toBe("Alice - 25");
|
|
188
|
+
// Trigger update
|
|
189
|
+
container.querySelector("button").click();
|
|
190
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
191
|
+
expect(container.children[0].children[0].textContent).toBe("Bob - 30");
|
|
192
|
+
});
|
|
193
|
+
it("should correctly update props in deeply nested components on state change", async () => {
|
|
194
|
+
const container = document.createElement("div");
|
|
195
|
+
const DeepChild = (props) => {
|
|
196
|
+
return () => jsx("span", { children: String(props.value) });
|
|
197
|
+
};
|
|
198
|
+
const MiddleChild = (props) => {
|
|
199
|
+
return () => jsx("div", {
|
|
200
|
+
children: jsx(DeepChild, { value: props.data }),
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
const ParentComponent = () => {
|
|
204
|
+
const state = createState({ value: 100 });
|
|
205
|
+
return () => jsx("div", {
|
|
206
|
+
children: [
|
|
207
|
+
jsx(MiddleChild, { data: state.value }),
|
|
208
|
+
jsx("button", {
|
|
209
|
+
onclick: () => (state.value += 50),
|
|
210
|
+
children: "Update",
|
|
211
|
+
}),
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
render(jsx(ParentComponent, {}), container);
|
|
216
|
+
// Check initial state
|
|
217
|
+
const span = container.querySelector("span");
|
|
218
|
+
expect(span.textContent).toBe("100");
|
|
219
|
+
// Trigger update
|
|
220
|
+
const button = container.querySelector("button");
|
|
221
|
+
button.click();
|
|
222
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
223
|
+
// THIS is the critical test - ensures new props propagate correctly
|
|
224
|
+
expect(span.textContent).toBe("150");
|
|
225
|
+
});
|
|
226
|
+
it("should handle multiple nested components receiving different updated props", async () => {
|
|
227
|
+
const container = document.createElement("div");
|
|
228
|
+
const ChildA = (props) => {
|
|
229
|
+
return () => jsx("div", { class: "child-a", children: props.value });
|
|
230
|
+
};
|
|
231
|
+
const ChildB = (props) => {
|
|
232
|
+
return () => jsx("div", { class: "child-b", children: props.value });
|
|
233
|
+
};
|
|
234
|
+
const ParentComponent = () => {
|
|
235
|
+
const state = createState({ valueA: "A1", valueB: "B1" });
|
|
236
|
+
return () => jsx("div", {
|
|
237
|
+
children: [
|
|
238
|
+
jsx(ChildA, { value: state.valueA }),
|
|
239
|
+
jsx(ChildB, { value: state.valueB }),
|
|
240
|
+
jsx("button", {
|
|
241
|
+
class: "btn-a",
|
|
242
|
+
onclick: () => (state.valueA = "A2"),
|
|
243
|
+
children: "Update A",
|
|
244
|
+
}),
|
|
245
|
+
jsx("button", {
|
|
246
|
+
class: "btn-b",
|
|
247
|
+
onclick: () => (state.valueB = "B2"),
|
|
248
|
+
children: "Update B",
|
|
249
|
+
}),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
render(jsx(ParentComponent, {}), container);
|
|
254
|
+
const childA = container.querySelector(".child-a");
|
|
255
|
+
const childB = container.querySelector(".child-b");
|
|
256
|
+
expect(childA.textContent).toBe("A1");
|
|
257
|
+
expect(childB.textContent).toBe("B1");
|
|
258
|
+
// Update only A
|
|
259
|
+
container.querySelector(".btn-a").click();
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
261
|
+
expect(childA.textContent).toBe("A2");
|
|
262
|
+
expect(childB.textContent).toBe("B1"); // B should remain unchanged
|
|
263
|
+
// Update only B
|
|
264
|
+
container.querySelector(".btn-b").click();
|
|
265
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
266
|
+
expect(childA.textContent).toBe("A2"); // A should remain unchanged
|
|
267
|
+
expect(childB.textContent).toBe("B2");
|
|
268
|
+
});
|
|
269
|
+
it("should pass array of props to nested components and update correctly", async () => {
|
|
270
|
+
const container = document.createElement("div");
|
|
271
|
+
const ItemComponent = (props) => {
|
|
272
|
+
return () => jsx("li", { children: `${props.index}: ${props.item}` });
|
|
273
|
+
};
|
|
274
|
+
const ListComponent = (props) => {
|
|
275
|
+
return () => jsx("ul", {
|
|
276
|
+
children: props.items.map((item, index) => jsx(ItemComponent, { item, index, key: index })),
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
const ParentComponent = () => {
|
|
280
|
+
const state = createState({ items: ["apple", "banana"] });
|
|
281
|
+
return () => jsx("div", {
|
|
282
|
+
children: [
|
|
283
|
+
jsx(ListComponent, { items: state.items }),
|
|
284
|
+
jsx("button", {
|
|
285
|
+
onclick: () => state.items.push("cherry"),
|
|
286
|
+
children: "Add",
|
|
287
|
+
}),
|
|
288
|
+
],
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
render(jsx(ParentComponent, {}), container);
|
|
292
|
+
const ul = container.querySelector("ul");
|
|
293
|
+
expect(ul.children.length).toBe(2);
|
|
294
|
+
expect(ul.children[0].textContent).toBe("0: apple");
|
|
295
|
+
expect(ul.children[1].textContent).toBe("1: banana");
|
|
296
|
+
// Add item
|
|
297
|
+
container.querySelector("button").click();
|
|
298
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
299
|
+
expect(ul.children.length).toBe(3);
|
|
300
|
+
expect(ul.children[2].textContent).toBe("2: cherry");
|
|
301
|
+
});
|
|
302
|
+
it("should maintain correct prop values when sibling components update", async () => {
|
|
303
|
+
const container = document.createElement("div");
|
|
304
|
+
const DisplayComponent = (props) => {
|
|
305
|
+
return () => jsx("div", {
|
|
306
|
+
class: props.label.toLowerCase().replace(/\s/g, "-"),
|
|
307
|
+
children: `${props.label}: ${props.value}`,
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const ParentComponent = () => {
|
|
311
|
+
const state = createState({ countA: 0, countB: 100 });
|
|
312
|
+
return () => jsx("div", {
|
|
313
|
+
children: [
|
|
314
|
+
jsx(DisplayComponent, { label: "Counter A", value: state.countA }),
|
|
315
|
+
jsx(DisplayComponent, { label: "Counter B", value: state.countB }),
|
|
316
|
+
jsx("button", {
|
|
317
|
+
class: "update-a",
|
|
318
|
+
onclick: () => state.countA++,
|
|
319
|
+
children: "Inc A",
|
|
320
|
+
}),
|
|
321
|
+
],
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
render(jsx(ParentComponent, {}), container);
|
|
325
|
+
const displayA = container.querySelector(".counter-a");
|
|
326
|
+
const displayB = container.querySelector(".counter-b");
|
|
327
|
+
// Update A
|
|
328
|
+
container.querySelector(".update-a").click();
|
|
329
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
330
|
+
// Ensure A updated and B kept its value (didn't get stale/wrong props)
|
|
331
|
+
expect(displayA?.textContent).toContain("1");
|
|
332
|
+
expect(displayB?.textContent).toContain("100");
|
|
87
333
|
});
|
|
88
334
|
});
|
|
@@ -11,7 +11,7 @@ describe("Component State", () => {
|
|
|
11
11
|
render(jsx(MyComponent, {}), container);
|
|
12
12
|
expect(container.children[0].textContent).toBe("0");
|
|
13
13
|
});
|
|
14
|
-
it("should update state when value changes", () => {
|
|
14
|
+
it("should update state when value changes", async () => {
|
|
15
15
|
const container = document.createElement("div");
|
|
16
16
|
let stateFn;
|
|
17
17
|
const MyComponent = () => {
|
|
@@ -20,9 +20,10 @@ describe("Component State", () => {
|
|
|
20
20
|
return () => jsx("div", { children: String(state.count) });
|
|
21
21
|
};
|
|
22
22
|
render(jsx(MyComponent, {}), container);
|
|
23
|
+
expect(container.children[0].textContent).toBe("0");
|
|
23
24
|
stateFn.count = 5;
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
26
|
+
expect(container.children[0].textContent).toBe("5");
|
|
26
27
|
});
|
|
27
28
|
it("should support multiple state values", () => {
|
|
28
29
|
const container = document.createElement("div");
|
|
@@ -40,7 +41,7 @@ describe("Component State", () => {
|
|
|
40
41
|
expect(div.children[0].textContent).toBe("0");
|
|
41
42
|
expect(div.children[1].textContent).toBe("John");
|
|
42
43
|
});
|
|
43
|
-
it("should support incremental state updates", () => {
|
|
44
|
+
it("should support incremental state updates", async () => {
|
|
44
45
|
const container = document.createElement("div");
|
|
45
46
|
let stateFn;
|
|
46
47
|
const MyComponent = () => {
|
|
@@ -49,11 +50,12 @@ describe("Component State", () => {
|
|
|
49
50
|
return () => jsx("div", { children: String(state.count) });
|
|
50
51
|
};
|
|
51
52
|
render(jsx(MyComponent, {}), container);
|
|
53
|
+
expect(container.children[0].textContent).toBe("0");
|
|
52
54
|
stateFn.count = stateFn.count + 1;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
56
|
+
expect(container.children[0].textContent).toBe("1");
|
|
55
57
|
});
|
|
56
|
-
it("should preserve state between re-renders", () => {
|
|
58
|
+
it("should preserve state between re-renders", async () => {
|
|
57
59
|
const container = document.createElement("div");
|
|
58
60
|
let stateFn;
|
|
59
61
|
const MyComponent = () => {
|
|
@@ -62,11 +64,12 @@ describe("Component State", () => {
|
|
|
62
64
|
return () => jsx("div", { children: String(state.count) });
|
|
63
65
|
};
|
|
64
66
|
render(jsx(MyComponent, {}), container);
|
|
67
|
+
expect(container.children[0].textContent).toBe("0");
|
|
65
68
|
stateFn.count = 1;
|
|
66
69
|
stateFn.count = 2;
|
|
67
70
|
stateFn.count = 3;
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
72
|
+
expect(container.children[0].textContent).toBe("3");
|
|
70
73
|
});
|
|
71
74
|
it("should support nested objects as state", () => {
|
|
72
75
|
const container = document.createElement("div");
|
|
@@ -94,7 +97,7 @@ describe("Component State", () => {
|
|
|
94
97
|
expect(ul.children[1].textContent).toBe("2");
|
|
95
98
|
expect(ul.children[2].textContent).toBe("3");
|
|
96
99
|
});
|
|
97
|
-
it("should update nested state properties", () => {
|
|
100
|
+
it("should update nested state properties", async () => {
|
|
98
101
|
const container = document.createElement("div");
|
|
99
102
|
let stateFn;
|
|
100
103
|
const MyComponent = () => {
|
|
@@ -108,10 +111,10 @@ describe("Component State", () => {
|
|
|
108
111
|
expect(container.children[0].textContent).toBe("Alice is 25");
|
|
109
112
|
stateFn.user.name = "Bob";
|
|
110
113
|
stateFn.user.age = 30;
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
115
|
+
expect(container.children[0].textContent).toBe("Bob is 30");
|
|
113
116
|
});
|
|
114
|
-
it("should support array mutations", () => {
|
|
117
|
+
it("should support array mutations", async () => {
|
|
115
118
|
const container = document.createElement("div");
|
|
116
119
|
let stateFn;
|
|
117
120
|
const MyComponent = () => {
|
|
@@ -122,8 +125,11 @@ describe("Component State", () => {
|
|
|
122
125
|
});
|
|
123
126
|
};
|
|
124
127
|
render(jsx(MyComponent, {}), container);
|
|
128
|
+
const ul = container.children[0];
|
|
129
|
+
expect(ul.children).toHaveLength(3);
|
|
125
130
|
stateFn.items.push(4);
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
132
|
+
expect(ul.children).toHaveLength(4);
|
|
133
|
+
expect(ul.children[3].textContent).toBe("4");
|
|
128
134
|
});
|
|
129
135
|
});
|
|
@@ -27,7 +27,7 @@ describe("createState", () => {
|
|
|
27
27
|
state.items.push(4);
|
|
28
28
|
expect(state.items).toEqual([1, 2, 3, 4]);
|
|
29
29
|
});
|
|
30
|
-
it("should track property access in observers", () => {
|
|
30
|
+
it("should track property access in observers", async () => {
|
|
31
31
|
const state = createState({ count: 0 });
|
|
32
32
|
let renderCount = 0;
|
|
33
33
|
const observer = new Observer(() => {
|
|
@@ -42,13 +42,9 @@ describe("createState", () => {
|
|
|
42
42
|
const value = state.count; // Track
|
|
43
43
|
dispose2(); // Stop observing, subscriptions are now active
|
|
44
44
|
state.count = 1;
|
|
45
|
-
// Wait for
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
expect(renderCount).toBeGreaterThan(0);
|
|
49
|
-
resolve(undefined);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
45
|
+
// Wait for microtasks to complete
|
|
46
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
47
|
+
expect(renderCount).toBeGreaterThan(0);
|
|
52
48
|
});
|
|
53
49
|
it("should handle property deletion", () => {
|
|
54
50
|
const state = createState({ count: 0, temp: "value" });
|
|
@@ -67,7 +63,7 @@ describe("createState", () => {
|
|
|
67
63
|
const state = createState({ [sym]: "value" });
|
|
68
64
|
expect(state[sym]).toBe("value");
|
|
69
65
|
});
|
|
70
|
-
it("should notify observers only on actual changes", () => {
|
|
66
|
+
it("should notify observers only on actual changes", async () => {
|
|
71
67
|
const state = createState({ count: 0 });
|
|
72
68
|
let notifyCount = 0;
|
|
73
69
|
const observer = new Observer(() => {
|
|
@@ -78,13 +74,9 @@ describe("createState", () => {
|
|
|
78
74
|
dispose();
|
|
79
75
|
state.count = 0; // Same value - should still notify per current implementation
|
|
80
76
|
state.count = 0;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
observer.dispose();
|
|
85
|
-
resolve(undefined);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
77
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
78
|
+
// The implementation notifies even for same value, except for optimization cases
|
|
79
|
+
observer.dispose();
|
|
88
80
|
});
|
|
89
81
|
it("should handle deeply nested objects", () => {
|
|
90
82
|
const state = createState({
|
|
@@ -39,26 +39,6 @@ describe("Signal", () => {
|
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
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
42
|
it("should track signals during observation", () => {
|
|
63
43
|
const callback = vi.fn();
|
|
64
44
|
const observer = new Observer(callback);
|
|
@@ -130,21 +110,4 @@ describe("Observer", () => {
|
|
|
130
110
|
dispose1();
|
|
131
111
|
expect(getCurrentObserver()).toBeUndefined();
|
|
132
112
|
});
|
|
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);
|
|
149
|
-
});
|
|
150
113
|
});
|
|
@@ -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;
|
|
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"}
|
|
@@ -78,28 +78,38 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
78
78
|
}
|
|
79
79
|
return normalizeChildren(renderResult);
|
|
80
80
|
};
|
|
81
|
+
let isObserverQueued = false;
|
|
81
82
|
const instance = (this.instance = {
|
|
82
83
|
parent,
|
|
83
84
|
contexts: null,
|
|
84
85
|
onCleanups: [],
|
|
85
86
|
onMounts: [],
|
|
86
87
|
observer: new Observer(() => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const prevChildren = this.children;
|
|
90
|
-
this.children = this.patchChildren(newChildren);
|
|
91
|
-
// Typically components return a single element, which does
|
|
92
|
-
// not require the parent to apply elements to the DOM again
|
|
93
|
-
const canSelfUpdate = prevChildren.length === 1 &&
|
|
94
|
-
this.children.length === 1 &&
|
|
95
|
-
prevChildren[0] instanceof ElementVNode &&
|
|
96
|
-
this.children[0] instanceof ElementVNode &&
|
|
97
|
-
this.canPatch(prevChildren[0], this.children[0]);
|
|
98
|
-
if (!canSelfUpdate) {
|
|
99
|
-
this.parent?.rerender();
|
|
88
|
+
if (isObserverQueued) {
|
|
89
|
+
return;
|
|
100
90
|
}
|
|
101
|
-
|
|
102
|
-
this.root?.
|
|
91
|
+
isObserverQueued = true;
|
|
92
|
+
this.root?.queueObserver(() => {
|
|
93
|
+
if (instance.observer.isDisposed) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
isObserverQueued = false;
|
|
97
|
+
this.root?.setAsCurrent();
|
|
98
|
+
const newChildren = executeRender();
|
|
99
|
+
const prevChildren = this.children;
|
|
100
|
+
this.children = this.patchChildren(newChildren);
|
|
101
|
+
// Typically components return a single element, which does
|
|
102
|
+
// not require the parent to apply elements to the DOM again
|
|
103
|
+
const canSelfUpdate = prevChildren.length === 1 &&
|
|
104
|
+
this.children.length === 1 &&
|
|
105
|
+
prevChildren[0] instanceof ElementVNode &&
|
|
106
|
+
this.children[0] instanceof ElementVNode &&
|
|
107
|
+
this.canPatch(prevChildren[0], this.children[0]);
|
|
108
|
+
if (!canSelfUpdate) {
|
|
109
|
+
this.parent?.rerender();
|
|
110
|
+
}
|
|
111
|
+
this.root?.clearCurrent();
|
|
112
|
+
});
|
|
103
113
|
}),
|
|
104
114
|
reactiveProps: createReactiveProps(this.props),
|
|
105
115
|
get error() {
|
|
@@ -164,7 +174,7 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
164
174
|
this.root?.setAsCurrent();
|
|
165
175
|
this.root?.pushComponent(this.instance);
|
|
166
176
|
for (const prop in newNode.props) {
|
|
167
|
-
this.instance.reactiveProps[prop] =
|
|
177
|
+
this.instance.reactiveProps[prop] = newNode.props[prop];
|
|
168
178
|
}
|
|
169
179
|
this.root?.popComponent();
|
|
170
180
|
this.root?.clearCurrent();
|