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