@daz4126/helium 0.5.2 → 0.6.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 +1 -3
- package/helium.js +176 -72
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +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
|
|
17
|
+
To use, just import from the CDN (no install or build step required!):
|
|
18
18
|
|
|
19
19
|
```javascript
|
|
20
20
|
import helium from "https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js"
|
|
21
|
-
helium()
|
|
22
21
|
```
|
|
23
22
|
|
|
24
23
|
Alernatively you can install from NPM:
|
|
@@ -31,7 +30,6 @@ Then include it in your
|
|
|
31
30
|
|
|
32
31
|
```javascript
|
|
33
32
|
import helium from "@daz4126/helium"
|
|
34
|
-
helium()
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
# Helium Attributes
|
package/helium.js
CHANGED
|
@@ -3,6 +3,10 @@ export default function helium(data = {}) {
|
|
|
3
3
|
document.querySelector("[\\@helium]") ||
|
|
4
4
|
document.querySelector("[data-helium]") ||
|
|
5
5
|
document.body;
|
|
6
|
+
if (Object.keys(data).length > 0)
|
|
7
|
+
document
|
|
8
|
+
.querySelectorAll("[data-helium-processed]")
|
|
9
|
+
.forEach((el) => el.removeAttribute("data-helium-processed"));
|
|
6
10
|
const [bindings, refs] = [new Map(), new Map()];
|
|
7
11
|
const $ = (selector) => document.querySelector(selector);
|
|
8
12
|
const html = (string) => {
|
|
@@ -10,17 +14,43 @@ export default function helium(data = {}) {
|
|
|
10
14
|
temp.innerHTML = string;
|
|
11
15
|
return temp.content.firstChild;
|
|
12
16
|
};
|
|
17
|
+
|
|
18
|
+
const ajax = (url, method, target, options) => {
|
|
19
|
+
fetch(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: method === "GET" ? null : JSON.stringify(options),
|
|
23
|
+
})
|
|
24
|
+
.then((res) =>
|
|
25
|
+
res.headers.get("content-type")?.includes("application/json")
|
|
26
|
+
? res.json()
|
|
27
|
+
: res.text(),
|
|
28
|
+
)
|
|
29
|
+
.then((data) => (state[target] = data));
|
|
30
|
+
};
|
|
31
|
+
const get = (url, prop) => ajax(url, "GET", prop);
|
|
32
|
+
const post = (url, params, prop) => ajax(url, "POST", prop, params);
|
|
33
|
+
const patch = (url, params, prop) => ajax(url, "PATCH", prop, params);
|
|
34
|
+
const del = (url, params, prop) => ajax(url, "DELETE", prop, params);
|
|
35
|
+
|
|
13
36
|
const state = new Proxy(data, {
|
|
14
37
|
get(target, prop) {
|
|
15
38
|
return target[prop];
|
|
16
39
|
},
|
|
17
40
|
set(target, prop, value) {
|
|
41
|
+
// skip if identical (prevents redundant re-renders)
|
|
42
|
+
if (target[prop] === value) return true;
|
|
18
43
|
target[prop] = value;
|
|
19
|
-
if (bindings.has(prop))
|
|
44
|
+
if (bindings.has(prop)) {
|
|
45
|
+
// only call the bindings for this prop
|
|
20
46
|
bindings.get(prop).forEach((binding) => applyBinding(binding));
|
|
47
|
+
}
|
|
21
48
|
return true;
|
|
22
49
|
},
|
|
23
50
|
});
|
|
51
|
+
|
|
52
|
+
let isUpdatingDOM = false;
|
|
53
|
+
|
|
24
54
|
function applyBinding(binding, event = {}, elCtx = binding.el) {
|
|
25
55
|
const { el, prop, fn } = binding;
|
|
26
56
|
const result = fn(
|
|
@@ -29,12 +59,25 @@ export default function helium(data = {}) {
|
|
|
29
59
|
event,
|
|
30
60
|
elCtx,
|
|
31
61
|
html,
|
|
32
|
-
|
|
62
|
+
get,
|
|
63
|
+
post,
|
|
64
|
+
patch,
|
|
65
|
+
del,
|
|
66
|
+
...Object.values(data),
|
|
33
67
|
...[...refs.values()],
|
|
34
68
|
);
|
|
35
|
-
|
|
36
|
-
|
|
69
|
+
// guard to reduce observer recursion (optional but helpful)
|
|
70
|
+
if (prop === "innerHTML") {
|
|
71
|
+
isUpdatingDOM = true;
|
|
72
|
+
el.innerHTML = result;
|
|
73
|
+
isUpdatingDOM = false;
|
|
74
|
+
} else if (prop in el) {
|
|
75
|
+
el[prop] = result;
|
|
76
|
+
} else {
|
|
77
|
+
el.setAttribute(prop, result);
|
|
78
|
+
}
|
|
37
79
|
}
|
|
80
|
+
|
|
38
81
|
function compileExpression(expr, withReturn = false) {
|
|
39
82
|
try {
|
|
40
83
|
return new Function(
|
|
@@ -43,6 +86,10 @@ export default function helium(data = {}) {
|
|
|
43
86
|
"$event",
|
|
44
87
|
"$el",
|
|
45
88
|
"$html",
|
|
89
|
+
"$get",
|
|
90
|
+
"$post",
|
|
91
|
+
"$patch",
|
|
92
|
+
"$delete",
|
|
46
93
|
...Object.keys(data),
|
|
47
94
|
...[...refs.keys()],
|
|
48
95
|
`with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
|
|
@@ -51,7 +98,21 @@ export default function helium(data = {}) {
|
|
|
51
98
|
return () => expr;
|
|
52
99
|
}
|
|
53
100
|
}
|
|
101
|
+
|
|
102
|
+
// processElements now returns an array of newly-created binding objects
|
|
54
103
|
function processElements(element) {
|
|
104
|
+
const newlyAddedBindings = [];
|
|
105
|
+
|
|
106
|
+
// quick guard: avoid re-processing same node
|
|
107
|
+
if (
|
|
108
|
+
element.nodeType === 1 &&
|
|
109
|
+
element.hasAttribute &&
|
|
110
|
+
element.hasAttribute("data-helium-processed")
|
|
111
|
+
)
|
|
112
|
+
return newlyAddedBindings;
|
|
113
|
+
if (element.nodeType === 1 && element.setAttribute)
|
|
114
|
+
element.setAttribute("data-helium-processed", "");
|
|
115
|
+
|
|
55
116
|
const heliumElements = [
|
|
56
117
|
element,
|
|
57
118
|
...Array.from(element.querySelectorAll("*")).filter((el) =>
|
|
@@ -60,6 +121,7 @@ export default function helium(data = {}) {
|
|
|
60
121
|
),
|
|
61
122
|
),
|
|
62
123
|
];
|
|
124
|
+
|
|
63
125
|
const xpath = document.evaluate(
|
|
64
126
|
".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
|
|
65
127
|
element,
|
|
@@ -69,8 +131,10 @@ export default function helium(data = {}) {
|
|
|
69
131
|
);
|
|
70
132
|
for (let i = 0; i < xpath.snapshotLength; i++)
|
|
71
133
|
heliumElements.push(xpath.snapshotItem(i));
|
|
134
|
+
|
|
135
|
+
// seed default state values (no binding creation here)
|
|
72
136
|
heliumElements.forEach((el) => {
|
|
73
|
-
for (const { name, value } of el.attributes) {
|
|
137
|
+
for (const { name, value } of el.attributes || []) {
|
|
74
138
|
if (
|
|
75
139
|
name == "@text" ||
|
|
76
140
|
name == "data-he-text" ||
|
|
@@ -87,9 +151,11 @@ export default function helium(data = {}) {
|
|
|
87
151
|
}
|
|
88
152
|
}
|
|
89
153
|
});
|
|
154
|
+
|
|
155
|
+
// Register bindings for these elements and collect the newly added binding objects
|
|
90
156
|
heliumElements.forEach((el) => {
|
|
91
|
-
for (const { name, value } of el.attributes) {
|
|
92
|
-
if (name == "@data" || name == "data-he")
|
|
157
|
+
for (const { name, value } of el.attributes || []) {
|
|
158
|
+
if (name == "@data" || name == "data-he") {
|
|
93
159
|
Object.assign(
|
|
94
160
|
state,
|
|
95
161
|
compileExpression(value, true)(
|
|
@@ -98,85 +164,107 @@ export default function helium(data = {}) {
|
|
|
98
164
|
{},
|
|
99
165
|
el,
|
|
100
166
|
html,
|
|
101
|
-
|
|
167
|
+
get,
|
|
168
|
+
post,
|
|
169
|
+
patch,
|
|
170
|
+
del,
|
|
171
|
+
...Object.values(data),
|
|
102
172
|
...[...refs.values()],
|
|
103
173
|
),
|
|
104
174
|
);
|
|
175
|
+
}
|
|
105
176
|
if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
|
|
106
|
-
|
|
177
|
+
|
|
178
|
+
if (
|
|
179
|
+
name == "@text" ||
|
|
180
|
+
name == "data-he-text" ||
|
|
181
|
+
name == "@html" ||
|
|
182
|
+
name == "data-he-html"
|
|
183
|
+
) {
|
|
107
184
|
Object.keys(state)
|
|
108
185
|
.filter((key) => value.includes(key))
|
|
109
|
-
.forEach((val) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
186
|
+
.forEach((val) => {
|
|
187
|
+
const b = {
|
|
188
|
+
el,
|
|
189
|
+
prop:
|
|
190
|
+
name == "@text" || name == "data-he-text"
|
|
191
|
+
? "textContent"
|
|
192
|
+
: "innerHTML",
|
|
193
|
+
expr: value,
|
|
194
|
+
fn: compileExpression(value, true),
|
|
195
|
+
};
|
|
196
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
197
|
+
newlyAddedBindings.push(b);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
120
201
|
if (name == "@bind" || name == "data-he-bind") {
|
|
121
202
|
el.addEventListener("input", (e) => (state[value] = e.target.value));
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]);
|
|
203
|
+
const b = {
|
|
204
|
+
el,
|
|
205
|
+
prop: "value",
|
|
206
|
+
expr: value,
|
|
207
|
+
fn: compileExpression(value, true),
|
|
208
|
+
};
|
|
209
|
+
bindings.set(value, [...(bindings.get(value) || []), b]);
|
|
210
|
+
newlyAddedBindings.push(b);
|
|
131
211
|
el.value = state[value];
|
|
132
212
|
}
|
|
213
|
+
|
|
133
214
|
if (
|
|
134
215
|
name == "@hidden" ||
|
|
135
216
|
name == "@visible" ||
|
|
136
217
|
name == "data-he-hidden" ||
|
|
137
218
|
name == "data-he-visible"
|
|
138
|
-
)
|
|
219
|
+
) {
|
|
139
220
|
Object.keys(state)
|
|
140
221
|
.filter((key) => value.includes(key))
|
|
141
|
-
.forEach((val) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
222
|
+
.forEach((val) => {
|
|
223
|
+
const b = {
|
|
224
|
+
el,
|
|
225
|
+
prop: "hidden",
|
|
226
|
+
expr: value,
|
|
227
|
+
fn: compileExpression(
|
|
228
|
+
`${name == "@hidden" || name == "data-he-hidden" ? "!" : ""}!(${value})`,
|
|
229
|
+
true,
|
|
230
|
+
),
|
|
231
|
+
};
|
|
232
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
233
|
+
newlyAddedBindings.push(b);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (name.startsWith(":")) {
|
|
156
238
|
Object.keys(state)
|
|
157
239
|
.filter((key) => value.includes(key))
|
|
158
|
-
.forEach((val) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
240
|
+
.forEach((val) => {
|
|
241
|
+
const b = {
|
|
242
|
+
el,
|
|
243
|
+
prop: name.split(":")[1],
|
|
244
|
+
expr: value,
|
|
245
|
+
fn: compileExpression(value, true),
|
|
246
|
+
};
|
|
247
|
+
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
248
|
+
newlyAddedBindings.push(b);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (name == "@init" || name == "data-he-init") {
|
|
170
253
|
compileExpression(value, false)(
|
|
171
254
|
$,
|
|
172
255
|
state,
|
|
173
256
|
undefined,
|
|
174
257
|
el,
|
|
175
258
|
html,
|
|
176
|
-
|
|
259
|
+
get,
|
|
260
|
+
post,
|
|
261
|
+
patch,
|
|
262
|
+
del,
|
|
263
|
+
...Object.values(data),
|
|
177
264
|
...[...refs.values()],
|
|
178
265
|
);
|
|
179
|
-
else if (name.startsWith("@") || name.startsWith("data-he-on")) {
|
|
266
|
+
} else if (name.startsWith("@") || name.startsWith("data-he-on")) {
|
|
267
|
+
// event handling — keep this as before (register listeners)
|
|
180
268
|
const [eventName, ...modifiers] = name
|
|
181
269
|
.slice(name.startsWith("@") ? 1 : 10)
|
|
182
270
|
.split(".");
|
|
@@ -230,7 +318,11 @@ export default function helium(data = {}) {
|
|
|
230
318
|
e,
|
|
231
319
|
el,
|
|
232
320
|
html,
|
|
233
|
-
|
|
321
|
+
get,
|
|
322
|
+
post,
|
|
323
|
+
patch,
|
|
324
|
+
del,
|
|
325
|
+
...Object.values(data),
|
|
234
326
|
...[...refs.values()],
|
|
235
327
|
);
|
|
236
328
|
}
|
|
@@ -240,21 +332,33 @@ export default function helium(data = {}) {
|
|
|
240
332
|
|
|
241
333
|
const debouncedHandler =
|
|
242
334
|
debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
243
|
-
|
|
244
335
|
receiver.addEventListener(eventName, debouncedHandler);
|
|
245
336
|
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
337
|
+
} // end attributes loop
|
|
338
|
+
}); // end heliumElements.forEach
|
|
339
|
+
|
|
340
|
+
return newlyAddedBindings;
|
|
341
|
+
} // end processElements
|
|
342
|
+
|
|
343
|
+
// set up mutation observer that only processes and applies bindings for newly added nodes
|
|
344
|
+
const observer = new MutationObserver((mutations) => {
|
|
345
|
+
if (isUpdatingDOM) return; // ignore mutations we caused directly
|
|
346
|
+
for (const m of mutations) {
|
|
347
|
+
for (const node of m.addedNodes) {
|
|
348
|
+
if (
|
|
349
|
+
node.nodeType === 1 &&
|
|
350
|
+
!node.hasAttribute("data-helium-processed")
|
|
351
|
+
) {
|
|
352
|
+
const newly = processElements(node);
|
|
353
|
+
// apply only the bindings we just created for this node
|
|
354
|
+
newly.forEach(applyBinding);
|
|
252
355
|
}
|
|
253
356
|
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
processElements(root);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
360
|
+
|
|
361
|
+
const initialBindings = processElements(root);
|
|
362
|
+
for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
|
|
259
363
|
}
|
|
260
364
|
helium();
|