@daz4126/helium 0.5.2 → 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 +174 -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
|
@@ -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(
|
|
@@ -29,12 +55,25 @@ export default function helium(data = {}) {
|
|
|
29
55
|
event,
|
|
30
56
|
elCtx,
|
|
31
57
|
html,
|
|
32
|
-
|
|
58
|
+
get,
|
|
59
|
+
post,
|
|
60
|
+
patch,
|
|
61
|
+
del,
|
|
62
|
+
...Object.values(data),
|
|
33
63
|
...[...refs.values()],
|
|
34
64
|
);
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
|
37
75
|
}
|
|
76
|
+
|
|
38
77
|
function compileExpression(expr, withReturn = false) {
|
|
39
78
|
try {
|
|
40
79
|
return new Function(
|
|
@@ -43,6 +82,10 @@ export default function helium(data = {}) {
|
|
|
43
82
|
"$event",
|
|
44
83
|
"$el",
|
|
45
84
|
"$html",
|
|
85
|
+
"$get",
|
|
86
|
+
"$post",
|
|
87
|
+
"$patch",
|
|
88
|
+
"$delete",
|
|
46
89
|
...Object.keys(data),
|
|
47
90
|
...[...refs.keys()],
|
|
48
91
|
`with($data) { ${withReturn ? "return" : ""} (${expr.trim()}) }`,
|
|
@@ -51,7 +94,23 @@ export default function helium(data = {}) {
|
|
|
51
94
|
return () => expr;
|
|
52
95
|
}
|
|
53
96
|
}
|
|
97
|
+
|
|
98
|
+
// processElements now returns an array of newly-created binding objects
|
|
54
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
|
+
|
|
55
114
|
const heliumElements = [
|
|
56
115
|
element,
|
|
57
116
|
...Array.from(element.querySelectorAll("*")).filter((el) =>
|
|
@@ -60,6 +119,7 @@ export default function helium(data = {}) {
|
|
|
60
119
|
),
|
|
61
120
|
),
|
|
62
121
|
];
|
|
122
|
+
|
|
63
123
|
const xpath = document.evaluate(
|
|
64
124
|
".//*[@*[starts-with(name(), '@') or starts-with(name(), ':')]]",
|
|
65
125
|
element,
|
|
@@ -69,8 +129,10 @@ export default function helium(data = {}) {
|
|
|
69
129
|
);
|
|
70
130
|
for (let i = 0; i < xpath.snapshotLength; i++)
|
|
71
131
|
heliumElements.push(xpath.snapshotItem(i));
|
|
132
|
+
|
|
133
|
+
// seed default state values (no binding creation here)
|
|
72
134
|
heliumElements.forEach((el) => {
|
|
73
|
-
for (const { name, value } of el.attributes) {
|
|
135
|
+
for (const { name, value } of el.attributes || []) {
|
|
74
136
|
if (
|
|
75
137
|
name == "@text" ||
|
|
76
138
|
name == "data-he-text" ||
|
|
@@ -87,9 +149,11 @@ export default function helium(data = {}) {
|
|
|
87
149
|
}
|
|
88
150
|
}
|
|
89
151
|
});
|
|
152
|
+
|
|
153
|
+
// Register bindings for these elements and collect the newly added binding objects
|
|
90
154
|
heliumElements.forEach((el) => {
|
|
91
|
-
for (const { name, value } of el.attributes) {
|
|
92
|
-
if (name == "@data" || name == "data-he")
|
|
155
|
+
for (const { name, value } of el.attributes || []) {
|
|
156
|
+
if (name == "@data" || name == "data-he") {
|
|
93
157
|
Object.assign(
|
|
94
158
|
state,
|
|
95
159
|
compileExpression(value, true)(
|
|
@@ -98,85 +162,107 @@ export default function helium(data = {}) {
|
|
|
98
162
|
{},
|
|
99
163
|
el,
|
|
100
164
|
html,
|
|
101
|
-
|
|
165
|
+
get,
|
|
166
|
+
post,
|
|
167
|
+
patch,
|
|
168
|
+
del,
|
|
169
|
+
...Object.values(data),
|
|
102
170
|
...[...refs.values()],
|
|
103
171
|
),
|
|
104
172
|
);
|
|
173
|
+
}
|
|
105
174
|
if (name == "@ref" || name == "data-he-ref") refs.set("$" + value, el);
|
|
106
|
-
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
name == "@text" ||
|
|
178
|
+
name == "data-he-text" ||
|
|
179
|
+
name == "@html" ||
|
|
180
|
+
name == "data-he-html"
|
|
181
|
+
) {
|
|
107
182
|
Object.keys(state)
|
|
108
183
|
.filter((key) => value.includes(key))
|
|
109
|
-
.forEach((val) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
|
|
120
199
|
if (name == "@bind" || name == "data-he-bind") {
|
|
121
200
|
el.addEventListener("input", (e) => (state[value] = e.target.value));
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
]);
|
|
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);
|
|
131
209
|
el.value = state[value];
|
|
132
210
|
}
|
|
211
|
+
|
|
133
212
|
if (
|
|
134
213
|
name == "@hidden" ||
|
|
135
214
|
name == "@visible" ||
|
|
136
215
|
name == "data-he-hidden" ||
|
|
137
216
|
name == "data-he-visible"
|
|
138
|
-
)
|
|
217
|
+
) {
|
|
139
218
|
Object.keys(state)
|
|
140
219
|
.filter((key) => value.includes(key))
|
|
141
|
-
.forEach((val) =>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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(":")) {
|
|
156
236
|
Object.keys(state)
|
|
157
237
|
.filter((key) => value.includes(key))
|
|
158
|
-
.forEach((val) =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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") {
|
|
170
251
|
compileExpression(value, false)(
|
|
171
252
|
$,
|
|
172
253
|
state,
|
|
173
254
|
undefined,
|
|
174
255
|
el,
|
|
175
256
|
html,
|
|
176
|
-
|
|
257
|
+
get,
|
|
258
|
+
post,
|
|
259
|
+
patch,
|
|
260
|
+
del,
|
|
261
|
+
...Object.values(data),
|
|
177
262
|
...[...refs.values()],
|
|
178
263
|
);
|
|
179
|
-
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)
|
|
180
266
|
const [eventName, ...modifiers] = name
|
|
181
267
|
.slice(name.startsWith("@") ? 1 : 10)
|
|
182
268
|
.split(".");
|
|
@@ -230,7 +316,11 @@ export default function helium(data = {}) {
|
|
|
230
316
|
e,
|
|
231
317
|
el,
|
|
232
318
|
html,
|
|
233
|
-
|
|
319
|
+
get,
|
|
320
|
+
post,
|
|
321
|
+
patch,
|
|
322
|
+
del,
|
|
323
|
+
...Object.values(data),
|
|
234
324
|
...[...refs.values()],
|
|
235
325
|
);
|
|
236
326
|
}
|
|
@@ -240,21 +330,33 @@ export default function helium(data = {}) {
|
|
|
240
330
|
|
|
241
331
|
const debouncedHandler =
|
|
242
332
|
debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
243
|
-
|
|
244
333
|
receiver.addEventListener(eventName, debouncedHandler);
|
|
245
334
|
}
|
|
246
|
-
}
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
252
353
|
}
|
|
253
354
|
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
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);
|
|
259
361
|
}
|
|
260
362
|
helium();
|