@daz4126/helium 0.7.2 → 0.8.0
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 +5 -4
- package/helium.js +128 -290
- package/helium.test.js +441 -0
- package/package.json +6 -1
- package/vitest.config.js +8 -0
package/README.md
CHANGED
|
@@ -14,10 +14,10 @@ It's really simple to use - just sprinkle the magic @attributes into your HTML a
|
|
|
14
14
|
|
|
15
15
|
[See more examples here](https://codepen.io/daz4126/pen/YPwwdBK)
|
|
16
16
|
|
|
17
|
-
To use, just import from the CDN (no install or build step required!):
|
|
17
|
+
To use, just import from the CDN in a script tag directly in your HTML page (no install or build step required!):
|
|
18
18
|
|
|
19
|
-
```
|
|
20
|
-
import helium from
|
|
19
|
+
```html
|
|
20
|
+
<script type="module">import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';helium();</script>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Alernatively you can install from NPM:
|
|
@@ -26,10 +26,11 @@ Alernatively you can install from NPM:
|
|
|
26
26
|
npm install @daz4126/helium
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Then include it in your
|
|
29
|
+
Then include it in your JavaScript file and call the `helium()` function:
|
|
30
30
|
|
|
31
31
|
```javascript
|
|
32
32
|
import helium from "@daz4126/helium"
|
|
33
|
+
helium()
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
# Helium Attributes
|
package/helium.js
CHANGED
|
@@ -1,82 +1,77 @@
|
|
|
1
|
-
let ue = true;
|
|
2
1
|
export default function helium(data = {}) {
|
|
3
|
-
const root =
|
|
4
|
-
document.querySelector("[\\@helium]") ||
|
|
5
|
-
document.querySelector("[data-helium]") ||
|
|
6
|
-
document.body;
|
|
7
|
-
if (Object.keys(data).length > 0)
|
|
8
|
-
document
|
|
9
|
-
.querySelectorAll("[data-helium-processed]")
|
|
10
|
-
.forEach((el) => el.removeAttribute("data-helium-processed"));
|
|
2
|
+
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
11
3
|
const [bindings, refs] = [new Map(), new Map()];
|
|
12
|
-
const $ = (
|
|
13
|
-
const html = (
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
return
|
|
4
|
+
const $ = (s) => document.querySelector(s);
|
|
5
|
+
const html = (s) => {
|
|
6
|
+
const t = document.createElement("template");
|
|
7
|
+
t.innerHTML = s;
|
|
8
|
+
return t.content.firstChild;
|
|
17
9
|
};
|
|
18
10
|
|
|
19
|
-
const ajax = (
|
|
11
|
+
const ajax = (url, method, target, opts = {}) => {
|
|
20
12
|
fetch(url, {
|
|
21
13
|
method,
|
|
22
|
-
headers: {
|
|
23
|
-
|
|
14
|
+
headers: {
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
Accept: "text/vnd.turbo-stream.html, application/json, application/vnd.api+json, text/html, */*",
|
|
17
|
+
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content || ""
|
|
18
|
+
},
|
|
19
|
+
body: method === "GET" ? null : JSON.stringify(opts),
|
|
20
|
+
credentials: "same-origin"
|
|
24
21
|
})
|
|
25
|
-
.then(
|
|
26
|
-
|
|
27
|
-
res.headers.get("content-type")?.includes("json") ? "json" : "text"
|
|
28
|
-
](),
|
|
22
|
+
.then(r =>
|
|
23
|
+
r.headers.get("content-type")?.includes("application/json") ? r.json() : r.text()
|
|
29
24
|
)
|
|
30
|
-
.then(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const get = ax("GET");
|
|
35
|
-
const post = ax("POST");
|
|
36
|
-
const put = ax("PUT");
|
|
37
|
-
const patch = ax("PATCH");
|
|
38
|
-
const del = ax("DELETE");
|
|
25
|
+
.then(d => state[target] = d)
|
|
26
|
+
.catch(e => console.error("AJAX error:", e.message));
|
|
27
|
+
};
|
|
39
28
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
29
|
+
const get = (u,t) => ajax(u,"GET",t);
|
|
30
|
+
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map((m) => (u, d, t) => ajax(u, m, t, d));
|
|
31
|
+
|
|
32
|
+
const handler = {
|
|
33
|
+
get(target, prop, receiver) {
|
|
34
|
+
const val = Reflect.get(target, prop, receiver);
|
|
35
|
+
return typeof val === "object" && val !== null ? new Proxy(val, handler) : val;
|
|
43
36
|
},
|
|
44
|
-
set(target, prop,
|
|
45
|
-
|
|
46
|
-
if (target
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
37
|
+
set(target, prop, val, receiver) {
|
|
38
|
+
const result = Reflect.set(target, prop, val, receiver);
|
|
39
|
+
if (target === data && bindings.has(prop)) {
|
|
40
|
+
bindings.get(prop).forEach(applyBinding);
|
|
41
|
+
} else {
|
|
42
|
+
for (const [key, bound] of bindings.entries()) {
|
|
43
|
+
if (key === Object.keys(data).find(k => data[k] === target)) {
|
|
44
|
+
bound.forEach(applyBinding);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
51
47
|
}
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
55
51
|
|
|
52
|
+
const state = new Proxy(data, handler);
|
|
56
53
|
let isUpdatingDOM = false;
|
|
57
54
|
|
|
58
55
|
function applyBinding(binding, event = {}, elCtx = binding.el) {
|
|
59
56
|
const { el, prop, fn } = binding;
|
|
60
|
-
const result = fn(
|
|
61
|
-
|
|
62
|
-
state,
|
|
63
|
-
event,
|
|
64
|
-
elCtx,
|
|
65
|
-
html,
|
|
66
|
-
get,
|
|
67
|
-
post,
|
|
68
|
-
put,
|
|
69
|
-
patch,
|
|
70
|
-
del,
|
|
71
|
-
...Object.values(data),
|
|
72
|
-
...[...refs.values()],
|
|
73
|
-
);
|
|
74
|
-
// guard to reduce observer recursion (optional but helpful)
|
|
75
|
-
if (prop === "innerHTML") {
|
|
57
|
+
const result = fn($, state, event, elCtx, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
58
|
+
if (prop == "innerHTML") {
|
|
76
59
|
isUpdatingDOM = true;
|
|
77
60
|
el.innerHTML = result;
|
|
78
61
|
isUpdatingDOM = false;
|
|
79
|
-
} else if
|
|
62
|
+
} else if(prop == "class" && typeof(result) == "object"){
|
|
63
|
+
const add = [];
|
|
64
|
+
const remove = [];
|
|
65
|
+
|
|
66
|
+
for (const [key, value] of Object.entries(result)) {
|
|
67
|
+
(value ? add : remove).push(key);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
el.classList.add(...add);
|
|
71
|
+
el.classList.remove(...remove);
|
|
72
|
+
} else if(prop == "style" && typeof(result) == "object"){
|
|
73
|
+
el.style = Object.entries(result).map(([key, value]) => value ? `${key}: ${value};` : "").join``
|
|
74
|
+
} else if (prop in el) {
|
|
80
75
|
el[prop] = result;
|
|
81
76
|
} else {
|
|
82
77
|
el.setAttribute(prop, result);
|
|
@@ -85,286 +80,132 @@ export default function helium(data = {}) {
|
|
|
85
80
|
|
|
86
81
|
function compileExpression(expr, withReturn = false) {
|
|
87
82
|
try {
|
|
88
|
-
return new Function(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"$event",
|
|
92
|
-
"$el",
|
|
93
|
-
"$html",
|
|
94
|
-
"$get",
|
|
95
|
-
"$post",
|
|
96
|
-
"$put",
|
|
97
|
-
"$patch",
|
|
98
|
-
"$delete",
|
|
99
|
-
...Object.keys(data),
|
|
100
|
-
...[...refs.keys()],
|
|
101
|
-
`with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
|
|
102
|
-
);
|
|
83
|
+
return new Function("$", "$data", "$event", "$el", "$html", "$get", "$post", "$put", "$patch", "$delete",
|
|
84
|
+
...Object.keys(data), ...[...refs.keys()],
|
|
85
|
+
`with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`);
|
|
103
86
|
} catch (err) {
|
|
104
87
|
return () => expr;
|
|
105
88
|
}
|
|
106
89
|
}
|
|
107
90
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
91
|
+
function processElements(element) {
|
|
92
|
+
const newBindings = [];
|
|
93
|
+
if (element.nodeType === 1 && element.hasAttribute?.("data-helium-processed")) return newBindings;
|
|
94
|
+
if (element.nodeType === 1 && element.setAttribute) element.setAttribute("data-helium-processed", "");
|
|
111
95
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
element.nodeType === 1 &&
|
|
115
|
-
element.hasAttribute &&
|
|
116
|
-
element.hasAttribute("data-helium-processed")
|
|
117
|
-
)
|
|
118
|
-
return newlyAddedBindings;
|
|
119
|
-
if (element.nodeType === 1 && element.setAttribute)
|
|
120
|
-
element.setAttribute("data-helium-processed", "");
|
|
96
|
+
const heElements = [element, ...Array.from(element.querySelectorAll("*")).filter((el) =>
|
|
97
|
+
Array.from(el.attributes).some((attr) => attr.name.startsWith("data-he-")))];
|
|
121
98
|
|
|
122
|
-
const
|
|
123
|
-
element,
|
|
124
|
-
|
|
125
|
-
Array.from(el.attributes).some((attr) =>
|
|
126
|
-
attr.name.startsWith("data-he-"),
|
|
127
|
-
),
|
|
128
|
-
),
|
|
129
|
-
];
|
|
99
|
+
const xpath = document.evaluate(".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
|
|
100
|
+
element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
101
|
+
for (let i = 0; i < xpath.snapshotLength; i++) heElements.push(xpath.snapshotItem(i));
|
|
130
102
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
element,
|
|
134
|
-
null,
|
|
135
|
-
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
136
|
-
null,
|
|
137
|
-
);
|
|
138
|
-
for (let i = 0; i < xpath.snapshotLength; i++)
|
|
139
|
-
heliumElements.push(xpath.snapshotItem(i));
|
|
140
|
-
|
|
141
|
-
// seed default state values (no binding creation here)
|
|
142
|
-
heliumElements.forEach((el) => {
|
|
103
|
+
// Seed default state values
|
|
104
|
+
heElements.forEach((el) => {
|
|
143
105
|
for (const { name, value } of el.attributes || []) {
|
|
144
|
-
if (
|
|
145
|
-
name == "@html" ||
|
|
146
|
-
name == "data-he-html" ||
|
|
147
|
-
name == "@text" ||
|
|
148
|
-
name == "data-he-text" ||
|
|
149
|
-
name == "@bind" ||
|
|
150
|
-
name == "data-he-bind"
|
|
151
|
-
) {
|
|
106
|
+
if (["@html","data-he-html","@text","data-he-text","@bind","data-he-bind"].includes(name)) {
|
|
152
107
|
try {
|
|
153
108
|
new Function(`let ${value} = 1`);
|
|
154
|
-
state[value] ||=
|
|
155
|
-
name == "@bind" || name == "data-he-bind"
|
|
156
|
-
? el.value
|
|
157
|
-
: el.textContent;
|
|
109
|
+
state[value] ||= ["@bind","data-he-bind"].includes(name) ? el.value : el.textContent;
|
|
158
110
|
} catch (e) {}
|
|
159
111
|
}
|
|
160
112
|
}
|
|
161
113
|
});
|
|
162
114
|
|
|
163
|
-
// Register bindings for these elements and collect the newly added binding objects
|
|
164
|
-
heliumElements.forEach((el) => {
|
|
165
|
-
for (const { name, value } of el.attributes || []) {
|
|
166
|
-
if (name == "@data" || name == "data-he") {
|
|
167
|
-
Object.assign(
|
|
168
|
-
state,
|
|
169
|
-
compileExpression(value, true)(
|
|
170
|
-
$,
|
|
171
|
-
state,
|
|
172
|
-
{},
|
|
173
|
-
el,
|
|
174
|
-
html,
|
|
175
|
-
get,
|
|
176
|
-
post,
|
|
177
|
-
put,
|
|
178
|
-
patch,
|
|
179
|
-
del,
|
|
180
|
-
...Object.values(data),
|
|
181
|
-
...[...refs.values()],
|
|
182
|
-
),
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
|
|
186
115
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
? "textContent"
|
|
201
|
-
: "innerHTML",
|
|
202
|
-
expr: value,
|
|
203
|
-
fn: compileExpression(value, true),
|
|
204
|
-
};
|
|
205
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
206
|
-
newlyAddedBindings.push(b);
|
|
207
|
-
});
|
|
116
|
+
// Register bindings
|
|
117
|
+
heElements.forEach((el) => {
|
|
118
|
+
const execFn = (v) => compileExpression(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
119
|
+
for (const { name, value } of el.attributes || []) {
|
|
120
|
+
if (["@data","data-he"].includes(name)) Object.assign(state, execFn(value));
|
|
121
|
+
if (["@ref","data-he-ref"].includes(name)) refs.set("$" + value, el);
|
|
122
|
+
|
|
123
|
+
if (["@text","data-he-text","@html","data-he-html"].includes(name)) {
|
|
124
|
+
Object.keys(state).filter((k) => value.includes(k)).forEach((val) => {
|
|
125
|
+
const b = { el, prop: ["@text","data-he-text"].includes(name) ? "textContent" : "innerHTML", expr: value, fn: compileExpression(value, true) };
|
|
126
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
127
|
+
newBindings.push(b);
|
|
128
|
+
});
|
|
208
129
|
}
|
|
209
130
|
|
|
210
|
-
if (
|
|
131
|
+
if (["@bind","data-he-bind"].includes(name)) {
|
|
211
132
|
el.addEventListener("input", (e) => (state[value] = e.target.value));
|
|
212
|
-
const b = {
|
|
213
|
-
el,
|
|
214
|
-
prop: "value",
|
|
215
|
-
expr: value,
|
|
216
|
-
fn: compileExpression(value, true),
|
|
217
|
-
};
|
|
133
|
+
const b = { el, prop: "value", expr: value, fn: compileExpression(value, true) };
|
|
218
134
|
bindings.set(value, [...(bindings.get(value) || []), b]);
|
|
219
|
-
|
|
135
|
+
newBindings.push(b);
|
|
220
136
|
el.value = state[value];
|
|
221
137
|
}
|
|
222
138
|
|
|
223
|
-
if (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Object.keys(state)
|
|
230
|
-
.filter((key) => value.includes(key))
|
|
231
|
-
.forEach((val) => {
|
|
232
|
-
const b = {
|
|
233
|
-
el,
|
|
234
|
-
prop: "hidden",
|
|
235
|
-
expr: value,
|
|
236
|
-
fn: compileExpression(
|
|
237
|
-
`${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
|
|
238
|
-
true,
|
|
239
|
-
),
|
|
240
|
-
};
|
|
241
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
242
|
-
newlyAddedBindings.push(b);
|
|
243
|
-
});
|
|
139
|
+
if (["@hidden","@visible","data-he-hidden","data-he-visible"].includes(name)) {
|
|
140
|
+
Object.keys(state).filter((k) => value.includes(k)).forEach((val) => {
|
|
141
|
+
const b = { el, prop: "hidden", expr: value, fn: compileExpression(`${["@hidden","data-he-hidden"].includes(name) ? "!" : ""}!(${value})`, true) };
|
|
142
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
143
|
+
newBindings.push(b);
|
|
144
|
+
});
|
|
244
145
|
}
|
|
245
146
|
|
|
246
147
|
if (name.startsWith(":")) {
|
|
247
|
-
Object.keys(state)
|
|
248
|
-
.
|
|
249
|
-
.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
prop: name.split(":")[1],
|
|
253
|
-
expr: value,
|
|
254
|
-
fn: compileExpression(value, true),
|
|
255
|
-
};
|
|
256
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
257
|
-
newlyAddedBindings.push(b);
|
|
258
|
-
});
|
|
148
|
+
Object.keys(state).filter((k) => value.includes(k)).forEach((val) => {
|
|
149
|
+
const b = { el, prop: name.split(":")[1], expr: value, fn: compileExpression(value, true) };
|
|
150
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
151
|
+
newBindings.push(b);
|
|
152
|
+
});
|
|
259
153
|
}
|
|
260
154
|
|
|
261
|
-
if (
|
|
262
|
-
compileExpression(value, false)(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
el,
|
|
267
|
-
html,
|
|
268
|
-
get,
|
|
269
|
-
post,
|
|
270
|
-
put,
|
|
271
|
-
patch,
|
|
272
|
-
del,
|
|
273
|
-
...Object.values(data),
|
|
274
|
-
...[...refs.values()],
|
|
275
|
-
);
|
|
276
|
-
} else if (
|
|
277
|
-
(name.startsWith("@") || name.startsWith("data-he-on")) &&
|
|
278
|
-
addEvents
|
|
279
|
-
) {
|
|
280
|
-
// event handling — keep this as before (register listeners)
|
|
281
|
-
const [eventName, ...modifiers] = name
|
|
282
|
-
.slice(name.startsWith("@") ? 1 : 10)
|
|
283
|
-
.split(".");
|
|
284
|
-
const receiver =
|
|
285
|
-
modifiers.includes("outside") || modifiers.includes("document")
|
|
286
|
-
? document
|
|
287
|
-
: el;
|
|
155
|
+
if (["@init","data-he-init"].includes(name)) {
|
|
156
|
+
compileExpression(value, false)($, state, undefined, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
157
|
+
} else if (name.startsWith("@") || name.startsWith("data-he-on")) {
|
|
158
|
+
const [eventName, ...mods] = name.slice(name.startsWith("@") ? 1 : 10).split(".");
|
|
159
|
+
const receiver = mods.includes("outside") || mods.includes("document") ? document : el;
|
|
288
160
|
const debounce = (fn, delay) => {
|
|
289
161
|
let timeout;
|
|
290
|
-
return
|
|
162
|
+
return (...args) => {
|
|
291
163
|
clearTimeout(timeout);
|
|
292
|
-
timeout = setTimeout(() => fn
|
|
164
|
+
timeout = setTimeout(() => fn(...args), delay);
|
|
293
165
|
};
|
|
294
166
|
};
|
|
295
167
|
|
|
296
168
|
let debounceDelay = 0;
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const time = debounceModifier.split(":")[1];
|
|
302
|
-
if (time && !isNaN(time)) debounceDelay = Number(time);
|
|
303
|
-
else debounceDelay = 300;
|
|
169
|
+
const debounceMod = mods.find((m) => m.startsWith("debounce"));
|
|
170
|
+
if (debounceMod) {
|
|
171
|
+
const time = debounceMod.split(":")[1];
|
|
172
|
+
debounceDelay = time && !isNaN(time) ? Number(time) : 300;
|
|
304
173
|
}
|
|
305
174
|
|
|
306
175
|
function _handler(e) {
|
|
307
|
-
if (
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
alt: "altKey",
|
|
312
|
-
meta: "metaKey",
|
|
313
|
-
};
|
|
314
|
-
for (const [mod, prop] of Object.entries(keyModifiers)) {
|
|
315
|
-
if (modifiers.includes(mod) && !e[prop]) return;
|
|
176
|
+
if (mods.includes("prevent")) e.preventDefault();
|
|
177
|
+
const keyMods = { shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey" };
|
|
178
|
+
for (const [mod, prop] of Object.entries(keyMods)) {
|
|
179
|
+
if (mods.includes(mod) && !e[prop]) return;
|
|
316
180
|
}
|
|
317
|
-
if (["keydown",
|
|
318
|
-
const last =
|
|
181
|
+
if (["keydown","keyup","keypress"].includes(eventName)) {
|
|
182
|
+
const last = mods[mods.length - 1];
|
|
319
183
|
if (last) {
|
|
320
|
-
let keyName = e.key;
|
|
321
|
-
if (keyName === " ") keyName = "Space";
|
|
322
|
-
else if (keyName === "Escape") keyName = "Esc";
|
|
184
|
+
let keyName = e.key === " " ? "Space" : e.key === "Escape" ? "Esc" : e.key;
|
|
323
185
|
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
324
186
|
}
|
|
325
187
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
compileExpression(value, false)(
|
|
329
|
-
$,
|
|
330
|
-
state,
|
|
331
|
-
e,
|
|
332
|
-
el,
|
|
333
|
-
html,
|
|
334
|
-
get,
|
|
335
|
-
post,
|
|
336
|
-
put,
|
|
337
|
-
patch,
|
|
338
|
-
del,
|
|
339
|
-
...Object.values(data),
|
|
340
|
-
...[...refs.values()],
|
|
341
|
-
);
|
|
188
|
+
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
189
|
+
compileExpression(value, false)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
|
|
342
190
|
}
|
|
343
|
-
if (
|
|
344
|
-
el.removeEventListener(eventName, debouncedHandler);
|
|
191
|
+
if (mods.includes("once")) el.removeEventListener(eventName, debouncedHandler);
|
|
345
192
|
}
|
|
346
193
|
|
|
347
|
-
const debouncedHandler =
|
|
348
|
-
debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
194
|
+
const debouncedHandler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
349
195
|
receiver.addEventListener(eventName, debouncedHandler);
|
|
350
196
|
}
|
|
351
|
-
}
|
|
352
|
-
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
353
199
|
|
|
354
|
-
return
|
|
355
|
-
}
|
|
200
|
+
return newBindings;
|
|
201
|
+
}
|
|
356
202
|
|
|
357
|
-
// set up mutation observer that only processes and applies bindings for newly added nodes
|
|
358
203
|
const observer = new MutationObserver((mutations) => {
|
|
359
|
-
if (isUpdatingDOM) return;
|
|
204
|
+
if (isUpdatingDOM) return;
|
|
360
205
|
for (const m of mutations) {
|
|
361
206
|
for (const node of m.addedNodes) {
|
|
362
|
-
if (
|
|
363
|
-
|
|
364
|
-
!node.hasAttribute("data-helium-processed")
|
|
365
|
-
) {
|
|
366
|
-
const newly = processElements(node, false);
|
|
367
|
-
// apply only the bindings we just created for this node
|
|
207
|
+
if (node.nodeType === 1 && !node.hasAttribute("data-helium-processed")) {
|
|
208
|
+
const newly = processElements(node);
|
|
368
209
|
newly.forEach(applyBinding);
|
|
369
210
|
}
|
|
370
211
|
}
|
|
@@ -372,9 +213,6 @@ export default function helium(data = {}) {
|
|
|
372
213
|
});
|
|
373
214
|
observer.observe(root, { childList: true, subtree: true });
|
|
374
215
|
|
|
375
|
-
|
|
216
|
+
processElements(root);
|
|
376
217
|
for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
|
|
377
|
-
ue = false;
|
|
378
218
|
}
|
|
379
|
-
|
|
380
|
-
helium();
|
package/helium.test.js
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import helium from './helium.js';
|
|
3
|
+
|
|
4
|
+
describe('Helium Reactive Library', () => {
|
|
5
|
+
let container;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
container = document.createElement('div');
|
|
9
|
+
container.setAttribute('data-helium', '');
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
document.body.innerHTML = '';
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('State Reactivity', () => {
|
|
18
|
+
it('should initialize with default data', () => {
|
|
19
|
+
container.innerHTML = '<div @text="count"></div>';
|
|
20
|
+
helium({ count: 5 });
|
|
21
|
+
expect(container.querySelector('div').textContent).toBe('5');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should react to direct property assignment', (done) => {
|
|
25
|
+
container.innerHTML = '<div @text="count"></div>';
|
|
26
|
+
const state = helium({ count: 0 });
|
|
27
|
+
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
state.count = 10;
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
expect(container.querySelector('div').textContent).toBe('10');
|
|
32
|
+
done();
|
|
33
|
+
}, 0);
|
|
34
|
+
}, 0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should react to nested object mutations', (done) => {
|
|
38
|
+
container.innerHTML = '<div @text="user.name"></div>';
|
|
39
|
+
const state = helium({ user: { name: 'Alice' } });
|
|
40
|
+
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
state.user.name = 'Bob';
|
|
43
|
+
setTimeout(() => {
|
|
44
|
+
expect(container.querySelector('div').textContent).toBe('Bob');
|
|
45
|
+
done();
|
|
46
|
+
}, 0);
|
|
47
|
+
}, 0);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should react to array push', (done) => {
|
|
51
|
+
container.innerHTML = '<div @text="items.length"></div>';
|
|
52
|
+
const state = helium({ items: [1, 2, 3] });
|
|
53
|
+
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
state.items.push(4);
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
expect(container.querySelector('div').textContent).toBe('4');
|
|
58
|
+
done();
|
|
59
|
+
}, 0);
|
|
60
|
+
}, 0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should react to array index assignment', (done) => {
|
|
64
|
+
container.innerHTML = '<div @text="items[0]"></div>';
|
|
65
|
+
const state = helium({ items: ['a', 'b', 'c'] });
|
|
66
|
+
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
state.items[0] = 'z';
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
expect(container.querySelector('div').textContent).toBe('z');
|
|
71
|
+
done();
|
|
72
|
+
}, 0);
|
|
73
|
+
}, 0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should react to complete array replacement', (done) => {
|
|
77
|
+
container.innerHTML = '<div @text="items.length"></div>';
|
|
78
|
+
const state = helium({ items: [1, 2, 3] });
|
|
79
|
+
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
state.items = [...state.items, 4, 5];
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
expect(container.querySelector('div').textContent).toBe('5');
|
|
84
|
+
done();
|
|
85
|
+
}, 0);
|
|
86
|
+
}, 0);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('Directives - @text and @html', () => {
|
|
91
|
+
it('should bind text content with @text', () => {
|
|
92
|
+
container.innerHTML = '<div @text="message"></div>';
|
|
93
|
+
helium({ message: 'Hello World' });
|
|
94
|
+
expect(container.querySelector('div').textContent).toBe('Hello World');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should bind HTML content with @html', () => {
|
|
98
|
+
container.innerHTML = '<div @html="content"></div>';
|
|
99
|
+
helium({ content: '<strong>Bold</strong>' });
|
|
100
|
+
expect(container.querySelector('div').innerHTML).toBe('<strong>Bold</strong>');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should work with data-he-text prefix', () => {
|
|
104
|
+
container.innerHTML = '<div data-he-text="message"></div>';
|
|
105
|
+
helium({ message: 'Test' });
|
|
106
|
+
expect(container.querySelector('div').textContent).toBe('Test');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should evaluate expressions in @text', () => {
|
|
110
|
+
container.innerHTML = '<div @text="count * 2"></div>';
|
|
111
|
+
helium({ count: 5 });
|
|
112
|
+
expect(container.querySelector('div').textContent).toBe('10');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Directives - @bind', () => {
|
|
117
|
+
it('should create two-way binding with input', () => {
|
|
118
|
+
container.innerHTML = '<input @bind="name" />';
|
|
119
|
+
const state = helium({ name: 'John' });
|
|
120
|
+
const input = container.querySelector('input');
|
|
121
|
+
|
|
122
|
+
expect(input.value).toBe('John');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should update state when input changes', (done) => {
|
|
126
|
+
container.innerHTML = '<input @bind="name" /><span @text="name"></span>';
|
|
127
|
+
const state = helium({ name: 'John' });
|
|
128
|
+
const input = container.querySelector('input');
|
|
129
|
+
|
|
130
|
+
input.value = 'Jane';
|
|
131
|
+
input.dispatchEvent(new Event('input'));
|
|
132
|
+
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
expect(container.querySelector('span').textContent).toBe('Jane');
|
|
135
|
+
done();
|
|
136
|
+
}, 0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('Directives - @hidden and @visible', () => {
|
|
141
|
+
it('should hide element with @hidden when true', () => {
|
|
142
|
+
container.innerHTML = '<div @hidden="isHidden"></div>';
|
|
143
|
+
helium({ isHidden: true });
|
|
144
|
+
expect(container.querySelector('div').hidden).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should show element with @hidden when false', () => {
|
|
148
|
+
container.innerHTML = '<div @hidden="isHidden"></div>';
|
|
149
|
+
helium({ isHidden: false });
|
|
150
|
+
expect(container.querySelector('div').hidden).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should show element with @visible when true', () => {
|
|
154
|
+
container.innerHTML = '<div @visible="isVisible"></div>';
|
|
155
|
+
helium({ isVisible: true });
|
|
156
|
+
expect(container.querySelector('div').hidden).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should hide element with @visible when false', () => {
|
|
160
|
+
container.innerHTML = '<div @visible="isVisible"></div>';
|
|
161
|
+
helium({ isVisible: false });
|
|
162
|
+
expect(container.querySelector('div').hidden).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('Directives - Dynamic Attributes (:attr)', () => {
|
|
167
|
+
it('should bind attribute dynamically', () => {
|
|
168
|
+
container.innerHTML = '<div :class="className"></div>';
|
|
169
|
+
helium({ className: 'active' });
|
|
170
|
+
expect(container.querySelector('div').getAttribute('class')).toBe('active');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should update attribute on state change', (done) => {
|
|
174
|
+
container.innerHTML = '<div :title="tooltip"></div>';
|
|
175
|
+
const state = helium({ tooltip: 'Initial' });
|
|
176
|
+
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
state.tooltip = 'Updated';
|
|
179
|
+
setTimeout(() => {
|
|
180
|
+
expect(container.querySelector('div').getAttribute('title')).toBe('Updated');
|
|
181
|
+
done();
|
|
182
|
+
}, 0);
|
|
183
|
+
}, 0);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Event Handlers', () => {
|
|
188
|
+
it('should handle click events', () => {
|
|
189
|
+
container.innerHTML = '<button @click="count++">Click</button><span @text="count"></span>';
|
|
190
|
+
helium({ count: 0 });
|
|
191
|
+
|
|
192
|
+
container.querySelector('button').click();
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
expect(container.querySelector('span').textContent).toBe('1');
|
|
195
|
+
}, 0);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle event with prevent modifier', () => {
|
|
199
|
+
container.innerHTML = '<form @submit.prevent="submitted = true"></form>';
|
|
200
|
+
const state = helium({ submitted: false });
|
|
201
|
+
const form = container.querySelector('form');
|
|
202
|
+
const event = new Event('submit', { cancelable: true });
|
|
203
|
+
|
|
204
|
+
form.dispatchEvent(event);
|
|
205
|
+
expect(event.defaultPrevented).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle keyboard events with key modifiers', () => {
|
|
209
|
+
container.innerHTML = '<input @keydown.enter="pressed = true" />';
|
|
210
|
+
const state = helium({ pressed: false });
|
|
211
|
+
const input = container.querySelector('input');
|
|
212
|
+
|
|
213
|
+
const event = new KeyboardEvent('keydown', { key: 'Enter' });
|
|
214
|
+
input.dispatchEvent(event);
|
|
215
|
+
|
|
216
|
+
setTimeout(() => {
|
|
217
|
+
expect(state.pressed).toBe(true);
|
|
218
|
+
}, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle debounced events', (done) => {
|
|
222
|
+
container.innerHTML = '<input @input.debounce:100="value = $event.target.value" />';
|
|
223
|
+
const state = helium({ value: '' });
|
|
224
|
+
const input = container.querySelector('input');
|
|
225
|
+
|
|
226
|
+
input.value = 'test';
|
|
227
|
+
input.dispatchEvent(new Event('input'));
|
|
228
|
+
|
|
229
|
+
// Should not update immediately
|
|
230
|
+
expect(state.value).toBe('');
|
|
231
|
+
|
|
232
|
+
setTimeout(() => {
|
|
233
|
+
expect(state.value).toBe('test');
|
|
234
|
+
done();
|
|
235
|
+
}, 150);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should handle once modifier', () => {
|
|
239
|
+
container.innerHTML = '<button @click.once="count++">Click</button>';
|
|
240
|
+
const state = helium({ count: 0 });
|
|
241
|
+
const button = container.querySelector('button');
|
|
242
|
+
|
|
243
|
+
button.click();
|
|
244
|
+
button.click();
|
|
245
|
+
button.click();
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
expect(state.count).toBe(1);
|
|
249
|
+
}, 0);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('@ref Directive', () => {
|
|
254
|
+
it('should create reference to element', () => {
|
|
255
|
+
container.innerHTML = '<div @ref="myDiv"></div><button @click="$myDiv.textContent = \'Changed\'">Change</button>';
|
|
256
|
+
helium({});
|
|
257
|
+
|
|
258
|
+
container.querySelector('button').click();
|
|
259
|
+
setTimeout(() => {
|
|
260
|
+
expect(container.querySelector('div').textContent).toBe('Changed');
|
|
261
|
+
}, 0);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('@init Directive', () => {
|
|
266
|
+
it('should execute code on initialization', () => {
|
|
267
|
+
container.innerHTML = '<div @init="initialized = true"></div>';
|
|
268
|
+
const state = helium({ initialized: false });
|
|
269
|
+
expect(state.initialized).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('@data Directive', () => {
|
|
274
|
+
it('should merge data into state', () => {
|
|
275
|
+
container.innerHTML = '<div @data="{ user: \'Alice\', age: 30 }"></div><span @text="user"></span>';
|
|
276
|
+
helium({});
|
|
277
|
+
expect(container.querySelector('span').textContent).toBe('Alice');
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe('Helper Functions', () => {
|
|
282
|
+
it('should provide $ selector helper', () => {
|
|
283
|
+
container.innerHTML = '<div id="target"></div><button @click="$(\'#target\').textContent = \'Found\'">Click</button>';
|
|
284
|
+
helium({});
|
|
285
|
+
|
|
286
|
+
container.querySelector('button').click();
|
|
287
|
+
setTimeout(() => {
|
|
288
|
+
expect(container.querySelector('#target').textContent).toBe('Found');
|
|
289
|
+
}, 0);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should provide $html helper to create elements', () => {
|
|
293
|
+
container.innerHTML = '<div @init="$el.appendChild($html(\'<span>Test</span>\'))"></div>';
|
|
294
|
+
helium({});
|
|
295
|
+
expect(container.querySelector('span').textContent).toBe('Test');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('AJAX Helpers', () => {
|
|
300
|
+
beforeEach(() => {
|
|
301
|
+
global.fetch = vi.fn();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should call GET request with $get', async () => {
|
|
305
|
+
global.fetch.mockResolvedValueOnce({
|
|
306
|
+
headers: { get: () => 'application/json' },
|
|
307
|
+
json: async () => ({ data: 'test' })
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
container.innerHTML = '<button @click="$get(\'/api/data\', \'result\')">Load</button><div @text="result?.data"></div>';
|
|
311
|
+
helium({ result: null });
|
|
312
|
+
|
|
313
|
+
container.querySelector('button').click();
|
|
314
|
+
|
|
315
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
316
|
+
expect(global.fetch).toHaveBeenCalledWith('/api/data', expect.any(Object));
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should call POST request with $post', async () => {
|
|
320
|
+
global.fetch.mockResolvedValueOnce({
|
|
321
|
+
headers: { get: () => 'application/json' },
|
|
322
|
+
json: async () => ({ success: true })
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
container.innerHTML = '<button @click="$post(\'/api/save\', { name: \'test\' }, \'response\')">Save</button>';
|
|
326
|
+
helium({ response: null });
|
|
327
|
+
|
|
328
|
+
container.querySelector('button').click();
|
|
329
|
+
|
|
330
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
331
|
+
expect(global.fetch).toHaveBeenCalledWith('/api/save', expect.objectContaining({
|
|
332
|
+
method: 'POST',
|
|
333
|
+
body: JSON.stringify({ name: 'test' })
|
|
334
|
+
}));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('Dynamic DOM Updates', () => {
|
|
339
|
+
it('should process dynamically added elements', (done) => {
|
|
340
|
+
container.innerHTML = '<div id="container"></div>';
|
|
341
|
+
const state = helium({ message: 'Dynamic' });
|
|
342
|
+
|
|
343
|
+
setTimeout(() => {
|
|
344
|
+
const newEl = document.createElement('span');
|
|
345
|
+
newEl.setAttribute('@text', 'message');
|
|
346
|
+
container.querySelector('#container').appendChild(newEl);
|
|
347
|
+
|
|
348
|
+
setTimeout(() => {
|
|
349
|
+
expect(newEl.textContent).toBe('Dynamic');
|
|
350
|
+
done();
|
|
351
|
+
}, 100);
|
|
352
|
+
}, 0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should handle innerHTML updates with new bindings', (done) => {
|
|
356
|
+
container.innerHTML = '<div @html="content"></div>';
|
|
357
|
+
const state = helium({ content: '', name: 'Test' });
|
|
358
|
+
|
|
359
|
+
setTimeout(() => {
|
|
360
|
+
state.content = '<span @text="name"></span>';
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
expect(container.querySelector('span').textContent).toBe('Test');
|
|
363
|
+
done();
|
|
364
|
+
}, 100);
|
|
365
|
+
}, 0);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe('Edge Cases', () => {
|
|
370
|
+
it('should handle undefined state values', () => {
|
|
371
|
+
container.innerHTML = '<div @text="undefined"></div>';
|
|
372
|
+
helium({});
|
|
373
|
+
expect(container.querySelector('div').textContent).toBe('undefined');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle null state values', () => {
|
|
377
|
+
container.innerHTML = '<div @text="value"></div>';
|
|
378
|
+
helium({ value: null });
|
|
379
|
+
expect(container.querySelector('div').textContent).toBe('null');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle circular references in state', () => {
|
|
383
|
+
const obj = { name: 'test' };
|
|
384
|
+
obj.self = obj;
|
|
385
|
+
|
|
386
|
+
container.innerHTML = '<div @text="obj.name"></div>';
|
|
387
|
+
expect(() => helium({ obj })).not.toThrow();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should not process already processed elements', () => {
|
|
391
|
+
container.innerHTML = '<div @text="count"></div>';
|
|
392
|
+
const state = helium({ count: 0 });
|
|
393
|
+
|
|
394
|
+
// Manually mark as processed
|
|
395
|
+
container.querySelector('div').setAttribute('data-helium-processed', '');
|
|
396
|
+
|
|
397
|
+
state.count = 5;
|
|
398
|
+
// Should still work because binding was already created
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
expect(container.querySelector('div').textContent).toBe('5');
|
|
401
|
+
}, 0);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe('Performance', () => {
|
|
406
|
+
it('should handle many bindings efficiently', () => {
|
|
407
|
+
const html = Array.from({ length: 100 }, (_, i) =>
|
|
408
|
+
`<div @text="items[${i}]"></div>`
|
|
409
|
+
).join('');
|
|
410
|
+
|
|
411
|
+
container.innerHTML = html;
|
|
412
|
+
const items = Array.from({ length: 100 }, (_, i) => i);
|
|
413
|
+
|
|
414
|
+
const start = performance.now();
|
|
415
|
+
helium({ items });
|
|
416
|
+
const end = performance.now();
|
|
417
|
+
|
|
418
|
+
expect(end - start).toBeLessThan(100); // Should initialize in < 100ms
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should not cause infinite loops with DOM mutations', (done) => {
|
|
422
|
+
container.innerHTML = '<div @html="content"></div>';
|
|
423
|
+
const state = helium({ content: '<span>Test</span>', updateCount: 0 });
|
|
424
|
+
|
|
425
|
+
let mutationCount = 0;
|
|
426
|
+
const observer = new MutationObserver(() => {
|
|
427
|
+
mutationCount++;
|
|
428
|
+
});
|
|
429
|
+
observer.observe(container, { childList: true, subtree: true });
|
|
430
|
+
|
|
431
|
+
setTimeout(() => {
|
|
432
|
+
state.content = '<span>Updated</span>';
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
expect(mutationCount).toBeLessThan(10); // Should not cause excessive mutations
|
|
435
|
+
observer.disconnect();
|
|
436
|
+
done();
|
|
437
|
+
}, 100);
|
|
438
|
+
}, 0);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daz4126/helium",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"main": "helium.js",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest",
|
|
8
|
+
"test:ui": "vitest --ui",
|
|
9
|
+
"test:run": "vitest run"
|
|
10
|
+
},
|
|
6
11
|
"keywords": [
|
|
7
12
|
"tiny",
|
|
8
13
|
"reactive",
|