@daz4126/helium 0.15.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.
- package/README.md +222 -67
- package/helium.js +153 -109
- 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
|
-
|
|
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">
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
### @helium
|
|
43
48
|
|
|
44
|
-
|
|
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
|
-
|
|
51
|
+
**Alias:** `data-helium`
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
### @text
|
|
49
54
|
|
|
50
|
-
This will update the textContent of the element with the value of the
|
|
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
|
|
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()">
|
|
64
|
+
<span @text="name.toUpperCase()">Dave</span>
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
Alias
|
|
67
|
+
**Alias:** `data-he-text`
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
### @html
|
|
65
70
|
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
+
Works with text inputs, textareas, checkboxes, radio buttons, and select elements.
|
|
88
|
+
|
|
89
|
+
**Alias:** `data-he-bind`
|
|
75
90
|
|
|
76
|
-
|
|
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
|
|
100
|
+
**Alias:** `data-he-hidden` & `data-he-visible`
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
### @data
|
|
87
103
|
|
|
88
|
-
Initializes variables that can be used in
|
|
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
|
|
110
|
+
**Alias:** `data-he-data`
|
|
95
111
|
|
|
96
|
-
|
|
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
|
|
126
|
+
**Alias:** `data-he-ref`
|
|
113
127
|
|
|
114
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
196
|
+
**Alias:** Prepend the event name with `data-he-on`, for example `data-he-onclick="count++"`
|
|
139
197
|
|
|
140
|
-
##
|
|
198
|
+
## HTTP Requests
|
|
141
199
|
|
|
142
|
-
|
|
200
|
+
Helium includes built-in support for making HTTP requests directly from event handlers.
|
|
143
201
|
|
|
144
|
-
|
|
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
|
-
<
|
|
148
|
-
|
|
149
|
-
</div>
|
|
213
|
+
<button @get="/api/data">Load Data</button>
|
|
214
|
+
<form @post="/api/users">Submit</form>
|
|
150
215
|
```
|
|
151
216
|
|
|
152
|
-
|
|
217
|
+
### HTTP Request Options
|
|
153
218
|
|
|
154
|
-
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
|
261
|
+
<div :class="count < 10 ? 'normal' : 'danger'">
|
|
262
|
+
The count is <b @text="count"></b>
|
|
263
|
+
</div>
|
|
165
264
|
```
|
|
166
265
|
|
|
167
|
-
|
|
266
|
+
### Special Dynamic Attributes
|
|
168
267
|
|
|
268
|
+
**:class** - Can accept an object to toggle multiple classes:
|
|
169
269
|
|
|
170
270
|
```html
|
|
171
|
-
<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
|
-
|
|
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="
|
|
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
|
|
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({
|
|
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
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
|
|
5
|
+
// Single global object to hold all settings
|
|
6
|
+
let HELIUM = null;
|
|
7
|
+
|
|
8
|
+
window.helium = function() {
|
|
6
9
|
let initFn;
|
|
7
|
-
const
|
|
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
|
-
|
|
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;
|
|
@@ -22,11 +40,18 @@ export default function helium(data = {}) {
|
|
|
22
40
|
const ajax = (u,m,o={},p={}) => {
|
|
23
41
|
if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
|
|
24
42
|
const fd = p instanceof FormData, t = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
43
|
+
const url = new URL(u, window.location.href);
|
|
44
|
+
const sameOrigin = url.origin === window.location.origin;
|
|
45
|
+
fetch(u, {
|
|
46
|
+
method: m,
|
|
47
|
+
headers: {
|
|
48
|
+
Accept:"text/vnd.turbo-stream.html,application/json,text/html",
|
|
49
|
+
...(!fd && m !== "GET" && {"Content-Type":"application/json"}),
|
|
50
|
+
...(sameOrigin && t ? {"X-CSRF-Token": t} : {})
|
|
51
|
+
},
|
|
52
|
+
body: m === "GET" ? null : (fd ? p : JSON.stringify(p)),
|
|
53
|
+
credentials: sameOrigin ? "same-origin" : "omit"
|
|
54
|
+
})
|
|
30
55
|
.then(r => {
|
|
31
56
|
const type = r.headers.get("content-type") || "";
|
|
32
57
|
return (type.includes("turbo-stream") ? r.text().then(d => ({ t: true, d })) :
|
|
@@ -46,10 +71,10 @@ const handler = {
|
|
|
46
71
|
get(t,p,r) {
|
|
47
72
|
const v = Reflect.get(t,p,r);
|
|
48
73
|
if (v && typeof v === "object") {
|
|
49
|
-
if (proxyCache.has(v)) return proxyCache.get(v);
|
|
74
|
+
if (HELIUM.proxyCache.has(v)) return HELIUM.proxyCache.get(v);
|
|
50
75
|
const proxy = new Proxy(v, handler);
|
|
51
|
-
proxyCache.set(v, proxy);
|
|
52
|
-
parentKeys.set(v, p);
|
|
76
|
+
HELIUM.proxyCache.set(v, proxy);
|
|
77
|
+
HELIUM.parentKeys.set(v, p);
|
|
53
78
|
return proxy;
|
|
54
79
|
}
|
|
55
80
|
return v;
|
|
@@ -57,22 +82,21 @@ const handler = {
|
|
|
57
82
|
set: (t,p,v) => {
|
|
58
83
|
const res = Reflect.set(t,p,v);
|
|
59
84
|
if (Array.isArray(t) && !isNaN(p)) {
|
|
60
|
-
const parentKey = parentKeys.get(t);
|
|
61
|
-
if (parentKey) bindings.get(parentKey)?.forEach(applyBinding);
|
|
85
|
+
const parentKey = HELIUM.parentKeys.get(t);
|
|
86
|
+
if (parentKey) HELIUM.bindings.get(parentKey)?.forEach(applyBinding);
|
|
62
87
|
}
|
|
63
|
-
bindings.get(p)?.forEach(applyBinding);
|
|
88
|
+
HELIUM.bindings.get(p)?.concat(...(HELIUM.bindings.get(ALL) ?? []))?.forEach(applyBinding);
|
|
64
89
|
return res
|
|
65
90
|
}
|
|
66
91
|
};
|
|
67
92
|
|
|
68
|
-
|
|
93
|
+
// Initialize state if it doesn't exist
|
|
94
|
+
const state = new Proxy({}, handler);
|
|
69
95
|
|
|
70
96
|
function applyBinding(b,e={},elCtx=b.el){
|
|
71
|
-
const {el,prop,fn}=b;
|
|
72
|
-
const r=fn($,state,e,elCtx,html,...Object.values(
|
|
73
|
-
|
|
74
|
-
if (prop==="innerHTML" && Array.isArray(r) && el.children.length > 0)
|
|
75
|
-
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
|
|
76
100
|
|
|
77
101
|
if (prop==="innerHTML") {
|
|
78
102
|
const content = Array.isArray(r)?r.join``:r;
|
|
@@ -98,98 +122,75 @@ function applyBinding(b,e={},elCtx=b.el){
|
|
|
98
122
|
el.setAttribute(prop, parseEx(r));
|
|
99
123
|
}
|
|
100
124
|
|
|
101
|
-
|
|
102
|
-
function updateList(el,r){
|
|
103
|
-
const temp = html(`<${el.tagName.toLowerCase()}>${r.join``}</${el.tagName.toLowerCase()}>`);
|
|
104
|
-
const newChildren = [...temp.children];
|
|
105
|
-
|
|
106
|
-
const newItems = newChildren.map((child, idx) => ({
|
|
107
|
-
key: child.getAttribute("key") || child.dataset.key,
|
|
108
|
-
element: child,
|
|
109
|
-
index: idx
|
|
110
|
-
}));
|
|
111
|
-
|
|
112
|
-
const existingItems = [...el.children].map((child, idx) => ({
|
|
113
|
-
key: child.getAttribute("key") || child.dataset.key,
|
|
114
|
-
element: child,
|
|
115
|
-
index: idx
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < Math.max(newItems.length, existingItems.length); i++) {
|
|
119
|
-
const existing = existingItems[i];
|
|
120
|
-
const newItem = newItems[i];
|
|
121
|
-
|
|
122
|
-
if (!newItem && existing) {
|
|
123
|
-
cleanup(existing.element);
|
|
124
|
-
existing.element.remove();
|
|
125
|
-
} else if (newItem && !existing) {
|
|
126
|
-
el.appendChild(newItem.element);
|
|
127
|
-
} else if (newItem && existing) {
|
|
128
|
-
if (existing.element.outerHTML !== newItem.element.outerHTML) {
|
|
129
|
-
if(typeof Idiomorph === "object") {
|
|
130
|
-
Idiomorph.morph(existing.element, newItem.element.outerHTML);
|
|
131
|
-
} else {
|
|
132
|
-
cleanup(existing.element);
|
|
133
|
-
existing.element.replaceWith(newItem.element);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
125
|
const compile = (expr, withReturn = false) => {
|
|
141
|
-
|
|
142
|
-
|
|
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);
|
|
143
131
|
try {
|
|
144
132
|
const fn = new Function(
|
|
145
133
|
"$","$data","$event","$el","$html","$get","$post","$put","$patch","$delete",
|
|
146
|
-
...Object.keys(
|
|
147
|
-
|
|
134
|
+
...Object.keys(state), ...[...HELIUM.refs.keys()],
|
|
135
|
+
useReturn
|
|
136
|
+
? `with($data){return(${expr.trim()})}`
|
|
137
|
+
: `with($data){${expr.trim()}}`
|
|
148
138
|
);
|
|
149
|
-
fnCache.set(key, fn);
|
|
139
|
+
HELIUM.fnCache.set(key, fn);
|
|
150
140
|
return fn;
|
|
151
141
|
} catch {
|
|
152
142
|
return () => expr;
|
|
153
143
|
}
|
|
154
144
|
};
|
|
155
145
|
|
|
156
|
-
const trackDependencies = (fn, el) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
156
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
};
|
|
169
169
|
|
|
170
170
|
const cleanup = el => {
|
|
171
171
|
[el,...el.querySelectorAll('*')].forEach(e => {
|
|
172
|
-
listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
|
|
173
|
-
listeners.delete(e);
|
|
172
|
+
HELIUM.listeners.get(e)?.forEach(({receiver,event,handler}) => receiver.removeEventListener(event,handler));
|
|
173
|
+
HELIUM.listeners.delete(e);
|
|
174
174
|
});
|
|
175
175
|
};
|
|
176
176
|
|
|
177
177
|
function processElements(element) {
|
|
178
178
|
const newBindings = [];
|
|
179
|
+
const deferredBindings = [];
|
|
179
180
|
|
|
180
181
|
const heElements = [element, ...element.querySelectorAll("*")]
|
|
181
|
-
.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)));
|
|
182
183
|
|
|
183
184
|
const addBinding = (val, b) => {
|
|
184
|
-
bindings.set(val, [...(bindings.get(val) || []), b]);
|
|
185
|
-
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);
|
|
186
187
|
};
|
|
187
188
|
|
|
188
189
|
heElements.forEach(el => {
|
|
189
|
-
processed.add(el);
|
|
190
|
+
HELIUM.processed.add(el);
|
|
190
191
|
|
|
191
192
|
const attrs = el.attributes;
|
|
192
|
-
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(
|
|
193
|
+
const execFn = v => compile(v, true)($, state, {}, el, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()]);
|
|
193
194
|
const inputType = el.type?.toLowerCase();
|
|
194
195
|
const isCheckbox = inputType == "checkbox", isRadio = inputType == "radio", isSelect = el.tagName == "SELECT";
|
|
195
196
|
|
|
@@ -210,23 +211,27 @@ function processElements(element) {
|
|
|
210
211
|
|
|
211
212
|
// Process the attribute
|
|
212
213
|
if (["@data", "data-he"].includes(name)) {
|
|
213
|
-
Object.assign(state,
|
|
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})));
|
|
214
219
|
}
|
|
215
220
|
else if (he(name, "ref")) {
|
|
216
|
-
refs.set("$" + value, el);
|
|
221
|
+
HELIUM.refs.set("$" + value, el);
|
|
217
222
|
}
|
|
218
223
|
else if (he(name, "text", "html")) {
|
|
219
224
|
const fn = compile(value, true);
|
|
220
225
|
const b = {el, prop: he(name, "text") ? "textContent" : "innerHTML", fn};
|
|
221
|
-
trackDependencies(fn, el).forEach(dep => addBinding(dep, b));
|
|
226
|
+
deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, b)));
|
|
222
227
|
}
|
|
223
228
|
else if (he(name, "bind")) {
|
|
224
229
|
const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
|
|
225
230
|
const prop = isCheckbox ? "checked" : "value";
|
|
226
231
|
const inputHandler = e => state[value] = isCheckbox ? e.target.checked : e.target.value;
|
|
227
232
|
el.addEventListener(event, inputHandler);
|
|
228
|
-
if (!listeners.has(el)) listeners.set(el, []);
|
|
229
|
-
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});
|
|
230
235
|
addBinding(value, {el, prop, fn: compile(value, true)});
|
|
231
236
|
if (isCheckbox) el.checked = !!state[value];
|
|
232
237
|
else if (isRadio) el.checked = el.value == state[value];
|
|
@@ -234,15 +239,24 @@ function processElements(element) {
|
|
|
234
239
|
}
|
|
235
240
|
else if (he(name, "hidden", "visible")) {
|
|
236
241
|
const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
|
|
237
|
-
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})));
|
|
238
243
|
}
|
|
239
|
-
else if (he(name, "
|
|
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);
|
|
240
251
|
const fn = compile(value, true);
|
|
241
|
-
|
|
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
|
+
})
|
|
242
256
|
}
|
|
243
|
-
else if (name
|
|
257
|
+
else if (he(name, "class")) {
|
|
244
258
|
const fn = compile(value, true);
|
|
245
|
-
trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop:
|
|
259
|
+
deferredBindings.push(() => trackDependencies(fn, el).forEach(dep => addBinding(dep, {el, prop: "class", fn})));
|
|
246
260
|
}
|
|
247
261
|
else if (he(name, "init")) {
|
|
248
262
|
initFn = compile(value, true);
|
|
@@ -256,7 +270,7 @@ function processElements(element) {
|
|
|
256
270
|
const debounceMod = mods.find(m => m.startsWith("debounce"));
|
|
257
271
|
const debounceDelay = debounceMod ? (t => t && !isNaN(t) ? Number(t) : 300)(debounceMod.split(":")[1]) : 0;
|
|
258
272
|
const _handler = e => {
|
|
259
|
-
|
|
273
|
+
const exFn = v => compile(v,true)($, state, e, el, html, get, post, put, patch, del, ...Object.values(state), ...[...HELIUM.refs.values()])
|
|
260
274
|
if (mods.includes("prevent")) e.preventDefault();
|
|
261
275
|
const keyMods = {shift: "shiftKey", ctrl: "ctrlKey", alt: "altKey", meta: "metaKey"};
|
|
262
276
|
for (const [mod, prop] of Object.entries(keyMods)) if (mods.includes(mod) && !e[prop]) return;
|
|
@@ -272,11 +286,11 @@ function processElements(element) {
|
|
|
272
286
|
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
273
287
|
const [target, action] = (getAttr('target') || "").split(":");
|
|
274
288
|
const options = {
|
|
275
|
-
...
|
|
276
|
-
...(target && {target}),
|
|
277
|
-
...(action && {action}),
|
|
278
|
-
...
|
|
279
|
-
...getAttr(
|
|
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"))}),
|
|
280
294
|
};
|
|
281
295
|
let paramsAttr = getAttr('params') || '{}';
|
|
282
296
|
if (!paramsAttr.trim().startsWith("{") && paramsAttr.includes(":")) {
|
|
@@ -293,20 +307,50 @@ function processElements(element) {
|
|
|
293
307
|
};
|
|
294
308
|
const handler = debounceDelay > 0 ? debounce(_handler, debounceDelay) : _handler;
|
|
295
309
|
receiver.addEventListener(event, handler);
|
|
296
|
-
if (!listeners.has(el)) listeners.set(el, []);
|
|
297
|
-
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});
|
|
298
312
|
}
|
|
299
313
|
}
|
|
300
314
|
});
|
|
315
|
+
deferredBindings.forEach(fn => fn());
|
|
301
316
|
return newBindings;
|
|
302
317
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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));
|
|
307
327
|
}
|
|
308
|
-
})
|
|
328
|
+
});
|
|
329
|
+
HELIUM.observer.observe(root, { childList: true, subtree: true });
|
|
330
|
+
|
|
309
331
|
processElements(root);
|
|
310
|
-
for (const [key, items] of bindings.entries()) items.forEach(applyBinding);
|
|
311
|
-
if(initFn) initFn($, state, {}, {}, html,get,post, put,patch,del, ...Object.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()])
|
|
312
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
|
+
});
|