@daz4126/helium 0.23.0 → 0.25.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 +18 -58
- package/helium.js +69 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,11 +18,10 @@ It's really simple to use - just sprinkle the magic @attributes into your HTML a
|
|
|
18
18
|
|
|
19
19
|
Helium is designed for developers who want:
|
|
20
20
|
|
|
21
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **Progressive enhancement** - Add interactivity gradually where you need it
|
|
21
|
+
- **Lightweight** - Just over 3KB minified and gzipped
|
|
22
|
+
- **Powerful** - Declarative JavaScript in your HTML
|
|
23
|
+
- **Zero build step** Works directly in the browser with no compiling
|
|
24
|
+
- **Easy to learn** If you know HTML and basic JavaScript, you're ready
|
|
26
25
|
|
|
27
26
|
## Installation
|
|
28
27
|
|
|
@@ -33,7 +32,6 @@ Just import from the CDN in a script tag directly in your HTML page:
|
|
|
33
32
|
```html
|
|
34
33
|
<script type="module">
|
|
35
34
|
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';
|
|
36
|
-
helium();
|
|
37
35
|
</script>
|
|
38
36
|
```
|
|
39
37
|
|
|
@@ -47,7 +45,6 @@ Then include it in your JavaScript file and call the helium() function:
|
|
|
47
45
|
|
|
48
46
|
```javascript
|
|
49
47
|
import helium from "@daz4126/helium"
|
|
50
|
-
helium()
|
|
51
48
|
```
|
|
52
49
|
|
|
53
50
|
### Automatic Initialization
|
|
@@ -705,34 +702,16 @@ Object containing all elements marked with `@ref` (prefixed with `$`):
|
|
|
705
702
|
<button @click="console.log($username.value)">Log Username</button>
|
|
706
703
|
```
|
|
707
704
|
|
|
708
|
-
##
|
|
705
|
+
## Functions
|
|
709
706
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
### Setting Default Values
|
|
713
|
-
|
|
714
|
-
The following will set the count variable to an initial value of 29 and the name variable to "Helium":
|
|
715
|
-
|
|
716
|
-
```javascript
|
|
717
|
-
helium({
|
|
718
|
-
count: 29,
|
|
719
|
-
name: "Helium"
|
|
720
|
-
})
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
These variables will be available in all Helium expressions:
|
|
724
|
-
|
|
725
|
-
```html
|
|
726
|
-
<p @text="count"></p> <!-- Shows: 29 -->
|
|
727
|
-
<p @text="name"></p> <!-- Shows: Helium -->
|
|
728
|
-
```
|
|
707
|
+
Functions can be imported using `@import` or defined using the `@data` attribute.
|
|
729
708
|
|
|
730
709
|
### Adding Functions
|
|
731
710
|
|
|
732
711
|
You can add functions that can be called from event handlers and other expressions:
|
|
733
712
|
|
|
734
|
-
```
|
|
735
|
-
|
|
713
|
+
```html
|
|
714
|
+
@data= "{
|
|
736
715
|
appendTo(element) {
|
|
737
716
|
const li = document.createElement("li")
|
|
738
717
|
li.textContent = "New Item"
|
|
@@ -745,7 +724,7 @@ helium({
|
|
|
745
724
|
currency: 'USD'
|
|
746
725
|
}).format(amount)
|
|
747
726
|
}
|
|
748
|
-
}
|
|
727
|
+
}"
|
|
749
728
|
```
|
|
750
729
|
|
|
751
730
|
Using these functions:
|
|
@@ -765,12 +744,12 @@ Using these functions:
|
|
|
765
744
|
|
|
766
745
|
❌ **This won't work as expected:**
|
|
767
746
|
|
|
768
|
-
```
|
|
769
|
-
|
|
747
|
+
```html
|
|
748
|
+
@data = "{
|
|
770
749
|
increment(n = 1) {
|
|
771
750
|
count += n // 'count' is not defined in this scope
|
|
772
751
|
}
|
|
773
|
-
}
|
|
752
|
+
}"
|
|
774
753
|
```
|
|
775
754
|
|
|
776
755
|
```html
|
|
@@ -780,12 +759,12 @@ helium({
|
|
|
780
759
|
✅ **Instead, pass variables as arguments:**
|
|
781
760
|
|
|
782
761
|
**Option 1: Pass specific variables**
|
|
783
|
-
```
|
|
784
|
-
|
|
762
|
+
```html
|
|
763
|
+
@data = "{
|
|
785
764
|
increment(currentCount, n = 1) {
|
|
786
765
|
return currentCount + n
|
|
787
766
|
}
|
|
788
|
-
}
|
|
767
|
+
}"
|
|
789
768
|
```
|
|
790
769
|
|
|
791
770
|
```html
|
|
@@ -796,8 +775,8 @@ helium({
|
|
|
796
775
|
|
|
797
776
|
This is the recommended approach when you need to update variables:
|
|
798
777
|
|
|
799
|
-
```
|
|
800
|
-
|
|
778
|
+
```html
|
|
779
|
+
@data = "{
|
|
801
780
|
increment(data, n = 1) {
|
|
802
781
|
data.count += n // Will trigger reactivity
|
|
803
782
|
},
|
|
@@ -807,7 +786,7 @@ helium({
|
|
|
807
786
|
data.name = ''
|
|
808
787
|
data.items = []
|
|
809
788
|
}
|
|
810
|
-
}
|
|
789
|
+
}"
|
|
811
790
|
```
|
|
812
791
|
|
|
813
792
|
```html
|
|
@@ -829,7 +808,6 @@ If you include [Idiomorph](https://github.com/bigskysoftware/idiomorph), Helium
|
|
|
829
808
|
<script src="https://unpkg.com/idiomorph@0.3.0/dist/idiomorph.min.js"></script>
|
|
830
809
|
<script type="module">
|
|
831
810
|
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';
|
|
832
|
-
helium();
|
|
833
811
|
</script>
|
|
834
812
|
```
|
|
835
813
|
|
|
@@ -891,24 +869,6 @@ Helium automatically integrates with Turbo Drive:
|
|
|
891
869
|
|
|
892
870
|
No additional configuration needed - just use Helium with Turbo normally.
|
|
893
871
|
|
|
894
|
-
### Manual Cleanup
|
|
895
|
-
|
|
896
|
-
If you need to manually clean up Helium (for example, when unmounting a section of your page):
|
|
897
|
-
|
|
898
|
-
```javascript
|
|
899
|
-
window.heliumTeardown()
|
|
900
|
-
```
|
|
901
|
-
|
|
902
|
-
This will:
|
|
903
|
-
- Disconnect the MutationObserver
|
|
904
|
-
- Remove all event listeners
|
|
905
|
-
- Clear all internal state
|
|
906
|
-
|
|
907
|
-
To reinitialize after teardown:
|
|
908
|
-
```javascript
|
|
909
|
-
helium()
|
|
910
|
-
```
|
|
911
|
-
|
|
912
872
|
## Security Considerations
|
|
913
873
|
|
|
914
874
|
### XSS Prevention
|
package/helium.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
const parseEx=v=>{try{return Function(`return(${v})`)()}catch{return v}}
|
|
2
|
-
const
|
|
2
|
+
const INPUT_EVENTS = { form: "submit", input: "input", textarea:"input", select:"change" };
|
|
3
|
+
const getEvent = el => INPUT_EVENTS[el.tagName.toLowerCase()] || "click";
|
|
3
4
|
const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
|
|
4
5
|
|
|
5
6
|
// Single global object to hold all settings
|
|
6
7
|
let HELIUM = null;
|
|
7
8
|
|
|
8
|
-
window.helium = function() {
|
|
9
|
+
window.helium = async function() {
|
|
9
10
|
let initFn;
|
|
10
11
|
const ALL = Symbol("all");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const HE_ATTR_REGEX = /^(@|:|data-he)/;
|
|
13
|
+
const he = (n, ...a) => {
|
|
14
|
+
const prefix = n.split(/[.:]/)[0];
|
|
15
|
+
if (prefix === ":" || prefix === "") return false;
|
|
16
|
+
for (const attr of a) {
|
|
17
|
+
if (prefix === `@${attr}` || prefix === `data-he-${attr}`) return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
};
|
|
16
21
|
const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
|
|
17
22
|
|
|
18
23
|
// Initialize or reuse HELIUM object
|
|
@@ -34,7 +39,7 @@ window.helium = function() {
|
|
|
34
39
|
|
|
35
40
|
const update = (data,targets,actions,template) => {
|
|
36
41
|
const newTargets = [];
|
|
37
|
-
targets
|
|
42
|
+
targets.forEach((target,i) => {
|
|
38
43
|
const element = target instanceof Node ? target : (HELIUM.refs.get(target.trim()) || $(target.trim()));
|
|
39
44
|
if(element){
|
|
40
45
|
const content = template ? template(data) : data;
|
|
@@ -46,7 +51,7 @@ window.helium = function() {
|
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
const ajax = (url,method,options={},params={}) => {
|
|
49
|
-
if(options.loading
|
|
54
|
+
if(options.loading) options.target = update(options.loading,options.target,options.action) || options.target;
|
|
50
55
|
const fd = params instanceof FormData, token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
51
56
|
const path = new URL(url, window.location.href);
|
|
52
57
|
const sameOrigin = path.origin === window.location.origin;
|
|
@@ -66,7 +71,7 @@ fetch(url, {
|
|
|
66
71
|
type.includes("json") ? res.json() :
|
|
67
72
|
res.text());
|
|
68
73
|
}).then(data =>
|
|
69
|
-
data.turbo &&
|
|
74
|
+
(data.turbo && window.Turbo)
|
|
70
75
|
? Turbo.renderStreamMessage(data.data)
|
|
71
76
|
: update(data, options.target, options.loading ? options.action.map(a => a && "replace") : options.action, options.template)
|
|
72
77
|
).catch(e => console.error("AJAX:", e.message));
|
|
@@ -103,7 +108,7 @@ const handler = {
|
|
|
103
108
|
|
|
104
109
|
function applyBinding(b,e={},elCtx=b.el){
|
|
105
110
|
const {el,prop,fn,calc}=b;
|
|
106
|
-
const r=fn($,state,e,elCtx,html
|
|
111
|
+
const r=fn($,state,e,elCtx,html,get,post,put,patch,del);
|
|
107
112
|
if (calc) state[calc] = r
|
|
108
113
|
|
|
109
114
|
if (prop==="innerHTML") {
|
|
@@ -117,7 +122,10 @@ function applyBinding(b,e={},elCtx=b.el){
|
|
|
117
122
|
k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
|
|
118
123
|
|
|
119
124
|
if (prop==="style" && r && typeof r==="object")
|
|
120
|
-
return el.style.cssText = Object.entries(r)
|
|
125
|
+
return el.style.cssText = Object.entries(r)
|
|
126
|
+
.filter(([_, v]) => v != null && v !== false)
|
|
127
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
128
|
+
.join("; ");
|
|
121
129
|
|
|
122
130
|
if (prop in el) {
|
|
123
131
|
if (el.type === "radio" && prop != "checked") el.checked = el.value===r;
|
|
@@ -134,7 +142,6 @@ const compile = (expr, withReturn = false) => {
|
|
|
134
142
|
try {
|
|
135
143
|
const fn = new Function(
|
|
136
144
|
"$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
|
|
137
|
-
...Object.keys(state), ...[...HELIUM.refs.keys()],
|
|
138
145
|
withReturn
|
|
139
146
|
? `with($data){return(${expr.trim()})}`
|
|
140
147
|
: `with($data){${expr.trim()}}`
|
|
@@ -142,17 +149,7 @@ const compile = (expr, withReturn = false) => {
|
|
|
142
149
|
HELIUM.fnCache.set(key, fn);
|
|
143
150
|
return fn;
|
|
144
151
|
} catch {
|
|
145
|
-
|
|
146
|
-
const fn = new Function(
|
|
147
|
-
"$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
|
|
148
|
-
...Object.keys(state), ...[...HELIUM.refs.keys()],
|
|
149
|
-
`with($data){${expr.trim()}}`
|
|
150
|
-
);
|
|
151
|
-
HELIUM.fnCache.set(key, fn);
|
|
152
|
-
return fn;
|
|
153
|
-
} catch {
|
|
154
|
-
return () => expr;
|
|
155
|
-
}
|
|
152
|
+
return () => expr;
|
|
156
153
|
}
|
|
157
154
|
};
|
|
158
155
|
|
|
@@ -173,30 +170,56 @@ const trackDependencies = (fn, el, excludeChanged = false) => {
|
|
|
173
170
|
return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
|
|
174
171
|
};
|
|
175
172
|
|
|
176
|
-
|
|
173
|
+
const cleanup = el => {
|
|
177
174
|
[el,...el.querySelectorAll('*')].forEach(e => {
|
|
178
175
|
HELIUM.listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
|
|
179
176
|
HELIUM.listeners.delete(e);
|
|
180
177
|
});
|
|
181
178
|
};
|
|
182
179
|
|
|
183
|
-
function processElements(element) {
|
|
180
|
+
async function processElements(element) {
|
|
184
181
|
const newBindings = [];
|
|
185
182
|
const deferredBindings = [];
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
const heElements = [element, ...element.querySelectorAll("*")].filter(e => {
|
|
185
|
+
if (HELIUM.processed.has(e)) return false;
|
|
186
|
+
for (let i = 0; i < e.attributes.length; i++) {
|
|
187
|
+
if (HE_ATTR_REGEX.test(e.attributes[i].name)) return true;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
});
|
|
189
191
|
|
|
190
192
|
const addBinding = (val, b) => {
|
|
191
193
|
HELIUM.bindings.set(val, b.calc ? [b,...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
|
|
192
194
|
b.calc ? newBindings.unshift(b) : newBindings.push(b);
|
|
193
195
|
};
|
|
196
|
+
|
|
197
|
+
const importPromises = [];
|
|
198
|
+
|
|
199
|
+
heElements.forEach(el => {
|
|
200
|
+
const importAttr = el.getAttribute("@import") || el.getAttribute("data-he-import");
|
|
201
|
+
if (importAttr) {
|
|
202
|
+
importAttr.split(",").map(m => m.trim()).forEach(moduleName => {
|
|
203
|
+
importPromises.push(
|
|
204
|
+
import(`helium_modules/${moduleName}`)
|
|
205
|
+
.then(module => {
|
|
206
|
+
Object.keys(module).forEach(key => state[key] = module[key]);
|
|
207
|
+
})
|
|
208
|
+
.catch(error => {
|
|
209
|
+
console.error(`Failed to import module: ${moduleName}`, error.message);
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (importPromises.length > 0) await Promise.all(importPromises);
|
|
194
217
|
|
|
195
218
|
heElements.forEach(el => {
|
|
196
219
|
HELIUM.processed.add(el);
|
|
197
220
|
|
|
198
221
|
const attrs = el.attributes;
|
|
199
|
-
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del
|
|
222
|
+
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del);
|
|
200
223
|
const inputType = el.type?.toLowerCase();
|
|
201
224
|
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
202
225
|
|
|
@@ -216,7 +239,7 @@ function processElements(element) {
|
|
|
216
239
|
}
|
|
217
240
|
|
|
218
241
|
// Process the attribute
|
|
219
|
-
if (
|
|
242
|
+
if (name === "@data" || name === "data-he") {
|
|
220
243
|
Object.assign(state, parseEx(value));
|
|
221
244
|
}
|
|
222
245
|
else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
|
|
@@ -249,9 +272,6 @@ function processElements(element) {
|
|
|
249
272
|
else if (he(name, "effect")) {
|
|
250
273
|
deferredBindings.push({el, prop: null, fn: compile(value, true), keys: name.split(":").slice(1)});
|
|
251
274
|
}
|
|
252
|
-
else if (he(name, "import")) {
|
|
253
|
-
value.split(",").map(s => s.trim()).forEach(v => state[v] = window[v]);
|
|
254
|
-
}
|
|
255
275
|
else if (he(name, "init")) {
|
|
256
276
|
initFn = compile(value, true);
|
|
257
277
|
}
|
|
@@ -264,7 +284,7 @@ function processElements(element) {
|
|
|
264
284
|
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
265
285
|
const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
|
|
266
286
|
const _handler = e => {
|
|
267
|
-
const exFn = v => compile(v,true)($, state, e, el, html, get, post, put, patch, del
|
|
287
|
+
const exFn = v => compile(v,true)($, state, e, el, html, get, post, put, patch, del)
|
|
268
288
|
if (mods.includes("prevent")) e.preventDefault();
|
|
269
289
|
const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
|
|
270
290
|
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
@@ -278,9 +298,9 @@ function processElements(element) {
|
|
|
278
298
|
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
279
299
|
if (isHttpMethod) {
|
|
280
300
|
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
281
|
-
const pairs = getAttr('target')
|
|
282
|
-
const target = pairs
|
|
283
|
-
const action = pairs
|
|
301
|
+
const pairs = (getAttr('target') || "").split(",").map(p => p.split(":").map(s => s.trim()));
|
|
302
|
+
const target = pairs.map(([target]) => target);
|
|
303
|
+
const action = pairs.map(([, action]) => action);
|
|
284
304
|
const options = {
|
|
285
305
|
...(getAttr("options") && parseEx(getAttr("options") || "{}")),
|
|
286
306
|
...(target && { target }),
|
|
@@ -318,17 +338,23 @@ const action = pairs?.map(([, action]) => action);
|
|
|
318
338
|
}
|
|
319
339
|
|
|
320
340
|
// Create new observer
|
|
321
|
-
HELIUM.observer = new MutationObserver(ms => {
|
|
341
|
+
HELIUM.observer = new MutationObserver(async (ms) => {
|
|
322
342
|
for (const m of ms) {
|
|
323
|
-
m.removedNodes.forEach(n => n.nodeType === 1 && cleanup(n));
|
|
324
|
-
|
|
343
|
+
m.removedNodes.forEach((n) => n.nodeType === 1 && cleanup(n));
|
|
344
|
+
|
|
345
|
+
for (const n of m.addedNodes) {
|
|
346
|
+
if (n.nodeType === 1 && !HELIUM.processed.has(n)) {
|
|
347
|
+
const bindings = await processElements(n);
|
|
348
|
+
bindings?.forEach(applyBinding);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
325
351
|
}
|
|
326
352
|
});
|
|
327
353
|
HELIUM.observer.observe(root, { childList: true, subtree: true });
|
|
328
354
|
|
|
329
|
-
processElements(root);
|
|
330
|
-
|
|
331
|
-
if(initFn) initFn($, state, {}, {}, html, get, post, put, patch, del
|
|
355
|
+
const initialBindings = await processElements(root);
|
|
356
|
+
initialBindings.forEach(applyBinding);
|
|
357
|
+
if(initFn) initFn($, state, {}, {}, html, get, post, put, patch, del)
|
|
332
358
|
}
|
|
333
359
|
|
|
334
360
|
window.heliumTeardown = function() {
|