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