@daz4126/helium 0.7.3 → 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 +130 -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,80 +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 = (
|
|
20
|
-
fetch(
|
|
21
|
-
method
|
|
22
|
-
headers: {
|
|
23
|
-
|
|
11
|
+
const ajax = (url, method, target, opts = {}) => {
|
|
12
|
+
fetch(url, {
|
|
13
|
+
method,
|
|
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
|
-
r
|
|
22
|
+
.then(r =>
|
|
23
|
+
r.headers.get("content-type")?.includes("application/json") ? r.json() : r.text()
|
|
27
24
|
)
|
|
28
|
-
.then(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"GET",
|
|
32
|
-
"POST",
|
|
33
|
-
"PUT",
|
|
34
|
-
"PATCH",
|
|
35
|
-
"DELETE",
|
|
36
|
-
].map((m) => (u, d, t) => ajax(m, u, t, d));
|
|
25
|
+
.then(d => state[target] = d)
|
|
26
|
+
.catch(e => console.error("AJAX error:", e.message));
|
|
27
|
+
};
|
|
37
28
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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;
|
|
41
36
|
},
|
|
42
|
-
set(target, prop,
|
|
43
|
-
|
|
44
|
-
if (target
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
47
|
}
|
|
50
|
-
return
|
|
51
|
-
}
|
|
52
|
-
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
53
51
|
|
|
52
|
+
const state = new Proxy(data, handler);
|
|
54
53
|
let isUpdatingDOM = false;
|
|
55
54
|
|
|
56
55
|
function applyBinding(binding, event = {}, elCtx = binding.el) {
|
|
57
56
|
const { el, prop, fn } = binding;
|
|
58
|
-
const result = fn(
|
|
59
|
-
|
|
60
|
-
state,
|
|
61
|
-
event,
|
|
62
|
-
elCtx,
|
|
63
|
-
html,
|
|
64
|
-
get,
|
|
65
|
-
post,
|
|
66
|
-
put,
|
|
67
|
-
patch,
|
|
68
|
-
del,
|
|
69
|
-
...Object.values(data),
|
|
70
|
-
...[...refs.values()],
|
|
71
|
-
);
|
|
72
|
-
// guard to reduce observer recursion (optional but helpful)
|
|
73
|
-
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") {
|
|
74
59
|
isUpdatingDOM = true;
|
|
75
60
|
el.innerHTML = result;
|
|
76
61
|
isUpdatingDOM = false;
|
|
77
|
-
} 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) {
|
|
78
75
|
el[prop] = result;
|
|
79
76
|
} else {
|
|
80
77
|
el.setAttribute(prop, result);
|
|
@@ -83,286 +80,132 @@ export default function helium(data = {}) {
|
|
|
83
80
|
|
|
84
81
|
function compileExpression(expr, withReturn = false) {
|
|
85
82
|
try {
|
|
86
|
-
return new Function(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"$event",
|
|
90
|
-
"$el",
|
|
91
|
-
"$html",
|
|
92
|
-
"$get",
|
|
93
|
-
"$post",
|
|
94
|
-
"$put",
|
|
95
|
-
"$patch",
|
|
96
|
-
"$delete",
|
|
97
|
-
...Object.keys(data),
|
|
98
|
-
...[...refs.keys()],
|
|
99
|
-
`with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
|
|
100
|
-
);
|
|
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()}) }`);
|
|
101
86
|
} catch (err) {
|
|
102
87
|
return () => expr;
|
|
103
88
|
}
|
|
104
89
|
}
|
|
105
90
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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", "");
|
|
109
95
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
element.nodeType === 1 &&
|
|
113
|
-
element.hasAttribute &&
|
|
114
|
-
element.hasAttribute("data-helium-processed")
|
|
115
|
-
)
|
|
116
|
-
return newlyAddedBindings;
|
|
117
|
-
if (element.nodeType === 1 && element.setAttribute)
|
|
118
|
-
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-")))];
|
|
119
98
|
|
|
120
|
-
const
|
|
121
|
-
element,
|
|
122
|
-
|
|
123
|
-
Array.from(el.attributes).some((attr) =>
|
|
124
|
-
attr.name.startsWith("data-he-"),
|
|
125
|
-
),
|
|
126
|
-
),
|
|
127
|
-
];
|
|
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));
|
|
128
102
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
element,
|
|
132
|
-
null,
|
|
133
|
-
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
|
134
|
-
null,
|
|
135
|
-
);
|
|
136
|
-
for (let i = 0; i < xpath.snapshotLength; i++)
|
|
137
|
-
heliumElements.push(xpath.snapshotItem(i));
|
|
138
|
-
|
|
139
|
-
// seed default state values (no binding creation here)
|
|
140
|
-
heliumElements.forEach((el) => {
|
|
103
|
+
// Seed default state values
|
|
104
|
+
heElements.forEach((el) => {
|
|
141
105
|
for (const { name, value } of el.attributes || []) {
|
|
142
|
-
if (
|
|
143
|
-
name == "@html" ||
|
|
144
|
-
name == "data-he-html" ||
|
|
145
|
-
name == "@text" ||
|
|
146
|
-
name == "data-he-text" ||
|
|
147
|
-
name == "@bind" ||
|
|
148
|
-
name == "data-he-bind"
|
|
149
|
-
) {
|
|
106
|
+
if (["@html","data-he-html","@text","data-he-text","@bind","data-he-bind"].includes(name)) {
|
|
150
107
|
try {
|
|
151
108
|
new Function(`let ${value} = 1`);
|
|
152
|
-
state[value] ||=
|
|
153
|
-
name == "@bind" || name == "data-he-bind"
|
|
154
|
-
? el.value
|
|
155
|
-
: el.textContent;
|
|
109
|
+
state[value] ||= ["@bind","data-he-bind"].includes(name) ? el.value : el.textContent;
|
|
156
110
|
} catch (e) {}
|
|
157
111
|
}
|
|
158
112
|
}
|
|
159
113
|
});
|
|
160
114
|
|
|
161
|
-
// Register bindings for these elements and collect the newly added binding objects
|
|
162
|
-
heliumElements.forEach((el) => {
|
|
163
|
-
for (const { name, value } of el.attributes || []) {
|
|
164
|
-
if (name == "@data" || name == "data-he") {
|
|
165
|
-
Object.assign(
|
|
166
|
-
state,
|
|
167
|
-
compileExpression(value, true)(
|
|
168
|
-
$,
|
|
169
|
-
state,
|
|
170
|
-
{},
|
|
171
|
-
el,
|
|
172
|
-
html,
|
|
173
|
-
get,
|
|
174
|
-
post,
|
|
175
|
-
put,
|
|
176
|
-
patch,
|
|
177
|
-
del,
|
|
178
|
-
...Object.values(data),
|
|
179
|
-
...[...refs.values()],
|
|
180
|
-
),
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
|
|
184
115
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
? "textContent"
|
|
199
|
-
: "innerHTML",
|
|
200
|
-
expr: value,
|
|
201
|
-
fn: compileExpression(value, true),
|
|
202
|
-
};
|
|
203
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
204
|
-
newlyAddedBindings.push(b);
|
|
205
|
-
});
|
|
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
|
+
});
|
|
206
129
|
}
|
|
207
130
|
|
|
208
|
-
if (
|
|
131
|
+
if (["@bind","data-he-bind"].includes(name)) {
|
|
209
132
|
el.addEventListener("input", (e) => (state[value] = e.target.value));
|
|
210
|
-
const b = {
|
|
211
|
-
el,
|
|
212
|
-
prop: "value",
|
|
213
|
-
expr: value,
|
|
214
|
-
fn: compileExpression(value, true),
|
|
215
|
-
};
|
|
133
|
+
const b = { el, prop: "value", expr: value, fn: compileExpression(value, true) };
|
|
216
134
|
bindings.set(value, [...(bindings.get(value) || []), b]);
|
|
217
|
-
|
|
135
|
+
newBindings.push(b);
|
|
218
136
|
el.value = state[value];
|
|
219
137
|
}
|
|
220
138
|
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
Object.keys(state)
|
|
228
|
-
.filter((key) => value.includes(key))
|
|
229
|
-
.forEach((val) => {
|
|
230
|
-
const b = {
|
|
231
|
-
el,
|
|
232
|
-
prop: "hidden",
|
|
233
|
-
expr: value,
|
|
234
|
-
fn: compileExpression(
|
|
235
|
-
`${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
|
|
236
|
-
true,
|
|
237
|
-
),
|
|
238
|
-
};
|
|
239
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
240
|
-
newlyAddedBindings.push(b);
|
|
241
|
-
});
|
|
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
|
+
});
|
|
242
145
|
}
|
|
243
146
|
|
|
244
147
|
if (name.startsWith(":")) {
|
|
245
|
-
Object.keys(state)
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
prop: name.split(":")[1],
|
|
251
|
-
expr: value,
|
|
252
|
-
fn: compileExpression(value, true),
|
|
253
|
-
};
|
|
254
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
255
|
-
newlyAddedBindings.push(b);
|
|
256
|
-
});
|
|
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
|
+
});
|
|
257
153
|
}
|
|
258
154
|
|
|
259
|
-
if (
|
|
260
|
-
compileExpression(value, false)(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
el,
|
|
265
|
-
html,
|
|
266
|
-
get,
|
|
267
|
-
post,
|
|
268
|
-
put,
|
|
269
|
-
patch,
|
|
270
|
-
del,
|
|
271
|
-
...Object.values(data),
|
|
272
|
-
...[...refs.values()],
|
|
273
|
-
);
|
|
274
|
-
} else if (
|
|
275
|
-
(name.startsWith("@") || name.startsWith("data-he-on")) &&
|
|
276
|
-
addEvents
|
|
277
|
-
) {
|
|
278
|
-
// event handling — keep this as before (register listeners)
|
|
279
|
-
const [eventName, ...modifiers] = name
|
|
280
|
-
.slice(name.startsWith("@") ? 1 : 10)
|
|
281
|
-
.split(".");
|
|
282
|
-
const receiver =
|
|
283
|
-
modifiers.includes("outside") || modifiers.includes("document")
|
|
284
|
-
? document
|
|
285
|
-
: 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;
|
|
286
160
|
const debounce = (fn, delay) => {
|
|
287
161
|
let timeout;
|
|
288
|
-
return
|
|
162
|
+
return (...args) => {
|
|
289
163
|
clearTimeout(timeout);
|
|
290
|
-
timeout = setTimeout(() => fn
|
|
164
|
+
timeout = setTimeout(() => fn(...args), delay);
|
|
291
165
|
};
|
|
292
166
|
};
|
|
293
167
|
|
|
294
168
|
let debounceDelay = 0;
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const time = debounceModifier.split(":")[1];
|
|
300
|
-
if (time && !isNaN(time)) debounceDelay = Number(time);
|
|
301
|
-
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;
|
|
302
173
|
}
|
|
303
174
|
|
|
304
175
|
function _handler(e) {
|
|
305
|
-
if (
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
alt: "altKey",
|
|
310
|
-
meta: "metaKey",
|
|
311
|
-
};
|
|
312
|
-
for (const [mod, prop] of Object.entries(keyModifiers)) {
|
|
313
|
-
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;
|
|
314
180
|
}
|
|
315
|
-
if (["keydown",
|
|
316
|
-
const last =
|
|
181
|
+
if (["keydown","keyup","keypress"].includes(eventName)) {
|
|
182
|
+
const last = mods[mods.length - 1];
|
|
317
183
|
if (last) {
|
|
318
|
-
let keyName = e.key;
|
|
319
|
-
if (keyName === " ") keyName = "Space";
|
|
320
|
-
else if (keyName === "Escape") keyName = "Esc";
|
|
184
|
+
let keyName = e.key === " " ? "Space" : e.key === "Escape" ? "Esc" : e.key;
|
|
321
185
|
if (keyName.toLowerCase() !== last.toLowerCase()) return;
|
|
322
186
|
}
|
|
323
187
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
compileExpression(value, false)(
|
|
327
|
-
$,
|
|
328
|
-
state,
|
|
329
|
-
e,
|
|
330
|
-
el,
|
|
331
|
-
html,
|
|
332
|
-
get,
|
|
333
|
-
post,
|
|
334
|
-
put,
|
|
335
|
-
patch,
|
|
336
|
-
del,
|
|
337
|
-
...Object.values(data),
|
|
338
|
-
...[...refs.values()],
|
|
339
|
-
);
|
|
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()]);
|
|
340
190
|
}
|
|
341
|
-
if (
|
|
342
|
-
el.removeEventListener(eventName, debouncedHandler);
|
|
191
|
+
if (mods.includes("once")) el.removeEventListener(eventName, debouncedHandler);
|
|
343
192
|
}
|
|
344
193
|
|
|
345
|
-
const debouncedHandler =
|
|
346
|
-
debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
194
|
+
const debouncedHandler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
347
195
|
receiver.addEventListener(eventName, debouncedHandler);
|
|
348
196
|
}
|
|
349
|
-
}
|
|
350
|
-
});
|
|
197
|
+
}
|
|
198
|
+
});
|
|
351
199
|
|
|
352
|
-
return
|
|
353
|
-
}
|
|
200
|
+
return newBindings;
|
|
201
|
+
}
|
|
354
202
|
|
|
355
|
-
// set up mutation observer that only processes and applies bindings for newly added nodes
|
|
356
203
|
const observer = new MutationObserver((mutations) => {
|
|
357
|
-
if (isUpdatingDOM) return;
|
|
204
|
+
if (isUpdatingDOM) return;
|
|
358
205
|
for (const m of mutations) {
|
|
359
206
|
for (const node of m.addedNodes) {
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
!node.hasAttribute("data-helium-processed")
|
|
363
|
-
) {
|
|
364
|
-
const newly = processElements(node, false);
|
|
365
|
-
// 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);
|
|
366
209
|
newly.forEach(applyBinding);
|
|
367
210
|
}
|
|
368
211
|
}
|
|
@@ -370,9 +213,6 @@ export default function helium(data = {}) {
|
|
|
370
213
|
});
|
|
371
214
|
observer.observe(root, { childList: true, subtree: true });
|
|
372
215
|
|
|
373
|
-
|
|
216
|
+
processElements(root);
|
|
374
217
|
for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
|
|
375
|
-
ue = false;
|
|
376
218
|
}
|
|
377
|
-
|
|
378
|
-
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",
|