@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.
Files changed (3) hide show
  1. package/README.md +18 -58
  2. package/helium.js +69 -43
  3. 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
- - **Zero build step** - Works directly in the browser with a simple script tag
22
- - **Minimal learning curve** - If you know HTML and basic JavaScript, you're ready
23
- - **Ultra-lightweight** - Under 3KB minified and gzipped
24
- - **Template-first** - Your HTML is the source of truth, not JavaScript
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
- ## Default Variables and Functions
705
+ ## Functions
709
706
 
710
- The helium function accepts a single JavaScript object as an argument. This can include default variable values and functions that can then be called inside the JavaScript expressions.
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
- ```javascript
735
- helium({
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
- ```javascript
769
- helium({
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
- ```javascript
784
- helium({
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
- ```javascript
800
- helium({
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 getEvent = el => ({form:"submit",input:"input",textarea:"input",select:"change"}[el.tagName.toLowerCase()]||"click")
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 he = (n,...a) => {
12
- const prefix = n.split(/[.:]/)[0];
13
- if (prefix === ":" || prefix === "") return false;
14
- return a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${prefix}|`);
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?.forEach((target,i) => {
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 && options.target) options.target = update(options.loading,options.target,options.action) || options.target;
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 && "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,...Object.values(state),...[...HELIUM.refs.values()]);
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).map(([k,v])=>v?`${k}:${v}`:'').join(";");
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
- try {
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
- const cleanup = el => {
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
- const heElements = [element, ...element.querySelectorAll("*")]
188
- .filter(e => !HELIUM.processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
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, ...Object.values(state), ...[...HELIUM.refs.values()]);
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 (["@data", "data-he"].includes(name)) {
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, ...Object.values(state), ...[...HELIUM.refs.values()])
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')?.split(",")?.map(p => p.split(":").map(s => s.trim()));
282
- const target = pairs?.map(([target]) => target);
283
- const action = pairs?.map(([, action]) => action);
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
- m.addedNodes.forEach(n => n.nodeType === 1 && !HELIUM.processed.has(n) && processElements(n).forEach(applyBinding));
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
- for (const [key, items] of HELIUM.bindings.entries()) items.forEach(applyBinding);
331
- if(initFn) initFn($, state, {}, {}, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()])
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {