@daz4126/helium 0.20.0 → 0.22.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 +847 -76
- package/helium.js +46 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,16 @@ 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
|
+
## Why Helium?
|
|
18
|
+
|
|
19
|
+
Helium is designed for developers who want:
|
|
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
|
|
26
|
+
|
|
17
27
|
## Installation
|
|
18
28
|
|
|
19
29
|
### CDN (No build step required!)
|
|
@@ -40,14 +50,24 @@ import helium from "@daz4126/helium"
|
|
|
40
50
|
helium()
|
|
41
51
|
```
|
|
42
52
|
|
|
53
|
+
### Automatic Initialization
|
|
54
|
+
|
|
55
|
+
Helium automatically initializes on `DOMContentLoaded`, so you typically don't need to call `helium()` manually unless you're providing default values or functions.
|
|
56
|
+
|
|
43
57
|
## Helium Attributes
|
|
44
58
|
|
|
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.
|
|
59
|
+
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 (useful for HTML validators).
|
|
46
60
|
|
|
47
61
|
### @helium
|
|
48
62
|
|
|
49
63
|
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`.
|
|
50
64
|
|
|
65
|
+
```html
|
|
66
|
+
<div @helium>
|
|
67
|
+
<!-- All Helium attributes work here -->
|
|
68
|
+
</div>
|
|
69
|
+
```
|
|
70
|
+
|
|
51
71
|
**Alias:** `data-helium`
|
|
52
72
|
|
|
53
73
|
### @text
|
|
@@ -74,6 +94,13 @@ Similar to `@text`, but inserts HTML content into the element's innerHTML. Suppo
|
|
|
74
94
|
<div @html="'<strong>Bold text</strong>'"></div>
|
|
75
95
|
```
|
|
76
96
|
|
|
97
|
+
**Rendering Arrays:**
|
|
98
|
+
```html
|
|
99
|
+
<ul @html="items.map(item => `<li>${item}</li>`)"></ul>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Security Note:** Be careful with `@html` when rendering user-generated content, as it can lead to XSS vulnerabilities. Always sanitize user input before rendering it as HTML.
|
|
103
|
+
|
|
77
104
|
**Alias:** `data-he-html`
|
|
78
105
|
|
|
79
106
|
### @bind
|
|
@@ -84,7 +111,33 @@ Creates a 2-way binding between an input element's value and a variable. Whateve
|
|
|
84
111
|
<input @bind="name" placeholder="Enter your name">
|
|
85
112
|
```
|
|
86
113
|
|
|
87
|
-
Works with
|
|
114
|
+
Works with:
|
|
115
|
+
- Text inputs and textareas (binds to `value`)
|
|
116
|
+
- Checkboxes (binds to `checked`)
|
|
117
|
+
- Radio buttons (binds to `value`, checking the one that matches)
|
|
118
|
+
- Select elements (binds to `value`)
|
|
119
|
+
|
|
120
|
+
**Examples:**
|
|
121
|
+
```html
|
|
122
|
+
<!-- Text input -->
|
|
123
|
+
<input @bind="username">
|
|
124
|
+
<p>Hello, <span @text="username"></span>!</p>
|
|
125
|
+
|
|
126
|
+
<!-- Checkbox -->
|
|
127
|
+
<input type="checkbox" @bind="agreed">
|
|
128
|
+
<span @text="agreed ? 'Agreed' : 'Not agreed'"></span>
|
|
129
|
+
|
|
130
|
+
<!-- Radio buttons -->
|
|
131
|
+
<input type="radio" name="color" value="red" @bind="color">
|
|
132
|
+
<input type="radio" name="color" value="blue" @bind="color">
|
|
133
|
+
<p>Selected: <span @text="color"></span></p>
|
|
134
|
+
|
|
135
|
+
<!-- Select -->
|
|
136
|
+
<select @bind="country">
|
|
137
|
+
<option value="us">United States</option>
|
|
138
|
+
<option value="uk">United Kingdom</option>
|
|
139
|
+
</select>
|
|
140
|
+
```
|
|
88
141
|
|
|
89
142
|
**Alias:** `data-he-bind`
|
|
90
143
|
|
|
@@ -101,36 +154,46 @@ Makes the element hidden or visible depending on the result of a JavaScript expr
|
|
|
101
154
|
|
|
102
155
|
### @data
|
|
103
156
|
|
|
104
|
-
Initializes variables that can be used in JavaScript expressions.
|
|
157
|
+
Initializes variables that can be used in JavaScript expressions. This is useful for setting up initial state.
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<div @data="{ count: 0, open: false, name: 'Helium' }"></div>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
You can then use these variables in other Helium attributes:
|
|
105
164
|
|
|
106
165
|
```html
|
|
107
|
-
<div @data="{ count: 0
|
|
166
|
+
<div @data="{ count: 0 }">
|
|
167
|
+
<button @click="count++">Increment</button>
|
|
168
|
+
<p @text="count"></p>
|
|
169
|
+
</div>
|
|
108
170
|
```
|
|
109
171
|
|
|
110
172
|
**Alias:** `data-he-data`
|
|
111
173
|
|
|
112
174
|
### @ref
|
|
113
175
|
|
|
114
|
-
Creates a reference to the element that can be used in JavaScript expressions.
|
|
176
|
+
Creates a reference to the element that can be used in JavaScript expressions. References are prefixed with `$` when accessed.
|
|
115
177
|
|
|
116
178
|
```html
|
|
117
179
|
<ul @ref="list"></ul>
|
|
118
180
|
```
|
|
119
181
|
|
|
120
|
-
This element can then be accessed in other JavaScript expressions as `$list
|
|
182
|
+
This element can then be accessed in other JavaScript expressions as `$list`:
|
|
121
183
|
|
|
122
184
|
```html
|
|
123
|
-
<button @click="
|
|
185
|
+
<button @click="$list.appendChild($html('<li>New item</li>'))">Add Task</button>
|
|
124
186
|
```
|
|
125
187
|
|
|
126
188
|
**Alias:** `data-he-ref`
|
|
127
189
|
|
|
128
190
|
### @init
|
|
129
191
|
|
|
130
|
-
A JavaScript expression that will run once when Helium initializes.
|
|
192
|
+
A JavaScript expression that will run once when Helium initializes. Useful for setup code that should run on page load.
|
|
131
193
|
|
|
132
194
|
```html
|
|
133
195
|
<div @init="timestamp = Date.now()"></div>
|
|
196
|
+
<div @init="console.log('Helium initialized!')"></div>
|
|
134
197
|
```
|
|
135
198
|
|
|
136
199
|
**Alias:** `data-he-init`
|
|
@@ -145,59 +208,164 @@ Creates a computed property that automatically updates when its dependencies cha
|
|
|
145
208
|
|
|
146
209
|
This will create a `total` variable that automatically recalculates whenever `price` or `quantity` changes.
|
|
147
210
|
|
|
211
|
+
**Practical Examples:**
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<!-- Shopping cart total -->
|
|
215
|
+
<div @data="{ price: 10, quantity: 2, taxRate: 0.1 }">
|
|
216
|
+
<input type="number" @bind="quantity">
|
|
217
|
+
<div @calculate:subtotal="price * quantity"></div>
|
|
218
|
+
<div @calculate:tax="subtotal * taxRate"></div>
|
|
219
|
+
<div @calculate:total="subtotal + tax"></div>
|
|
220
|
+
|
|
221
|
+
<p>Subtotal: $<span @text="subtotal"></span></p>
|
|
222
|
+
<p>Tax: $<span @text="tax"></span></p>
|
|
223
|
+
<p>Total: $<span @text="total"></span></p>
|
|
224
|
+
</div>
|
|
225
|
+
|
|
226
|
+
<!-- Full name from first and last -->
|
|
227
|
+
<div @data="{ firstName: 'John', lastName: 'Doe' }">
|
|
228
|
+
<input @bind="firstName" placeholder="First name">
|
|
229
|
+
<input @bind="lastName" placeholder="Last name">
|
|
230
|
+
<div @calculate:fullName="firstName + ' ' + lastName"></div>
|
|
231
|
+
<p>Hello, <span @text="fullName"></span>!</p>
|
|
232
|
+
</div>
|
|
233
|
+
```
|
|
234
|
+
|
|
148
235
|
**Alias:** `data-he-calculate`
|
|
149
236
|
|
|
150
237
|
### @effect
|
|
151
238
|
|
|
152
|
-
Runs a side effect whenever specified dependencies change. Use `:*` to run on any state change, or list specific dependencies.
|
|
239
|
+
Runs a side effect whenever specified dependencies change. Use `:*` to run on any state change, or list specific dependencies separated by colons.
|
|
153
240
|
|
|
154
241
|
```html
|
|
155
242
|
<!-- Run on any state change -->
|
|
156
|
-
<div @effect:*="console.log('State changed')"></div>
|
|
243
|
+
<div @effect:*="console.log('State changed:', $data)"></div>
|
|
157
244
|
|
|
158
245
|
<!-- Run when specific variables change -->
|
|
159
246
|
<div @effect:count:name="console.log('Count or name changed')"></div>
|
|
160
247
|
```
|
|
161
248
|
|
|
249
|
+
**Practical Examples:**
|
|
250
|
+
|
|
251
|
+
```html
|
|
252
|
+
<!-- Save to localStorage when username changes -->
|
|
253
|
+
<div @effect:username="localStorage.setItem('user', username)"></div>
|
|
254
|
+
|
|
255
|
+
<!-- Log analytics when count reaches threshold -->
|
|
256
|
+
<div @effect:count="count > 10 && console.log('Threshold reached!')"></div>
|
|
257
|
+
|
|
258
|
+
<!-- Update page title -->
|
|
259
|
+
<div @effect:unreadCount="document.title = `(${unreadCount}) Messages`"></div>
|
|
260
|
+
|
|
261
|
+
<!-- Multiple dependencies -->
|
|
262
|
+
<div @effect:firstName:lastName="console.log('Name changed:', firstName, lastName)"></div>
|
|
263
|
+
```
|
|
264
|
+
|
|
162
265
|
**Alias:** `data-he-effect`
|
|
163
266
|
|
|
267
|
+
### @import
|
|
268
|
+
|
|
269
|
+
Imports global functions or variables from the `window` object into Helium's scope, making them available in Helium expressions.
|
|
270
|
+
|
|
271
|
+
```html
|
|
272
|
+
<div @import="myFunction,myVariable">
|
|
273
|
+
<button @click="myFunction()">Call Imported Function</button>
|
|
274
|
+
<p @text="myVariable"></p>
|
|
275
|
+
</div>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
This is useful when you have existing global functions and want to use them with Helium without passing them through the `helium()` initialization.
|
|
279
|
+
|
|
280
|
+
**Example:**
|
|
281
|
+
```html
|
|
282
|
+
<script>
|
|
283
|
+
function greet(name) {
|
|
284
|
+
alert(`Hello, ${name}!`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
window.appConfig = {
|
|
288
|
+
version: '1.0.0',
|
|
289
|
+
apiUrl: 'https://api.example.com'
|
|
290
|
+
};
|
|
291
|
+
</script>
|
|
292
|
+
|
|
293
|
+
<div @import="greet,appConfig">
|
|
294
|
+
<button @click="greet('World')">Greet</button>
|
|
295
|
+
<p @text="appConfig.version"></p>
|
|
296
|
+
</div>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Alias:** `data-he-import`
|
|
300
|
+
|
|
164
301
|
## Event Listeners & Handlers
|
|
165
302
|
|
|
166
303
|
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.
|
|
167
304
|
|
|
168
305
|
```html
|
|
169
306
|
<button @click="count++">Increment</button>
|
|
307
|
+
<input @input="search = $event.target.value">
|
|
308
|
+
<form @submit.prevent="handleSubmit()">
|
|
170
309
|
```
|
|
171
310
|
|
|
311
|
+
**Common Events:**
|
|
312
|
+
- `@click` - Mouse click
|
|
313
|
+
- `@input` - Input value changed
|
|
314
|
+
- `@change` - Input value committed (blur for text, immediate for select/checkbox)
|
|
315
|
+
- `@submit` - Form submission
|
|
316
|
+
- `@keydown` / `@keyup` / `@keypress` - Keyboard events
|
|
317
|
+
- `@mouseenter` / `@mouseleave` - Mouse hover
|
|
318
|
+
- `@focus` / `@blur` - Focus events
|
|
319
|
+
|
|
172
320
|
### Event Modifiers
|
|
173
321
|
|
|
174
322
|
You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
175
323
|
|
|
176
|
-
- **prevent** - Prevents the default browser behavior
|
|
177
|
-
- **once** - Only runs the event handler once
|
|
324
|
+
- **prevent** - Prevents the default browser behavior (e.g., form submission, link navigation)
|
|
325
|
+
- **once** - Only runs the event handler once, then removes the listener
|
|
178
326
|
- **outside** - Only fires when the event happens outside the element
|
|
179
327
|
- **document** - Attaches the listener to the document instead of the element
|
|
180
328
|
- **debounce** - Debounces the event handler (default 300ms)
|
|
181
329
|
- **debounce:500** - Debounces with custom delay in milliseconds
|
|
182
330
|
- **shift, ctrl, alt, meta** - Only fires if the modifier key is pressed
|
|
183
|
-
- **Key names** - For keyboard events, specify which key (e.g.,
|
|
331
|
+
- **Key names** - For keyboard events, specify which key (e.g., `enter`, `esc`, `space`)
|
|
184
332
|
|
|
185
|
-
Examples
|
|
333
|
+
**Examples:**
|
|
186
334
|
|
|
187
335
|
```html
|
|
188
|
-
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
336
|
+
<!-- Prevent form submission -->
|
|
337
|
+
<form @submit.prevent="handleSubmit()">
|
|
338
|
+
<button>Save</button>
|
|
339
|
+
</form>
|
|
340
|
+
|
|
341
|
+
<!-- Run only once -->
|
|
342
|
+
<button @click.once="initialize()">Initialize (once)</button>
|
|
343
|
+
|
|
344
|
+
<!-- Close modal when clicking outside -->
|
|
345
|
+
<div @click.outside="open = false" @hidden="!open">
|
|
346
|
+
<p>Click outside to close</p>
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<!-- Debounced search -->
|
|
350
|
+
<input @input.debounce:500="performSearch()" placeholder="Search...">
|
|
351
|
+
|
|
352
|
+
<!-- Keyboard shortcuts -->
|
|
192
353
|
<input @keydown.enter="submit()">
|
|
193
|
-
<input @keydown.
|
|
354
|
+
<input @keydown.esc="cancel()">
|
|
355
|
+
<div @keydown.ctrl.s.prevent="save()">Press Ctrl+S to save</div>
|
|
356
|
+
|
|
357
|
+
<!-- Modifier keys -->
|
|
358
|
+
<div @click.shift="console.log('Shift+Click!')">Shift-click me</div>
|
|
359
|
+
|
|
360
|
+
<!-- Listen on document level -->
|
|
361
|
+
<div @keydown.document.esc="closeModal()">Press ESC anywhere</div>
|
|
194
362
|
```
|
|
195
363
|
|
|
196
364
|
**Alias:** Prepend the event name with `data-he-on`, for example `data-he-onclick="count++"`
|
|
197
365
|
|
|
198
366
|
## HTTP Requests
|
|
199
367
|
|
|
200
|
-
Helium includes built-in support for making HTTP requests directly from event handlers.
|
|
368
|
+
Helium includes built-in support for making HTTP requests directly from event handlers. This makes it easy to load data, submit forms, and update parts of your page without writing fetch code.
|
|
201
369
|
|
|
202
370
|
### Available HTTP Methods
|
|
203
371
|
|
|
@@ -207,49 +375,202 @@ Helium includes built-in support for making HTTP requests directly from event ha
|
|
|
207
375
|
- `@patch` - PATCH request
|
|
208
376
|
- `@delete` - DELETE request
|
|
209
377
|
|
|
210
|
-
The HTTP method is triggered by the element's default event
|
|
378
|
+
The HTTP method is triggered by the element's default event:
|
|
379
|
+
- Buttons: `click`
|
|
380
|
+
- Forms: `submit`
|
|
381
|
+
- Inputs/Textareas: `input`
|
|
382
|
+
- Selects: `change`
|
|
383
|
+
|
|
384
|
+
**Simple Examples:**
|
|
211
385
|
|
|
212
386
|
```html
|
|
387
|
+
<!-- Load data on button click -->
|
|
213
388
|
<button @get="/api/data">Load Data</button>
|
|
214
|
-
|
|
389
|
+
|
|
390
|
+
<!-- Submit form -->
|
|
391
|
+
<form @post="/api/users">
|
|
392
|
+
<input name="username">
|
|
393
|
+
<button>Submit</button>
|
|
394
|
+
</form>
|
|
395
|
+
|
|
396
|
+
<!-- Delete on click -->
|
|
397
|
+
<button @delete="/api/users/123">Delete User</button>
|
|
215
398
|
```
|
|
216
399
|
|
|
217
|
-
### HTTP Request
|
|
400
|
+
### HTTP Request Attributes
|
|
218
401
|
|
|
219
402
|
Configure requests using these additional attributes:
|
|
220
403
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
-
|
|
225
|
-
-
|
|
226
|
-
-
|
|
404
|
+
#### @target
|
|
405
|
+
|
|
406
|
+
Specifies where to insert the response. Can be:
|
|
407
|
+
- A CSS selector (e.g., `#result`, `.container`)
|
|
408
|
+
- A ref (e.g., `$myElement`)
|
|
409
|
+
- A variable name (response will be stored in state)
|
|
410
|
+
|
|
411
|
+
```html
|
|
412
|
+
<button @get="/api/users" @target="#user-list">Load Users</button>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Multiple Targets:**
|
|
416
|
+
You can specify multiple targets with different actions using comma-separated values:
|
|
417
|
+
|
|
418
|
+
```html
|
|
419
|
+
<button
|
|
420
|
+
@get="/api/stats"
|
|
421
|
+
@target="#count, #chart, #message">
|
|
422
|
+
Load Stats
|
|
423
|
+
</button>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
#### :action
|
|
427
|
+
|
|
428
|
+
An action can be appended to the target to specify how to insert the response into the target
|
|
429
|
+
|
|
430
|
+
The following actions can all be used:
|
|
431
|
+
|
|
432
|
+
- `:replace` - Replace the entire element
|
|
433
|
+
- `:append` - Append to the end of the element's children
|
|
434
|
+
- `:prepend` - Prepend to the beginning of the element's children
|
|
435
|
+
- `:before` - Insert before the element
|
|
436
|
+
- `:after` - Insert after the element
|
|
437
|
+
|
|
438
|
+
If omitted, defaults to replacing the innerHTML.
|
|
227
439
|
|
|
228
440
|
```html
|
|
229
441
|
<button
|
|
230
442
|
@get="/api/users"
|
|
231
|
-
@target="#user-list"
|
|
232
|
-
|
|
233
|
-
Load Users
|
|
443
|
+
@target="#user-list:append"
|
|
444
|
+
Load More Users
|
|
234
445
|
</button>
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
#### @params
|
|
449
|
+
|
|
450
|
+
Specifies the request parameters. Can be:
|
|
451
|
+
- An object literal
|
|
452
|
+
- A reference to a variable
|
|
453
|
+
- FormData (automatically for forms)
|
|
454
|
+
- A shorthand syntax
|
|
235
455
|
|
|
236
|
-
|
|
456
|
+
**Object Literal:**
|
|
457
|
+
```html
|
|
458
|
+
<button
|
|
237
459
|
@post="/api/users"
|
|
238
|
-
@params="{ name: username, email: email }"
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
460
|
+
@params="{ name: username, email: email }">
|
|
461
|
+
Create User
|
|
462
|
+
</button>
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
**Shorthand Syntax:**
|
|
466
|
+
If the element has a `name` attribute, Helium automatically extracts its value:
|
|
467
|
+
|
|
468
|
+
```html
|
|
469
|
+
<!-- Automatically sends { username: [input value] } -->
|
|
470
|
+
<input name="username" @bind="username">
|
|
471
|
+
<button @post="/api/save" name="username">Save</button>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
For checkboxes:
|
|
475
|
+
```html
|
|
476
|
+
<!-- Sends { agreed: true/false } -->
|
|
477
|
+
<input type="checkbox" name="agreed">
|
|
478
|
+
<button @post="/api/consent" name="agreed">Submit</button>
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Nested Object Syntax:**
|
|
482
|
+
```html
|
|
483
|
+
<!-- Creates { user: { name: [value] } } -->
|
|
484
|
+
<button @post="/api/save" @params="user:name:value" name="value">
|
|
485
|
+
Save
|
|
486
|
+
</button>
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**FormData Example:**
|
|
490
|
+
```html
|
|
491
|
+
<form @post="/api/upload">
|
|
492
|
+
<input type="file" name="avatar">
|
|
493
|
+
<input type="text" name="caption">
|
|
494
|
+
<button>Upload</button>
|
|
244
495
|
</form>
|
|
245
496
|
```
|
|
246
497
|
|
|
498
|
+
#### @template
|
|
499
|
+
|
|
500
|
+
A JavaScript function that transforms the response before inserting it:
|
|
501
|
+
|
|
502
|
+
```html
|
|
503
|
+
<button
|
|
504
|
+
@get="/api/users"
|
|
505
|
+
@target="#list"
|
|
506
|
+
@template="(data) => data.map(u => `<li>${u.name}</li>`).join('')">
|
|
507
|
+
Load Users
|
|
508
|
+
</button>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
#### @loading
|
|
512
|
+
|
|
513
|
+
Content to show while the request is in progress:
|
|
514
|
+
|
|
515
|
+
```html
|
|
516
|
+
<button
|
|
517
|
+
@get="/api/users"
|
|
518
|
+
@target="#list"
|
|
519
|
+
@loading="<div class='spinner'>Loading...</div>">
|
|
520
|
+
Load Users
|
|
521
|
+
</button>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### @options
|
|
525
|
+
|
|
526
|
+
Additional fetch options (as an object):
|
|
527
|
+
|
|
528
|
+
```html
|
|
529
|
+
<button
|
|
530
|
+
@get="/api/users"
|
|
531
|
+
@options="{ cache: 'no-cache' }">
|
|
532
|
+
Load Users
|
|
533
|
+
</button>
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Alias:** All HTTP attributes have `data-he-` aliases:
|
|
537
|
+
- `data-he-target`
|
|
538
|
+
- `data-he-params`
|
|
539
|
+
- `data-he-template`
|
|
540
|
+
- `data-he-loading`
|
|
541
|
+
- `data-he-options`
|
|
542
|
+
|
|
543
|
+
### Complete Example
|
|
544
|
+
|
|
545
|
+
```html
|
|
546
|
+
<form @post="/api/users" @target="#result" @loading="Saving...">
|
|
547
|
+
<input @bind="username" placeholder="Username">
|
|
548
|
+
<input @bind="email" placeholder="Email">
|
|
549
|
+
<button @params="{ name: username, email: email }">Create User</button>
|
|
550
|
+
</form>
|
|
551
|
+
|
|
552
|
+
<div id="result"></div>
|
|
553
|
+
```
|
|
554
|
+
|
|
247
555
|
### Special Features
|
|
248
556
|
|
|
249
|
-
- Automatically includes CSRF tokens from `<meta name="csrf-token">` for same-origin requests
|
|
250
|
-
- Supports Turbo Stream responses
|
|
251
|
-
-
|
|
252
|
-
- Works with
|
|
557
|
+
- **CSRF Protection:** Automatically includes CSRF tokens from `<meta name="csrf-token">` for same-origin requests
|
|
558
|
+
- **Turbo Streams:** Supports Turbo Stream responses for Rails applications
|
|
559
|
+
- **Content Type Detection:** Automatically handles JSON and HTML responses
|
|
560
|
+
- **FormData:** Works seamlessly with file uploads and multipart forms
|
|
561
|
+
- **Same-Origin Credentials:** Automatically includes cookies for same-origin requests
|
|
562
|
+
|
|
563
|
+
**CSRF Token Example:**
|
|
564
|
+
```html
|
|
565
|
+
<head>
|
|
566
|
+
<meta name="csrf-token" content="your-token-here">
|
|
567
|
+
</head>
|
|
568
|
+
|
|
569
|
+
<!-- Token automatically included in same-origin POST requests -->
|
|
570
|
+
<form @post="/api/users">
|
|
571
|
+
<button>Submit</button>
|
|
572
|
+
</form>
|
|
573
|
+
```
|
|
253
574
|
|
|
254
575
|
## Dynamic Attributes
|
|
255
576
|
|
|
@@ -263,48 +584,127 @@ In the following example, the `<div>` element has a dynamic class attribute that
|
|
|
263
584
|
</div>
|
|
264
585
|
```
|
|
265
586
|
|
|
587
|
+
**Any HTML attribute can be dynamic:**
|
|
588
|
+
|
|
589
|
+
```html
|
|
590
|
+
<input :placeholder="'Enter ' + fieldName">
|
|
591
|
+
<button :disabled="!isValid">Submit</button>
|
|
592
|
+
<a :href="'/users/' + userId">View Profile</a>
|
|
593
|
+
<img :src="imageUrl" :alt="imageDescription">
|
|
594
|
+
```
|
|
595
|
+
|
|
266
596
|
### Special Dynamic Attributes
|
|
267
597
|
|
|
268
598
|
**:class** - Can accept an object to toggle multiple classes:
|
|
269
599
|
|
|
270
600
|
```html
|
|
271
|
-
<div :class="{
|
|
601
|
+
<div :class="{
|
|
602
|
+
active: isActive,
|
|
603
|
+
disabled: !isEnabled,
|
|
604
|
+
'has-error': errorMessage
|
|
605
|
+
}"></div>
|
|
272
606
|
```
|
|
273
607
|
|
|
608
|
+
This is more convenient than ternary operators when you need to toggle multiple classes.
|
|
609
|
+
|
|
274
610
|
**:style** - Can accept an object for multiple styles:
|
|
275
611
|
|
|
276
612
|
```html
|
|
277
|
-
<div :style="{
|
|
613
|
+
<div :style="{
|
|
614
|
+
color: textColor,
|
|
615
|
+
fontSize: size + 'px',
|
|
616
|
+
display: isVisible ? 'block' : 'none'
|
|
617
|
+
}"></div>
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
You can also use a string:
|
|
621
|
+
```html
|
|
622
|
+
<div :style="'color: ' + color + '; font-size: ' + size + 'px'"></div>
|
|
278
623
|
```
|
|
279
624
|
|
|
280
625
|
**Alias:** `data-he-attr:attributeName`
|
|
281
626
|
|
|
627
|
+
**Example:**
|
|
628
|
+
```html
|
|
629
|
+
<button data-he-attr:disabled="!isValid">Submit</button>
|
|
630
|
+
```
|
|
631
|
+
|
|
282
632
|
## Magic Variables
|
|
283
633
|
|
|
284
634
|
These special variables are available in all JavaScript expressions:
|
|
285
635
|
|
|
286
|
-
|
|
287
|
-
|
|
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:
|
|
636
|
+
### $
|
|
637
|
+
Alias for `document.querySelector` - quickly select elements:
|
|
295
638
|
|
|
296
639
|
```html
|
|
297
640
|
<div @click="$('#header').classList.add('active')">Activate Header!</div>
|
|
641
|
+
<button @click="$('.sidebar').style.display = 'none'">Hide Sidebar</button>
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### $el
|
|
645
|
+
Reference to the current element:
|
|
646
|
+
|
|
647
|
+
```html
|
|
298
648
|
<div @click="$el.remove()">Click to remove me!</div>
|
|
649
|
+
<button @click="$el.classList.toggle('active')">Toggle Active</button>
|
|
650
|
+
<input @input="console.log($el.value)">
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### $event
|
|
654
|
+
The event object (available in event handlers):
|
|
655
|
+
|
|
656
|
+
```html
|
|
299
657
|
<div @click="console.log($event.timeStamp)">Log the timestamp</div>
|
|
658
|
+
<input @keydown="$event.key === 'Enter' && submit()">
|
|
659
|
+
<form @submit="$event.preventDefault(); handleSubmit()">
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### $data
|
|
663
|
+
The reactive data object containing all Helium variables:
|
|
664
|
+
|
|
665
|
+
```html
|
|
300
666
|
<div @click="console.log($data)">Log all data</div>
|
|
667
|
+
<button @click="localStorage.setItem('state', JSON.stringify($data))">
|
|
668
|
+
Save State
|
|
669
|
+
</button>
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
This is particularly useful when passing to functions (see "Default Variables and Functions" section).
|
|
673
|
+
|
|
674
|
+
### $html
|
|
675
|
+
Helper function to create HTML elements from strings:
|
|
676
|
+
|
|
677
|
+
```html
|
|
678
|
+
<button @click="$list.appendChild($html('<li>New item</li>'))">
|
|
679
|
+
Add Item
|
|
680
|
+
</button>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
### $get, $post, $put, $patch, $delete
|
|
684
|
+
HTTP request functions that can be called programmatically:
|
|
685
|
+
|
|
686
|
+
```html
|
|
687
|
+
<button @click="$get('/api/data', '#result')">Load Data</button>
|
|
688
|
+
<button @click="$post('/api/users', { name: username }, { target: '#result' })">
|
|
689
|
+
Create User
|
|
690
|
+
</button>
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### $refs
|
|
694
|
+
Object containing all elements marked with `@ref` (prefixed with `$`):
|
|
695
|
+
|
|
696
|
+
```html
|
|
697
|
+
<input @ref="username">
|
|
698
|
+
<button @click="console.log($username.value)">Log Username</button>
|
|
301
699
|
```
|
|
302
700
|
|
|
303
701
|
## Default Variables and Functions
|
|
304
702
|
|
|
305
703
|
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.
|
|
306
704
|
|
|
307
|
-
|
|
705
|
+
### Setting Default Values
|
|
706
|
+
|
|
707
|
+
The following will set the count variable to an initial value of 29 and the name variable to "Helium":
|
|
308
708
|
|
|
309
709
|
```javascript
|
|
310
710
|
helium({
|
|
@@ -313,7 +713,16 @@ helium({
|
|
|
313
713
|
})
|
|
314
714
|
```
|
|
315
715
|
|
|
316
|
-
|
|
716
|
+
These variables will be available in all Helium expressions:
|
|
717
|
+
|
|
718
|
+
```html
|
|
719
|
+
<p @text="count"></p> <!-- Shows: 29 -->
|
|
720
|
+
<p @text="name"></p> <!-- Shows: Helium -->
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
### Adding Functions
|
|
724
|
+
|
|
725
|
+
You can add functions that can be called from event handlers and other expressions:
|
|
317
726
|
|
|
318
727
|
```javascript
|
|
319
728
|
helium({
|
|
@@ -321,73 +730,435 @@ helium({
|
|
|
321
730
|
const li = document.createElement("li")
|
|
322
731
|
li.textContent = "New Item"
|
|
323
732
|
element.append(li)
|
|
733
|
+
},
|
|
734
|
+
|
|
735
|
+
formatCurrency(amount) {
|
|
736
|
+
return new Intl.NumberFormat('en-US', {
|
|
737
|
+
style: 'currency',
|
|
738
|
+
currency: 'USD'
|
|
739
|
+
}).format(amount)
|
|
324
740
|
}
|
|
325
741
|
})
|
|
326
742
|
```
|
|
327
743
|
|
|
328
|
-
|
|
744
|
+
Using these functions:
|
|
329
745
|
|
|
330
746
|
```html
|
|
331
747
|
<ul @ref="list"></ul>
|
|
332
748
|
<button @click="appendTo($list)">Append item to list</button>
|
|
749
|
+
|
|
750
|
+
<div @data="{ price: 19.99 }">
|
|
751
|
+
<p @text="formatCurrency(price)"></p> <!-- Shows: $19.99 -->
|
|
752
|
+
</div>
|
|
333
753
|
```
|
|
334
754
|
|
|
335
|
-
### Important Note About Functions
|
|
755
|
+
### Important Note About Functions and Reactivity
|
|
336
756
|
|
|
337
|
-
Magic variables and Helium variables are not available inside these functions
|
|
757
|
+
**Magic variables and Helium variables are not available inside these functions by default.** However, you can pass them as arguments.
|
|
338
758
|
|
|
339
|
-
|
|
759
|
+
❌ **This won't work as expected:**
|
|
340
760
|
|
|
341
|
-
|
|
761
|
+
```javascript
|
|
762
|
+
helium({
|
|
763
|
+
increment(n = 1) {
|
|
764
|
+
count += n // 'count' is not defined in this scope
|
|
765
|
+
}
|
|
766
|
+
})
|
|
767
|
+
```
|
|
342
768
|
|
|
343
769
|
```html
|
|
344
|
-
<button @click="increment(
|
|
770
|
+
<button @click="increment()">Increment Count</button>
|
|
345
771
|
```
|
|
346
772
|
|
|
773
|
+
✅ **Instead, pass variables as arguments:**
|
|
774
|
+
|
|
775
|
+
**Option 1: Pass specific variables**
|
|
347
776
|
```javascript
|
|
348
777
|
helium({
|
|
349
|
-
increment(
|
|
350
|
-
|
|
778
|
+
increment(currentCount, n = 1) {
|
|
779
|
+
return currentCount + n
|
|
351
780
|
}
|
|
352
781
|
})
|
|
353
782
|
```
|
|
354
783
|
|
|
355
|
-
You should do this instead:
|
|
356
|
-
|
|
357
784
|
```html
|
|
358
|
-
<button @click="increment(
|
|
785
|
+
<button @click="count = increment(count)">Increment Count</button>
|
|
359
786
|
```
|
|
360
787
|
|
|
788
|
+
**Option 2: Pass $data for reactive updates**
|
|
789
|
+
|
|
790
|
+
This is the recommended approach when you need to update variables:
|
|
791
|
+
|
|
361
792
|
```javascript
|
|
362
793
|
helium({
|
|
363
794
|
increment(data, n = 1) {
|
|
364
795
|
data.count += n // Will trigger reactivity
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
resetAll(data) {
|
|
799
|
+
data.count = 0
|
|
800
|
+
data.name = ''
|
|
801
|
+
data.items = []
|
|
365
802
|
}
|
|
366
803
|
})
|
|
367
804
|
```
|
|
368
805
|
|
|
806
|
+
```html
|
|
807
|
+
<button @click="increment($data)">Increment Count</button>
|
|
808
|
+
<button @click="resetAll($data)">Reset Everything</button>
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
**Why pass $data?** When you update properties of the `$data` object, Helium's reactivity system detects the changes and updates the UI accordingly.
|
|
812
|
+
|
|
369
813
|
## Advanced Features
|
|
370
814
|
|
|
371
|
-
### DOM Morphing
|
|
815
|
+
### DOM Morphing with Idiomorph
|
|
816
|
+
|
|
817
|
+
By default, when you update innerHTML with `@html`, Helium replaces the entire content. This can cause issues like losing focus, resetting scroll positions, or interrupting animations.
|
|
372
818
|
|
|
373
|
-
If
|
|
819
|
+
If you include [Idiomorph](https://github.com/bigskysoftware/idiomorph), Helium will automatically use it for efficient DOM updates:
|
|
374
820
|
|
|
375
|
-
|
|
821
|
+
```html
|
|
822
|
+
<script src="https://unpkg.com/idiomorph@0.3.0/dist/idiomorph.min.js"></script>
|
|
823
|
+
<script type="module">
|
|
824
|
+
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';
|
|
825
|
+
helium();
|
|
826
|
+
</script>
|
|
827
|
+
```
|
|
376
828
|
|
|
377
|
-
|
|
829
|
+
**Benefits:**
|
|
830
|
+
- Preserves focus on input elements
|
|
831
|
+
- Maintains scroll positions
|
|
832
|
+
- Reduces flicker and improves perceived performance
|
|
833
|
+
- Keeps CSS animations running smoothly
|
|
378
834
|
|
|
835
|
+
**Example:**
|
|
379
836
|
```html
|
|
380
|
-
<
|
|
837
|
+
<div @html="items.map(i => `<div>${i}</div>`)">
|
|
838
|
+
<!-- Content morphs smoothly without full replacement -->
|
|
839
|
+
</div>
|
|
381
840
|
```
|
|
382
841
|
|
|
842
|
+
### List Rendering with Keys
|
|
843
|
+
|
|
844
|
+
When rendering lists with `@html`, you can add `key` or `data-key` attributes to help Helium (and Idiomorph) efficiently track and update individual items:
|
|
845
|
+
|
|
846
|
+
```html
|
|
847
|
+
<ul @html="items.map(item => `
|
|
848
|
+
<li key='${item.id}'>
|
|
849
|
+
${item.name}
|
|
850
|
+
</li>
|
|
851
|
+
`)"></ul>
|
|
852
|
+
```
|
|
853
|
+
|
|
854
|
+
Without keys, the entire list is re-rendered. With keys, only changed items are updated.
|
|
855
|
+
|
|
383
856
|
### MutationObserver
|
|
384
857
|
|
|
385
|
-
Helium automatically observes the DOM and processes new elements as they're added
|
|
858
|
+
Helium automatically observes the DOM and processes new elements as they're added. This means Helium works seamlessly with:
|
|
859
|
+
|
|
860
|
+
- Dynamically inserted content
|
|
861
|
+
- Content loaded via AJAX
|
|
862
|
+
- Third-party widgets that inject HTML
|
|
863
|
+
- Turbo/Hotwire page updates
|
|
864
|
+
|
|
865
|
+
**Example:**
|
|
866
|
+
```html
|
|
867
|
+
<div id="container"></div>
|
|
868
|
+
|
|
869
|
+
<script>
|
|
870
|
+
// This will automatically work with Helium
|
|
871
|
+
document.getElementById('container').innerHTML = `
|
|
872
|
+
<button @click="count++">Click me</button>
|
|
873
|
+
<span @text="count">0</span>
|
|
874
|
+
`;
|
|
875
|
+
</script>
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
### Integration with Turbo/Hotwire
|
|
879
|
+
|
|
880
|
+
Helium automatically integrates with Turbo Drive:
|
|
881
|
+
|
|
882
|
+
- Cleans up listeners before page navigation (`turbo:before-render`)
|
|
883
|
+
- Re-initializes after page loads (`turbo:render`)
|
|
884
|
+
|
|
885
|
+
No additional configuration needed - just use Helium with Turbo normally.
|
|
886
|
+
|
|
887
|
+
### Manual Cleanup
|
|
888
|
+
|
|
889
|
+
If you need to manually clean up Helium (for example, when unmounting a section of your page):
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
window.heliumTeardown()
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
This will:
|
|
896
|
+
- Disconnect the MutationObserver
|
|
897
|
+
- Remove all event listeners
|
|
898
|
+
- Clear all internal state
|
|
899
|
+
|
|
900
|
+
To reinitialize after teardown:
|
|
901
|
+
```javascript
|
|
902
|
+
helium()
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
## Security Considerations
|
|
906
|
+
|
|
907
|
+
### XSS Prevention
|
|
908
|
+
|
|
909
|
+
When using `@html`, be very careful with user-generated content:
|
|
910
|
+
|
|
911
|
+
❌ **Dangerous:**
|
|
912
|
+
```html
|
|
913
|
+
<div @html="userComment"></div>
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
✅ **Safe:**
|
|
917
|
+
```html
|
|
918
|
+
<!-- Use @text for user content -->
|
|
919
|
+
<div @text="userComment"></div>
|
|
920
|
+
|
|
921
|
+
<!-- Or sanitize first -->
|
|
922
|
+
<div @html="DOMPurify.sanitize(userComment)"></div>
|
|
923
|
+
```
|
|
386
924
|
|
|
387
925
|
### CSRF Protection
|
|
388
926
|
|
|
389
|
-
Helium automatically includes CSRF tokens
|
|
927
|
+
Helium automatically includes CSRF tokens for same-origin requests:
|
|
928
|
+
|
|
929
|
+
```html
|
|
930
|
+
<head>
|
|
931
|
+
<meta name="csrf-token" content="your-token-here">
|
|
932
|
+
</head>
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
The token is automatically included in POST, PUT, PATCH, and DELETE requests to the same origin.
|
|
936
|
+
|
|
937
|
+
### Content Security Policy
|
|
938
|
+
|
|
939
|
+
If you're using a Content Security Policy, note that Helium uses `new Function()` to evaluate expressions. You'll need to allow `unsafe-eval` or use a build step to pre-compile expressions (coming in a future version).
|
|
940
|
+
|
|
941
|
+
## Best Practices
|
|
942
|
+
|
|
943
|
+
### Performance Tips
|
|
944
|
+
|
|
945
|
+
**Use @calculate for derived values:**
|
|
946
|
+
|
|
947
|
+
```html
|
|
948
|
+
<!-- Good: Calculated once, updates automatically -->
|
|
949
|
+
<div @calculate:total="items.reduce((sum, item) => sum + item.price, 0)"></div>
|
|
950
|
+
<div @text="total"></div>
|
|
951
|
+
|
|
952
|
+
<!-- Avoid: Recalculates on every render -->
|
|
953
|
+
<div @text="items.reduce((sum, item) => sum + item.price, 0)"></div>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**Debounce expensive operations:**
|
|
957
|
+
|
|
958
|
+
```html
|
|
959
|
+
<input @input.debounce:500="search()" placeholder="Search...">
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
**Use @effect for side effects:**
|
|
963
|
+
|
|
964
|
+
```html
|
|
965
|
+
<!-- Persist to localStorage when username changes -->
|
|
966
|
+
<div @effect:username="localStorage.setItem('user', username)"></div>
|
|
967
|
+
|
|
968
|
+
<!-- Track analytics on state changes -->
|
|
969
|
+
<div @effect:page="analytics.track('page_view', { page })"></div>
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Structuring Larger Apps
|
|
973
|
+
|
|
974
|
+
**Organize state at the root:**
|
|
975
|
+
|
|
976
|
+
```html
|
|
977
|
+
<div @helium @data="{
|
|
978
|
+
user: { name: '', email: '' },
|
|
979
|
+
cart: { items: [], total: 0 },
|
|
980
|
+
ui: { modal: false, loading: false }
|
|
981
|
+
}">
|
|
982
|
+
<!-- Child elements can access all state -->
|
|
983
|
+
</div>
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**Use refs for complex interactions:**
|
|
987
|
+
|
|
988
|
+
```html
|
|
989
|
+
<div @ref="modal" @hidden="!showModal" class="modal">
|
|
990
|
+
<button @click="$modal.close()">Close</button>
|
|
991
|
+
</div>
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
**Break down complex expressions:**
|
|
995
|
+
|
|
996
|
+
```html
|
|
997
|
+
<!-- Instead of complex inline logic -->
|
|
998
|
+
<div @html="items.filter(i => i.active).map(i => `<li>${i.name}</li>`).join('')"></div>
|
|
999
|
+
|
|
1000
|
+
<!-- Use @calculate to break it down -->
|
|
1001
|
+
<div @calculate:activeItems="items.filter(i => i.active)"></div>
|
|
1002
|
+
<div @html="activeItems.map(i => `<li>${i.name}</li>`).join('')"></div>
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
### Debugging Tips
|
|
1006
|
+
|
|
1007
|
+
**Inspect state with @effect:**
|
|
1008
|
+
|
|
1009
|
+
```html
|
|
1010
|
+
<div @effect:*="console.log('State changed:', $data)"></div>
|
|
1011
|
+
```
|
|
1012
|
+
|
|
1013
|
+
**Use @init for debugging:**
|
|
1014
|
+
|
|
1015
|
+
```html
|
|
1016
|
+
<div @init="console.log('Helium initialized', $data)"></div>
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
**Check element references:**
|
|
1020
|
+
|
|
1021
|
+
```html
|
|
1022
|
+
<div @ref="myElement"></div>
|
|
1023
|
+
<button @click="console.log($myElement)">Inspect Element</button>
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
### Common Pitfalls
|
|
1027
|
+
|
|
1028
|
+
**❌ Don't mutate arrays/objects without triggering reactivity:**
|
|
1029
|
+
|
|
1030
|
+
```javascript
|
|
1031
|
+
helium({
|
|
1032
|
+
addItem(items, item) {
|
|
1033
|
+
items.push(item); // ❌ Won't trigger updates
|
|
1034
|
+
}
|
|
1035
|
+
})
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
**✅ Pass $data and update through it:**
|
|
1039
|
+
|
|
1040
|
+
```javascript
|
|
1041
|
+
helium({
|
|
1042
|
+
addItem(data, item) {
|
|
1043
|
+
data.items.push(item); // ✅ Triggers updates
|
|
1044
|
+
}
|
|
1045
|
+
})
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
```html
|
|
1049
|
+
<button @click="addItem($data, newItem)">Add Item</button>
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
**❌ Don't use magic variables inside functions:**
|
|
1053
|
+
|
|
1054
|
+
```javascript
|
|
1055
|
+
helium({
|
|
1056
|
+
badFunction() {
|
|
1057
|
+
console.log($data); // ❌ $data is undefined
|
|
1058
|
+
}
|
|
1059
|
+
})
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
**✅ Pass them as arguments:**
|
|
1063
|
+
|
|
1064
|
+
```javascript
|
|
1065
|
+
helium({
|
|
1066
|
+
goodFunction(data) {
|
|
1067
|
+
console.log(data); // ✅ Works!
|
|
1068
|
+
}
|
|
1069
|
+
})
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
```html
|
|
1073
|
+
<button @click="goodFunction($data)">Works!</button>
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
## Security Considerations
|
|
1077
|
+
|
|
1078
|
+
### XSS Protection
|
|
1079
|
+
|
|
1080
|
+
**Always sanitize user input when using @html:**
|
|
1081
|
+
|
|
1082
|
+
```html
|
|
1083
|
+
<!-- ❌ Dangerous if userInput contains scripts -->
|
|
1084
|
+
<div @html="userInput"></div>
|
|
1085
|
+
|
|
1086
|
+
<!-- ✅ Sanitize first -->
|
|
1087
|
+
<div @html="sanitize(userInput)"></div>
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
Consider using a sanitization library like [DOMPurify](https://github.com/cure53/DOMPurify):
|
|
1091
|
+
|
|
1092
|
+
```javascript
|
|
1093
|
+
import DOMPurify from 'dompurify';
|
|
1094
|
+
|
|
1095
|
+
helium({
|
|
1096
|
+
sanitize(html) {
|
|
1097
|
+
return DOMPurify.sanitize(html);
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
**Use @text for plain text:**
|
|
1103
|
+
|
|
1104
|
+
```html
|
|
1105
|
+
<!-- ✅ Safe - automatically escapes HTML -->
|
|
1106
|
+
<div @text="userInput"></div>
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
### CSRF Protection
|
|
1110
|
+
|
|
1111
|
+
Helium automatically includes CSRF tokens in same-origin requests. Add this meta tag to your HTML:
|
|
1112
|
+
|
|
1113
|
+
```html
|
|
1114
|
+
<meta name="csrf-token" content="your-csrf-token">
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
All POST, PUT, PATCH, and DELETE requests will include the `X-CSRF-Token` header automatically.
|
|
1118
|
+
|
|
1119
|
+
### Content Security Policy
|
|
1120
|
+
|
|
1121
|
+
If you're using a strict CSP, you may need to allow `'unsafe-eval'` since Helium uses `new Function()` to compile expressions, or use a hash/nonce for the script.
|
|
1122
|
+
|
|
1123
|
+
## Error Handling
|
|
1124
|
+
|
|
1125
|
+
### JavaScript Expression Errors
|
|
1126
|
+
|
|
1127
|
+
If an expression throws an error, Helium catches it silently and continues. Check the browser console for error messages.
|
|
1128
|
+
|
|
1129
|
+
```html
|
|
1130
|
+
<!-- If items is undefined, this won't crash the page -->
|
|
1131
|
+
<div @text="items.length"></div>
|
|
1132
|
+
```
|
|
1133
|
+
|
|
1134
|
+
### HTTP Request Errors
|
|
1135
|
+
|
|
1136
|
+
Failed requests log errors to the console. Handle them in your expressions:
|
|
1137
|
+
|
|
1138
|
+
```html
|
|
1139
|
+
<button
|
|
1140
|
+
@post="/api/save"
|
|
1141
|
+
@params="{ data: formData }"
|
|
1142
|
+
@target="#message">
|
|
1143
|
+
Save
|
|
1144
|
+
</button>
|
|
1145
|
+
|
|
1146
|
+
<div id="message" @html="saveError || 'Ready to save'"></div>
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Invalid Attribute Syntax
|
|
1150
|
+
|
|
1151
|
+
Helium gracefully handles invalid syntax. If an expression can't be compiled, it treats it as a literal value.
|
|
1152
|
+
|
|
1153
|
+
|
|
1154
|
+
## Contributing
|
|
1155
|
+
|
|
1156
|
+
Helium is open source! Contributions, issues, and feature requests are welcome.
|
|
1157
|
+
|
|
1158
|
+
- GitHub: [github.com/daz-codes/helium](https://github.com/daz-codes/helium)
|
|
1159
|
+
- Report issues: Create an issue on GitHub
|
|
1160
|
+
- Suggest features: Open a discussion on GitHub
|
|
390
1161
|
|
|
391
|
-
##
|
|
1162
|
+
## License
|
|
392
1163
|
|
|
393
|
-
|
|
1164
|
+
MIT License - feel free to use Helium in personal and commercial projects.
|
package/helium.js
CHANGED
|
@@ -32,44 +32,48 @@ window.helium = function() {
|
|
|
32
32
|
const $ = s => document.querySelector(s);
|
|
33
33
|
const html = s => Object.assign(document.createElement("template"),{innerHTML:s.trim()}).content.firstChild
|
|
34
34
|
|
|
35
|
-
const update = (data,
|
|
36
|
-
const
|
|
35
|
+
const update = (data,targets,actions,template) => {
|
|
36
|
+
const newTargets = [];
|
|
37
|
+
targets.forEach((target,i) => {
|
|
38
|
+
const element = target instanceof Node ? target : (HELIUM.refs.get(target.trim()) || $(target.trim()));
|
|
37
39
|
if(element){
|
|
38
|
-
const content =
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
const content = template ? template(data) : data;
|
|
41
|
+
actions[i] ? element[actions[i]=="replace"?"replaceWith":actions[i]](html(content)) : element.innerHTML = content;
|
|
42
|
+
newTargets.push(actions[i] ? content : element)
|
|
41
43
|
} else state[target] = data
|
|
44
|
+
})
|
|
45
|
+
return newTargets
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
const ajax = (
|
|
45
|
-
if(
|
|
46
|
-
const fd =
|
|
47
|
-
const
|
|
48
|
-
const sameOrigin =
|
|
49
|
-
fetch(
|
|
50
|
-
method
|
|
48
|
+
const ajax = (url,method,options={},params={}) => {
|
|
49
|
+
if(options.loading) options.target = update(options.loading,options.target,options.action) || options.target;
|
|
50
|
+
const fd = params instanceof FormData, token = document.querySelector('meta[name="csrf-token"]')?.content;
|
|
51
|
+
const path = new URL(url, window.location.href);
|
|
52
|
+
const sameOrigin = path.origin === window.location.origin;
|
|
53
|
+
fetch(url, {
|
|
54
|
+
method,
|
|
51
55
|
headers: {
|
|
52
56
|
Accept:"text/vnd.turbo-stream.html,application/json,text/html",
|
|
53
|
-
...(!fd &&
|
|
54
|
-
...(sameOrigin &&
|
|
57
|
+
...(!fd && method !== "GET" && {"Content-Type":"application/json"}),
|
|
58
|
+
...(sameOrigin && token ? {"X-CSRF-Token": token} : {})
|
|
55
59
|
},
|
|
56
|
-
body:
|
|
60
|
+
body: method === "GET" ? null : (fd ? params : JSON.stringify(parmas)),
|
|
57
61
|
credentials: sameOrigin ? "same-origin" : "omit"
|
|
58
62
|
})
|
|
59
|
-
.then(
|
|
60
|
-
const type =
|
|
61
|
-
return (type.includes("turbo-stream") ?
|
|
62
|
-
type.includes("json") ?
|
|
63
|
-
|
|
64
|
-
}).then(
|
|
65
|
-
|
|
66
|
-
? Turbo.renderStreamMessage(
|
|
67
|
-
: update(
|
|
63
|
+
.then(res => {
|
|
64
|
+
const type = res.headers.get("content-type") || "";
|
|
65
|
+
return (type.includes("turbo-stream") ? res.text().then(data => ({ turbo: true, data })) :
|
|
66
|
+
type.includes("json") ? res.json() :
|
|
67
|
+
res.text());
|
|
68
|
+
}).then(data =>
|
|
69
|
+
data.turbo && "Turbo"
|
|
70
|
+
? Turbo.renderStreamMessage(data.data)
|
|
71
|
+
: update(data, options.target, options.loading ? options.action.map(a => a && "replace") : options.action, options.template)
|
|
68
72
|
).catch(e => console.error("AJAX:", e.message));
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
const get = (
|
|
72
|
-
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(
|
|
75
|
+
const get = (url,target) => ajax(url,"GET",target);
|
|
76
|
+
const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(method => (url, params, options) => ajax(url, method, options, params));
|
|
73
77
|
|
|
74
78
|
const handler = {
|
|
75
79
|
get(t,p,r) {
|
|
@@ -113,8 +117,7 @@ function applyBinding(b,e={},elCtx=b.el){
|
|
|
113
117
|
k.split(/\s+/).forEach(c => el.classList.toggle(c,v)));
|
|
114
118
|
|
|
115
119
|
if (prop==="style" && r && typeof r==="object")
|
|
116
|
-
|
|
117
|
-
.map(([k,v])=>`${k}:${v}`).join(";");
|
|
120
|
+
return el.style.cssText = Object.entries(r).map(([k,v])=>v?`${k}:${v}`:'').join(";");
|
|
118
121
|
|
|
119
122
|
if (prop in el) {
|
|
120
123
|
if (el.type === "radio" && prop != "checked") el.checked = el.value===r;
|
|
@@ -158,11 +161,7 @@ const trackDependencies = (fn, el, excludeChanged = false) => {
|
|
|
158
161
|
const trackProxy = new Proxy(state, {
|
|
159
162
|
get(target, prop) {
|
|
160
163
|
if (typeof prop == 'string') {
|
|
161
|
-
|
|
162
|
-
accessed.set(prop, target[prop]); // Store initial value
|
|
163
|
-
} else if (!excludeChanged) {
|
|
164
|
-
accessed.add(prop);
|
|
165
|
-
}
|
|
164
|
+
excludeChanged ? !accessed.has(prop) && accessed.set(prop, target[prop]) : accessed.add(prop);
|
|
166
165
|
}
|
|
167
166
|
const val = target[prop];
|
|
168
167
|
return typeof val == "object" && val != null ? new Proxy(val, this) : val;
|
|
@@ -171,10 +170,7 @@ const trackDependencies = (fn, el, excludeChanged = false) => {
|
|
|
171
170
|
|
|
172
171
|
try { fn.call(null, $, trackProxy, HELIUM.refs); } catch {}
|
|
173
172
|
|
|
174
|
-
|
|
175
|
-
return [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop));
|
|
176
|
-
}
|
|
177
|
-
return [...accessed];
|
|
173
|
+
return excludeChanged ? [...accessed.keys()].filter(prop => state[prop] === accessed.get(prop)) : [...accessed];
|
|
178
174
|
};
|
|
179
175
|
|
|
180
176
|
const cleanup = el => {
|
|
@@ -224,15 +220,13 @@ function processElements(element) {
|
|
|
224
220
|
Object.assign(state, parseEx(value));
|
|
225
221
|
}
|
|
226
222
|
else if (name.startsWith(":") || name.startsWith("data-he-attr:")) {
|
|
227
|
-
|
|
228
|
-
deferredBindings.push({el, prop: name.slice(name.startsWith(":") ? 1 : 13), fn});
|
|
223
|
+
deferredBindings.push({el, prop: name.slice(name.startsWith(":") ? 1 : 13), fn: compile(value, true)});
|
|
229
224
|
}
|
|
230
225
|
else if (he(name, "ref")) {
|
|
231
226
|
HELIUM.refs.set("$" + value, el);
|
|
232
227
|
}
|
|
233
228
|
else if (he(name, "text", "html")) {
|
|
234
|
-
|
|
235
|
-
deferredBindings.push({el, prop: he(name, "text") ? "textContent" : "innerHTML", fn});
|
|
229
|
+
deferredBindings.push({el, prop: he(name, "text") ? "textContent" : "innerHTML", fn: compile(value, true)});
|
|
236
230
|
}
|
|
237
231
|
else if (he(name, "bind")) {
|
|
238
232
|
const event = (isCheckbox || isRadio || isSelect) ? "change" : "input";
|
|
@@ -241,24 +235,22 @@ function processElements(element) {
|
|
|
241
235
|
el.addEventListener(event, inputHandler);
|
|
242
236
|
if (!HELIUM.listeners.has(el)) HELIUM.listeners.set(el, []);
|
|
243
237
|
HELIUM.listeners.get(el).push({receiver: el, event, handler: inputHandler});
|
|
244
|
-
|
|
238
|
+
deferredBindings.push({el, prop, fn: compile(value, true)});
|
|
245
239
|
if (isCheckbox) el.checked = !!state[value];
|
|
246
240
|
else if (isRadio) el.checked = el.value == state[value];
|
|
247
241
|
else el.value = state[value] ?? "";
|
|
248
242
|
}
|
|
249
243
|
else if (he(name, "hidden", "visible")) {
|
|
250
|
-
|
|
251
|
-
deferredBindings.push({el, prop: "hidden", fn});
|
|
244
|
+
deferredBindings.push({el, prop: "hidden", fn: compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true)});
|
|
252
245
|
}
|
|
253
246
|
else if (he(name, "calculate")) {
|
|
254
|
-
|
|
255
|
-
const fn = compile(value, true);
|
|
256
|
-
deferredBindings.push({el, calc, prop: null, fn});
|
|
247
|
+
deferredBindings.push({el, calc: name.split(":")[1], prop: null, fn: compile(value, true)});
|
|
257
248
|
}
|
|
258
249
|
else if (he(name, "effect")) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
250
|
+
deferredBindings.push({el, prop: null, fn: compile(value, true), keys: name.split(":").slice(1)});
|
|
251
|
+
}
|
|
252
|
+
else if (he(name, "import")) {
|
|
253
|
+
value.split(",").map(s => s.trim()).forEach(v => state[v] = window[v]);
|
|
262
254
|
}
|
|
263
255
|
else if (he(name, "init")) {
|
|
264
256
|
initFn = compile(value, true);
|
|
@@ -286,7 +278,9 @@ function processElements(element) {
|
|
|
286
278
|
if (!mods.includes("outside") || !el.contains(e.target)) {
|
|
287
279
|
if (isHttpMethod) {
|
|
288
280
|
const getAttr = name => el.getAttribute(`data-he-${name}`) || el.getAttribute(`@${name}`);
|
|
289
|
-
const
|
|
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);
|
|
290
284
|
const options = {
|
|
291
285
|
...(getAttr("options") && parseEx(getAttr("options") || "{}")),
|
|
292
286
|
...(target && { target }),
|
|
@@ -305,9 +299,7 @@ function processElements(element) {
|
|
|
305
299
|
}
|
|
306
300
|
const params = exFn(paramsAttr);
|
|
307
301
|
ajax(value, eventName.toUpperCase(), options, params);
|
|
308
|
-
} else
|
|
309
|
-
exFn(value);
|
|
310
|
-
}
|
|
302
|
+
} else exFn(value);
|
|
311
303
|
}
|
|
312
304
|
if (mods.includes("once")) receiver.removeEventListener(event, handler);
|
|
313
305
|
};
|