@daz4126/helium 0.16.0 β†’ 0.17.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 +222 -67
  2. package/helium.js +141 -104
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 🎈Helium🎈
1
+ # 🎈 Helium 🎈
2
2
 
3
3
  The ultra-light library that makes HTML interactive!
4
4
 
@@ -14,90 +14,104 @@ 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 in a script tag directly in your HTML page (no install or build step required!):
17
+ ## Installation
18
+
19
+ ### CDN (No build step required!)
20
+
21
+ Just import from the CDN in a script tag directly in your HTML page:
18
22
 
19
23
  ```html
20
- <script type="module">import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';helium();</script>
24
+ <script type="module">
25
+ import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';
26
+ helium();
27
+ </script>
21
28
  ```
22
29
 
23
- Alernatively you can install from NPM:
30
+ ### NPM
24
31
 
25
32
  ```bash
26
33
  npm install @daz4126/helium
27
34
  ```
28
35
 
29
- Then include it in your JavaScript file and call the `helium()` function:
36
+ Then include it in your JavaScript file and call the helium() function:
30
37
 
31
38
  ```javascript
32
39
  import helium from "@daz4126/helium"
33
40
  helium()
34
41
  ```
35
42
 
36
- # Helium Attributes
37
-
38
- Helium uses custom attributes to add interactivity to HTML elements. To identify them, they all start with `@`, although there are also `data` attribute aliases that can be used instead.
43
+ ## Helium Attributes
39
44
 
40
- ## `@helium`
45
+ Helium uses custom attributes to add interactivity to HTML elements. To identify them, they all start with `@`, although there are also data attribute aliases that can be used instead.
41
46
 
42
- This attribute sets the root element. Helium attributes can only be used on this element and its children. If not set then it defaults to `document`.
47
+ ### @helium
43
48
 
44
- Alias: `data-helium`
49
+ This attribute sets the root element. Helium attributes can only be used on this element and its children. If not set then it defaults to `document.body`.
45
50
 
46
- ## `@text`
51
+ **Alias:** `data-helium`
47
52
 
48
- Inserts the result of a JavaScript expression into the text-content of the element.
53
+ ### @text
49
54
 
50
- This will update the textContent of the element with the value of the `count` variable:
55
+ Inserts the result of a JavaScript expression into the text-content of the element. This will update the textContent of the element with the value of the count variable:
51
56
 
52
57
  ```html
53
58
  <b @text="count">0</b>
54
59
  ```
55
60
 
56
- You can also use expresions. This will update the textContent of the element with the value of the `name` variable but in uppercase:
61
+ You can also use expressions. This will update the textContent of the element with the value of the name variable but in uppercase:
57
62
 
58
63
  ```html
59
- <span @text="name.toUpperCase()">0</span>
64
+ <span @text="name.toUpperCase()">Dave</span>
60
65
  ```
61
66
 
62
- Alias: `data-he-text`
67
+ **Alias:** `data-he-text`
63
68
 
64
- ## `@bind`
69
+ ### @html
65
70
 
66
- Creates a 2-way binding between an input or textcontent element's value attribute and a variable.
71
+ Similar to `@text`, but inserts HTML content into the element's innerHTML. Supports arrays, objects, and DOM morphing with Idiomorph if available.
67
72
 
68
- Whatever is entered in the following input field will be stored as a variable called `name`:
73
+ ```html
74
+ <div @html="'<strong>Bold text</strong>'"></div>
75
+ ```
76
+
77
+ **Alias:** `data-he-html`
78
+
79
+ ### @bind
80
+
81
+ Creates a 2-way binding between an input element's value and a variable. Whatever is entered in the following input field will be stored as a variable called name:
69
82
 
70
83
  ```html
71
84
  <input @bind="name" placeholder="Enter your name">
72
85
  ```
73
86
 
74
- Alias: `data-he-bind`
87
+ Works with text inputs, textareas, checkboxes, radio buttons, and select elements.
88
+
89
+ **Alias:** `data-he-bind`
75
90
 
76
- ## `@hidden` & `@visible`
91
+ ### @hidden & @visible
77
92
 
78
93
  Makes the element hidden or visible depending on the result of a JavaScript expression.
79
94
 
80
95
  ```html
81
96
  <div @visible="count > 3">Only visible if the count is greater than 3</div>
97
+ <div @hidden="count <= 3">Hidden when count is 3 or less</div>
82
98
  ```
83
99
 
84
- Alias: `data-he-hidden` & `data-he-visible`
100
+ **Alias:** `data-he-hidden` & `data-he-visible`
85
101
 
86
- ## `@data`
102
+ ### @data
87
103
 
88
- Initializes variables that can be used in JacaScript expressions.
104
+ Initializes variables that can be used in JavaScript expressions.
89
105
 
90
106
  ```html
91
107
  <div @data="{ count: 0, open: false }"></div>
92
108
  ```
93
109
 
94
- Alias: `data-he-data`
110
+ **Alias:** `data-he-data`
95
111
 
96
- ## `@ref`
112
+ ### @ref
97
113
 
98
- Creates a reference to the element that can be used in JavaScript expressions.
99
-
100
- For example, this will create a reference called `$list` to this element:
114
+ Creates a reference to the element that can be used in JavaScript expressions. For example, this will create a reference called `$list` to this element:
101
115
 
102
116
  ```html
103
117
  <ul @ref="list"></ul>
@@ -109,9 +123,9 @@ This element can then be accessed in other JavaScript expressions as `$list`, fo
109
123
  <button @click="appendTo($list)">Add Task</button>
110
124
  ```
111
125
 
112
- Alias: `data-he-ref`
126
+ **Alias:** `data-he-ref`
113
127
 
114
- ## `@init`
128
+ ### @init
115
129
 
116
130
  A JavaScript expression that will run once when Helium initializes.
117
131
 
@@ -119,84 +133,195 @@ A JavaScript expression that will run once when Helium initializes.
119
133
  <div @init="timestamp = Date.now()"></div>
120
134
  ```
121
135
 
122
- Alias: `data-he-init`
136
+ **Alias:** `data-he-init`
137
+
138
+ ### @calculate
139
+
140
+ Creates a computed property that automatically updates when its dependencies change. The calculated value is stored in a state variable.
141
+
142
+ ```html
143
+ <div @calculate:total="price * quantity"></div>
144
+ ```
145
+
146
+ This will create a `total` variable that automatically recalculates whenever `price` or `quantity` changes.
147
+
148
+ **Alias:** `data-he-calculate`
149
+
150
+ ### @effect
151
+
152
+ Runs a side effect whenever specified dependencies change. Use `:*` to run on any state change, or list specific dependencies.
153
+
154
+ ```html
155
+ <!-- Run on any state change -->
156
+ <div @effect:*="console.log('State changed')"></div>
157
+
158
+ <!-- Run when specific variables change -->
159
+ <div @effect:count:name="console.log('Count or name changed')"></div>
160
+ ```
161
+
162
+ **Alias:** `data-he-effect`
123
163
 
124
164
  ## Event Listeners & Handlers
125
165
 
126
- Event listeners and handlers can be created by prepending `@` before the event name, for example `@click="count++"` will run the cound `count++` when the element is clicked on.
166
+ Event listeners and handlers can be created by prepending `@` before the event name, for example `@click="count++"` will run the code `count++` when the element is clicked on.
127
167
 
128
168
  ```html
129
169
  <button @click="count++">Increment</button>
130
170
  ```
131
171
 
132
- You can add modifiers of `prevent` to prevent the default behaviour, `outside` to only fire when the event happens outside the element and `once` to only run the event handler once.
172
+ ### Event Modifiers
173
+
174
+ You can add modifiers by appending them with a dot (`.`) after the event name:
175
+
176
+ - **prevent** - Prevents the default browser behavior
177
+ - **once** - Only runs the event handler once
178
+ - **outside** - Only fires when the event happens outside the element
179
+ - **document** - Attaches the listener to the document instead of the element
180
+ - **debounce** - Debounces the event handler (default 300ms)
181
+ - **debounce:500** - Debounces with custom delay in milliseconds
182
+ - **shift, ctrl, alt, meta** - Only fires if the modifier key is pressed
183
+ - **Key names** - For keyboard events, specify which key (e.g., `@keydown.enter`, `@keyup.esc`)
184
+
185
+ Examples:
133
186
 
134
187
  ```html
135
188
  <button @click.prevent="submitForm()">Save</button>
189
+ <button @click.once="initialize()">Initialize</button>
190
+ <div @click.outside="closeModal()">Modal</div>
191
+ <input @input.debounce:500="search()">
192
+ <input @keydown.enter="submit()">
193
+ <input @keydown.ctrl.s.prevent="save()">
136
194
  ```
137
195
 
138
- Alias: prepend the event name with `data-he-on`, for example `data-he-onclick="count++"`
196
+ **Alias:** Prepend the event name with `data-he-on`, for example `data-he-onclick="count++"`
139
197
 
140
- ## Dynamic Attributes
198
+ ## HTTP Requests
141
199
 
142
- It's possible to dynamically update the attributes of elements. To do this, just prepend a `:` in front of the attribute name and write a JavaScript expression that evaulates to the desired attribute value. This will update whenever any of the Helium variables change value.
200
+ Helium includes built-in support for making HTTP requests directly from event handlers.
143
201
 
144
- In the following example, the `<div>` element has a dynamic class attribute that will be 'normal' if the count is less than 10, but 'danger' if the count is 10 or more:
202
+ ### Available HTTP Methods
203
+
204
+ - `@get` - GET request
205
+ - `@post` - POST request
206
+ - `@put` - PUT request
207
+ - `@patch` - PATCH request
208
+ - `@delete` - DELETE request
209
+
210
+ The HTTP method is triggered by the element's default event (click for buttons, submit for forms, input for inputs, etc.).
145
211
 
146
212
  ```html
147
- <div :class="count < 10 ? 'normal' : 'danger'>
148
- The count is <b @text=count></b>
149
- </div>
213
+ <button @get="/api/data">Load Data</button>
214
+ <form @post="/api/users">Submit</form>
150
215
  ```
151
216
 
152
- ## Magic Attributes
217
+ ### HTTP Request Options
153
218
 
154
- `$` is an alias for `document.querySelector`
219
+ Configure requests using these additional attributes:
155
220
 
221
+ - **@target** or **data-he-target** - Where to insert the response (CSS selector or ref)
222
+ - **@action** or **data-he-action** - How to insert: `replace`, `append`, `prepend`, `before`, `after`
223
+ - **@params** or **data-he-params** - Request parameters (object or FormData)
224
+ - **@options** or **data-he-options** - Additional fetch options
225
+ - **@template** or **data-he-template** - Template function to transform response
226
+ - **@loading** or **data-he-loading** - Loading state content to show during request
156
227
 
157
228
  ```html
158
- <div @click="$('#header').classList.add('active')">Activate Header!</div>
229
+ <button
230
+ @get="/api/users"
231
+ @target="#user-list"
232
+ @action="replace">
233
+ Load Users
234
+ </button>
235
+
236
+ <form
237
+ @post="/api/users"
238
+ @params="{ name: username, email: email }"
239
+ @target="#message"
240
+ @loading="Saving...">
241
+ <input @bind="username">
242
+ <input @bind="email">
243
+ <button>Save</button>
244
+ </form>
159
245
  ```
160
246
 
161
- `$el` is an alias for the element:
247
+ ### Special Features
248
+
249
+ - Automatically includes CSRF tokens from `<meta name="csrf-token">` for same-origin requests
250
+ - Supports Turbo Stream responses
251
+ - Handles JSON and HTML responses
252
+ - Works with FormData for file uploads
253
+
254
+ ## Dynamic Attributes
255
+
256
+ It's possible to dynamically update the attributes of elements. To do this, just prepend a `:` in front of the attribute name and write a JavaScript expression that evaluates to the desired attribute value. This will update whenever any of the Helium variables change value.
257
+
258
+ In the following example, the `<div>` element has a dynamic class attribute that will be 'normal' if the count is less than 10, but 'danger' if the count is 10 or more:
162
259
 
163
260
  ```html
164
- <div @click="$el.remove()">Click to remove me!</div>
261
+ <div :class="count < 10 ? 'normal' : 'danger'">
262
+ The count is <b @text="count"></b>
263
+ </div>
165
264
  ```
166
265
 
167
- `$event` is an alias for the event object of an event handler:
266
+ ### Special Dynamic Attributes
168
267
 
268
+ **:class** - Can accept an object to toggle multiple classes:
169
269
 
170
270
  ```html
171
- <div @click="console.log($event.timeStamp)">Log the timestamp</div>
271
+ <div :class="{ active: isActive, disabled: !isEnabled }"></div>
272
+ ```
273
+
274
+ **:style** - Can accept an object for multiple styles:
275
+
276
+ ```html
277
+ <div :style="{ color: textColor, fontSize: size + 'px' }"></div>
172
278
  ```
173
279
 
174
- `@data` gives you access to the data object that contains all the Helium variables:
280
+ **Alias:** `data-he-attr:attributeName`
281
+
282
+ ## Magic Variables
283
+
284
+ These special variables are available in all JavaScript expressions:
175
285
 
286
+ - **$** - Alias for `document.querySelector`
287
+ - **$el** - Reference to the current element
288
+ - **$event** - The event object (in event handlers)
289
+ - **$data** - The reactive data object containing all Helium variables
290
+ - **$html** - Helper function to create HTML elements from strings
291
+ - **$get, $post, $put, $patch, $delete** - HTTP request functions
292
+ - **$refs** - All elements marked with `@ref`
293
+
294
+ Examples:
176
295
 
177
296
  ```html
178
- <div @click="console.log($data)">Log the data</div>
297
+ <div @click="$('#header').classList.add('active')">Activate Header!</div>
298
+ <div @click="$el.remove()">Click to remove me!</div>
299
+ <div @click="console.log($event.timeStamp)">Log the timestamp</div>
300
+ <div @click="console.log($data)">Log all data</div>
179
301
  ```
180
302
 
181
303
  ## Default Variables and Functions
182
304
 
183
305
  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.
184
306
 
185
- For example, the following will set the `count` variable to an initial value of `29` and the `name` variable to "Helium":
307
+ For example, the following will set the count variable to an initial value of 29 and the name variable to "Helium":
186
308
 
187
309
  ```javascript
188
- helium({ count: 29, name: "Helium"})
310
+ helium({
311
+ count: 29,
312
+ name: "Helium"
313
+ })
189
314
  ```
190
315
 
191
- The following example shows how a function can be added into Helium and then used by even listeners:
316
+ The following example shows how a function can be added into Helium and then used by event listeners:
192
317
 
193
318
  ```javascript
194
319
  helium({
195
- appendTo(element){
196
- const li = document.createElement("li")
197
- li.textContent = "New Item"
198
- element.append(li)
199
- }
320
+ appendTo(element) {
321
+ const li = document.createElement("li")
322
+ li.textContent = "New Item"
323
+ element.append(li)
324
+ }
200
325
  })
201
326
  ```
202
327
 
@@ -204,10 +329,14 @@ This function can then be called from an event handler, such as `@click`:
204
329
 
205
330
  ```html
206
331
  <ul @ref="list"></ul>
207
- <button @click="appendTo($list)>Append item to list</button>
332
+ <button @click="appendTo($list)">Append item to list</button>
208
333
  ```
209
334
 
210
- Note that magic attributes and Helium variables are not available inside these functions. However, you can pass them as arguments and they will then be available in the function. If you pass Helium variables then they will be passed as values and updating them inside the function will *not* trigger a reactive update. A solution is to pass the magic `$data` attribute as an argument, then updating the properties of this inside the function *will* trigger a reactive update.
335
+ ### Important Note About Functions
336
+
337
+ Magic variables and Helium variables are not available inside these functions. However, you can pass them as arguments and they will then be available in the function.
338
+
339
+ If you pass Helium variables, they will be passed as values and updating them inside the function will **not** trigger a reactive update. A solution is to pass the magic `$data` attribute as an argument, then updating the properties of this inside the function **will** trigger a reactive update.
211
340
 
212
341
  So instead of this:
213
342
 
@@ -217,9 +346,9 @@ So instead of this:
217
346
 
218
347
  ```javascript
219
348
  helium({
220
- increment(count,n = 1){
221
- count += n
222
- }
349
+ increment(count, n = 1) {
350
+ count += n // Won't trigger reactivity
351
+ }
223
352
  })
224
353
  ```
225
354
 
@@ -231,8 +360,34 @@ You should do this instead:
231
360
 
232
361
  ```javascript
233
362
  helium({
234
- increment(data,n = 1){
235
- data.count += n
236
- }
363
+ increment(data, n = 1) {
364
+ data.count += n // Will trigger reactivity
365
+ }
237
366
  })
238
367
  ```
368
+
369
+ ## Advanced Features
370
+
371
+ ### DOM Morphing
372
+
373
+ If Idiomorph is available, Helium will use it for efficient DOM updates when updating innerHTML, preserving element state and reducing flicker.
374
+
375
+ ### List Rendering
376
+
377
+ When rendering arrays with `@html`, Helium can efficiently update lists by using `key` or `data-key` attributes on child elements for tracking.
378
+
379
+ ```html
380
+ <ul @html="items.map(item => `<li key='${item.id}'>${item.name}</li>`)"></ul>
381
+ ```
382
+
383
+ ### MutationObserver
384
+
385
+ Helium automatically observes the DOM and processes new elements as they're added, making it work seamlessly with dynamically inserted content.
386
+
387
+ ### CSRF Protection
388
+
389
+ Helium automatically includes CSRF tokens from `<meta name="csrf-token">` elements in same-origin requests for enhanced security.
390
+
391
+ ## Browser Compatibility
392
+
393
+ Helium uses modern JavaScript features including Proxy, MutationObserver, and ES6 syntax. It works in all modern browsers that support ES6 modules.
package/helium.js CHANGED
@@ -2,16 +2,34 @@ const parseEx=v=>{try{return Function(`return(${v})`)()}catch{return v}}
2
2
  const getEvent = el => ({form:"submit",input:"input",textarea:"input",select:"change"}[el.tagName.toLowerCase()]||"click")
3
3
  const debounce=(f,d)=>{let t;return(...a)=>(clearTimeout(t),t=setTimeout(f,d,...a))}
4
4
 
5
- export default function helium(data = {}) {
5
+ // Single global object to hold all settings
6
+ let HELIUM = null;
7
+
8
+ window.helium = function() {
6
9
  let initFn;
7
- const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split('.')[0]}|`);
10
+ const ALL = Symbol("all");
11
+ const he = (n,...a) => a.map(b => `|@${b}|data-he-${b}|`).join``.includes(`|${n.split(/[.:]/)[0]}|`);
8
12
  const root = document.querySelector("[\\@helium]") || document.querySelector("[data-helium]") || document.body;
9
- const [bindings, refs, listeners, processed, parentKeys, fnCache, proxyCache] = [new Map(), new Map(), new WeakMap(), new WeakSet(), new WeakMap(), new Map(), new WeakMap()];
13
+
14
+ // Initialize or reuse HELIUM object
15
+ if (!HELIUM) {
16
+ HELIUM = {
17
+ observer: null,
18
+ bindings: new Map(),
19
+ refs: new Map(),
20
+ listeners: new WeakMap(),
21
+ processed: new WeakSet(),
22
+ parentKeys: new WeakMap(),
23
+ fnCache: new Map(),
24
+ proxyCache: new WeakMap()
25
+ };
26
+ }
27
+
10
28
  const $ = s => document.querySelector(s);
11
29
  const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
12
30
 
13
31
  const update = (data,target,action,template) => {
14
- const element = target instanceof Node ? target : (refs.get(target) || $(target));
32
+ const element = target instanceof Node ? target : (HELIUM.refs.get(target) || $(target));
15
33
  if(element){
16
34
  const content = html(template ? template(data) : data);
17
35
  action ? element[action=="replace"?"replaceWith":action](content) : element.innerHTML = content;
@@ -53,10 +71,10 @@ const handler = {
53
71
  get(t,p,r) {
54
72
  const v = Reflect.get(t,p,r);
55
73
  if (v && typeof v === "object") {
56
- if (proxyCache.has(v)) return proxyCache.get(v);
74
+ if (HELIUM.proxyCache.has(v)) return HELIUM.proxyCache.get(v);
57
75
  const proxy = new Proxy(v, handler);
58
- proxyCache.set(v, proxy);
59
- parentKeys.set(v, p);
76
+ HELIUM.proxyCache.set(v, proxy);
77
+ HELIUM.parentKeys.set(v, p);
60
78
  return proxy;
61
79
  }
62
80
  return v;
@@ -64,22 +82,21 @@ const handler = {
64
82
  set: (t,p,v) => {
65
83
  const res = Reflect.set(t,p,v);
66
84
  if (Array.isArray(t) && !isNaN(p)) {
67
- const parentKey = parentKeys.get(t);
68
- if (parentKey) bindings.get(parentKey)?.forEach(applyBinding);
85
+ const parentKey = HELIUM.parentKeys.get(t);
86
+ if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(applyBinding);
69
87
  }
70
- bindings.get(p)?.forEach(applyBinding);
88
+ HELIUM.bindings.get(p)?.concat(...(HELIUM.bindings.get(ALL) ?? []))?.forEach(applyBinding);
71
89
  return res
72
90
  }
73
91
  };
74
92
 
75
- const state = new Proxy(data, handler);
93
+ // Initialize state if it doesn't exist
94
+ const state = new Proxy({}, handler);
76
95
 
77
96
  function applyBinding(b,e={},elCtx=b.el){
78
- const {el,prop,fn}=b;
79
- const r=fn($,state,e,elCtx,html,...Object.values(data),...[...refs.values()]);
80
-
81
- if (prop==="innerHTML" && Array.isArray(r) && el.children.length > 0)
82
- return updateList(el, r);
97
+ const {el,prop,fn,calc}=b;
98
+ const r=fn($,state,e,elCtx,html,...Object.values(state),...[...HELIUM.refs.values()]);
99
+ if (calc) state[calc] = r
83
100
 
84
101
  if (prop==="innerHTML") {
85
102
  const content = Array.isArray(r)?r.join``:r;
@@ -105,98 +122,75 @@ function applyBinding(b,e={},elCtx=b.el){
105
122
  el.setAttribute(prop, parseEx(r));
106
123
  }
107
124
 
108
-
109
- function updateList(el,r){
110
- const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
111
- const newChildren = [...temp.children];
112
-
113
- const newItems = newChildren.map((child, idx) => ({
114
- key: child.getAttribute("key") || child.dataset.key,
115
- element: child,
116
- index: idx
117
- }));
118
-
119
- const existingItems = [...el.children].map((child, idx) => ({
120
- key: child.getAttribute("key") || child.dataset.key,
121
- element: child,
122
- index: idx
123
- }));
124
-
125
- for (let i = 0; i < Math.max(newItems.length, existingItems.length); i++) {
126
- const existing = existingItems[i];
127
- const newItem = newItems[i];
128
-
129
- if (!newItem && existing) {
130
- cleanup(existing.element);
131
- existing.element.remove();
132
- } else if (newItem && !existing) {
133
- el.appendChild(newItem.element);
134
- } else if (newItem && existing) {
135
- if (existing.element.outerHTML !== newItem.element.outerHTML) {
136
- if(typeof Idiomorph === "object") {
137
- Idiomorph.morph(existing.element, newItem.element.outerHTML);
138
- } else {
139
- cleanup(existing.element);
140
- existing.element.replaceWith(newItem.element);
141
- }
142
- }
143
- }
144
- }
145
- }
146
-
147
125
  const compile = (expr, withReturn = false) => {
148
- const key = `${withReturn}:${expr}`;
149
- if (fnCache.has(key)) return fnCache.get(key);
126
+ // Auto-detect if this should be a statement block
127
+ const hasStatements = /[;\r\n]/.test(expr);
128
+ const useReturn = withReturn && (!hasStatements || expr.trim().startsWith('{') && expr.trim().endsWith('}'));
129
+ const key = `${useReturn}:${expr}`;
130
+ if (HELIUM.fnCache.has(key)) return HELIUM.fnCache.get(key);
150
131
  try {
151
132
  const fn = new Function(
152
133
  "$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
153
- ...Object.keys(data), ...[...refs.keys()],
154
- `with($data){${withReturn?"return":""}(${expr.trim()})}`
134
+ ...Object.keys(state), ...[...HELIUM.refs.keys()],
135
+ useReturn
136
+ ? `with($data){return(${expr.trim()})}`
137
+ : `with($data){${expr.trim()}}`
155
138
  );
156
- fnCache.set(key, fn);
139
+ HELIUM.fnCache.set(key, fn);
157
140
  return fn;
158
141
  } catch {
159
142
  return () => expr;
160
143
  }
161
144
  };
162
145
 
163
- const trackDependencies = (fn, el) => {
164
- const accessed = new Set();
165
- const trackProxy = new Proxy(data, {
166
- get(target, prop) {
167
- if (typeof prop == 'string') accessed.add(prop);
168
- const val = target[prop];
169
- return typeof val == "object" && val != null ? new Proxy(val, this) : val;
146
+ const trackDependencies = (fn, el, excludeChanged = false) => {
147
+ const accessed = excludeChanged ? new Map() : new Set();
148
+ const trackProxy = new Proxy(state, {
149
+ get(target, prop) {
150
+ if (typeof prop == 'string') {
151
+ if (excludeChanged && !accessed.has(prop)) {
152
+ accessed.set(prop, target[prop]); // Store initial value
153
+ } else if (!excludeChanged) {
154
+ accessed.add(prop);
155
+ }
170
156
  }
171
- });
172
-
173
- try { fn.call(null, $, trackProxy, refs); } catch {}
174
- return [...accessed];
175
- };
157
+ const val = target[prop];
158
+ return typeof val == "object" && val != null ? new Proxy(val, this) : val;
159
+ }
160
+ });
161
+
162
+ try { fn.call(null, $, trackProxy, HELIUM.refs); } catch {}
163
+
164
+ if (excludeChanged) {
165
+ return [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop));
166
+ }
167
+ return [...accessed];
168
+ };
176
169
 
177
170
  const cleanup = el => {
178
171
  [el,...el.querySelectorAll('*')].forEach(e => {
179
- listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
180
- listeners.delete(e);
172
+ HELIUM.listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
173
+ HELIUM.listeners.delete(e);
181
174
  });
182
175
  };
183
176
 
184
177
  function processElements(element) {
185
178
  const newBindings = [];
179
+ const deferredBindings = [];
186
180
 
187
181
  const heElements = [element, ...element.querySelectorAll("*")]
188
- .filter(e => !processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
182
+ .filter(e => !HELIUM.processed.has(e) && [...e.attributes].some(a => /^(@|:|data-he)/.test(a.name)));
189
183
 
190
184
  const addBinding = (val, b) => {
191
- bindings.set(val, [...(bindings.get(val) || []), b]);
192
- newBindings.push(b);
185
+ HELIUM.bindings.set(val, b.calc ? [b,...(HELIUM.bindings.get(val) || [])] : [...(HELIUM.bindings.get(val) || []), b]);
186
+ b.calc ? newBindings.unshift(b) : newBindings.push(b);
193
187
  };
194
188
 
195
189
  heElements.forEach(el => {
196
- processed.add(el);
190
+ HELIUM.processed.add(el);
197
191
 
198
192
  const attrs = el.attributes;
199
- const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()]);
193
+ const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()]);
200
194
  const inputType = el.type?.toLowerCase();
201
195
  const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
202
196
 
@@ -217,23 +211,27 @@ function processElements(element) {
217
211
 
218
212
  // Process the attribute
219
213
  if (["@data", "data-he"].includes(name)) {
220
- Object.assign(state, execFn(value));
214
+ Object.assign(state, parseEx(value));
215
+ }
216
+ else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
217
+ const fn = compile(value, true);
218
+ deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(name.startsWith(":") ? 1 : 13), fn})));
221
219
  }
222
220
  else if (he(name, "ref")) {
223
- refs.set("$" + value, el);
221
+ HELIUM.refs.set("$" + value, el);
224
222
  }
225
223
  else if (he(name, "text", "html")) {
226
224
  const fn = compile(value, true);
227
225
  const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
228
- trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
226
+ deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, b)));
229
227
  }
230
228
  else if (he(name, "bind")) {
231
229
  const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
232
230
  const prop = isCheckbox ? "checked" : "value";
233
231
  const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
234
232
  el.addEventListener(event, inputHandler);
235
- if (!listeners.has(el)) listeners.set(el, []);
236
- listeners.get(el).push({receiver: el, event, handler: inputHandler});
233
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
234
+ HELIUM.listeners.get(el).push({receiver: el, event, handler: inputHandler});
237
235
  addBinding(value, {el, prop, fn: compile(value, true)});
238
236
  if (isCheckbox) el.checked = !!state[value];
239
237
  else if (isRadio) el.checked = el.value == state[value];
@@ -241,15 +239,24 @@ function processElements(element) {
241
239
  }
242
240
  else if (he(name, "hidden", "visible")) {
243
241
  const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
244
- trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn}));
242
+ deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "hidden", fn})));
245
243
  }
246
- else if (he(name, "class")) {
244
+ else if (he(name, "calculate")) {
245
+ const calc = name.split(":")[1];
246
+ const fn = compile(value, true);
247
+ deferredBindings.push(() => trackDependencies(fn, el, true).forEach(key => addBinding(key, {el, calc, prop: null, fn})));
248
+ }
249
+ else if (he(name, "effect")) {
250
+ const keys = name.split(":").slice(1);
247
251
  const fn = compile(value, true);
248
- trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn}));
252
+ deferredBindings.push(() => {
253
+ const tracked = keys.includes("*") ? [ALL] : trackDependencies(fn, el, true).concat(keys);
254
+ tracked.forEach(key => addBinding(key, {el, prop: null, fn}));
255
+ })
249
256
  }
250
- else if (name.startsWith(":")) {
257
+ else if (he(name, "class")) {
251
258
  const fn = compile(value, true);
252
- trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: name.slice(1), fn}));
259
+ deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn})));
253
260
  }
254
261
  else if (he(name, "init")) {
255
262
  initFn = compile(value, true);
@@ -263,7 +270,7 @@ function processElements(element) {
263
270
  const debounceMod = mods.find(m => m.startsWith("debounce"));
264
271
  const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
265
272
  const _handler = e => {
266
- const exFn = v => compile(v, true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(data), ...[...refs.values()])
273
+ const exFn = v => compile(v,true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()])
267
274
  if (mods.includes("prevent")) e.preventDefault();
268
275
  const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
269
276
  for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
@@ -279,11 +286,11 @@ function processElements(element) {
279
286
  const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
280
287
  const [target, action] = (getAttr('target') || "").split(":");
281
288
  const options = {
282
- ...exFn(getAttr('options') || '{}'),
283
- ...(target && {target}),
284
- ...(action && {action}),
285
- ...execFn(getAttr('template')) && {template: execFn(getAttr('template'))},
286
- ...getAttr('loading') && {loading: getAttr('loading')}
289
+ ...(getAttr("options") && parseEx(getAttr("options") || "{}")),
290
+ ...(target && { target }),
291
+ ...(action && { action }),
292
+ ...(getAttr("template") && { template: execFn(getAttr("template")),}),
293
+ ...(getAttr("loading") && { loading: execFn(getAttr("loading"))}),
287
294
  };
288
295
  let paramsAttr = getAttr('params') || '{}';
289
296
  if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
@@ -300,20 +307,50 @@ function processElements(element) {
300
307
  };
301
308
  const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
302
309
  receiver.addEventListener(event, handler);
303
- if (!listeners.has(el)) listeners.set(el, []);
304
- listeners.get(el).push({receiver, event, handler});
310
+ if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
311
+ HELIUM.listeners.get(el).push({receiver, event, handler});
305
312
  }
306
313
  }
307
314
  });
315
+ deferredBindings.forEach(fn => fn());
308
316
  return newBindings;
309
317
  }
310
- new MutationObserver(ms=>{
311
- for(const m of ms){
312
- m.removedNodes.forEach(n=>n.nodeType===1&&cleanup(n));
313
- m.addedNodes.forEach(n=>n.nodeType===1&&!processed.has(n)&&processElements(n).forEach(applyBinding));
318
+
319
+ // Disconnect old observer if it exists
320
+ if (HELIUM.observer) HELIUM.observer.disconnect();
321
+
322
+ // Create new observer
323
+ HELIUM.observer = new MutationObserver(ms => {
324
+ for (const m of ms) {
325
+ m.removedNodes.forEach(n => n.nodeType === 1 && cleanup(n));
326
+ m.addedNodes.forEach(n => n.nodeType === 1 && !HELIUM.processed.has(n) && processElements(n).forEach(applyBinding));
314
327
  }
315
- }).observe(root,{childList:1,subtree:1});
328
+ });
329
+ HELIUM.observer.observe(root, { childList: true, subtree: true });
330
+
316
331
  processElements(root);
317
- for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
318
- if(initFn) initFn($, state, {}, {}, html,get,post, put,patch,del, ...Object.values(data), ...[...refs.values()])
332
+ for (const [key, items] of HELIUM.bindings.entries()) items.forEach(applyBinding);
333
+ if(initFn) initFn($, state, {}, {}, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()])
319
334
  }
335
+
336
+ window.heliumTeardown = function() {
337
+ if (HELIUM?.observer) HELIUM.observer.disconnect();
338
+ HELIUM?.bindings?.clear();
339
+ HELIUM?.refs?.clear();
340
+ HELIUM?.parentKeys?.clear();
341
+ HELIUM?.fnCache?.clear();
342
+ HELIUM = null;
343
+ }
344
+
345
+ // Initialize on load
346
+ document.addEventListener("DOMContentLoaded", () => helium());
347
+
348
+ // Turbo integration
349
+ document.addEventListener("turbo:before-render", () => {
350
+ if (HELIUM?.observer) HELIUM.observer.disconnect();
351
+ window.heliumTeardown();
352
+ });
353
+
354
+ document.addEventListener("turbo:render", () => {
355
+ helium();
356
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {