rask-ui 0.2.0 → 0.2.1
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/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/vdom/ComponentVNode.d.ts.map +1 -1
- package/dist/vdom/ComponentVNode.js +1 -1
- 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/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({
|
|
@@ -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;IAyH7B,KAAK,CAAC,OAAO,EAAE,cAAc;IAW7B,OAAO;CAcR"}
|
|
@@ -164,7 +164,7 @@ export class ComponentVNode extends AbstractVNode {
|
|
|
164
164
|
this.root?.setAsCurrent();
|
|
165
165
|
this.root?.pushComponent(this.instance);
|
|
166
166
|
for (const prop in newNode.props) {
|
|
167
|
-
this.instance.reactiveProps[prop] =
|
|
167
|
+
this.instance.reactiveProps[prop] = newNode.props[prop];
|
|
168
168
|
}
|
|
169
169
|
this.root?.popComponent();
|
|
170
170
|
this.root?.clearCurrent();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom-utils.d.ts","sourceRoot":"","sources":["../../src/vdom/dom-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,IAAI,GAAG,IAAI,EAAE,QAO3B;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,CAc3D;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,QAQtE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,QAE3E;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,IAAI,QAOrB;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,QAe/C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"dom-utils.d.ts","sourceRoot":"","sources":["../../src/vdom/dom-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,IAAI,GAAG,IAAI,EAAE,QAO3B;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI,EAAE,GAAG,IAAI,CAc3D;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,QAQtE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,QAE3E;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,IAAI,QAOrB;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,QAe/C;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,WAAW,EAChB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,QA0B3D"}
|
package/dist/vdom/dom-utils.js
CHANGED
|
@@ -59,16 +59,26 @@ export function isEventProp(name) {
|
|
|
59
59
|
}
|
|
60
60
|
export function setElementClass(elm, value) {
|
|
61
61
|
if (value === null || value === undefined) {
|
|
62
|
-
elm.
|
|
62
|
+
elm.removeAttribute("class");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
if (typeof value === "string") {
|
|
66
|
-
|
|
66
|
+
if (value === "") {
|
|
67
|
+
elm.removeAttribute("class");
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
elm.className = value;
|
|
71
|
+
}
|
|
67
72
|
return;
|
|
68
73
|
}
|
|
69
74
|
// Handle object notation: { "class-name": true, "other-class": false }
|
|
70
75
|
const classes = Object.keys(value)
|
|
71
76
|
.filter((key) => value[key])
|
|
72
77
|
.join(" ");
|
|
73
|
-
|
|
78
|
+
if (classes === "") {
|
|
79
|
+
elm.removeAttribute("class");
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
elm.className = classes;
|
|
83
|
+
}
|
|
74
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rask-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -19,10 +19,11 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
|
-
"dist"
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|
|
25
|
-
"build": "tsc && cp src/jsx.d.ts dist/",
|
|
26
|
+
"build": "tsc && cp src/jsx.d.ts dist/ && cp ../../README.md .",
|
|
26
27
|
"dev": "tsc --watch",
|
|
27
28
|
"test": "vitest",
|
|
28
29
|
"test:ui": "vitest --ui",
|