@daz4126/helium 0.21.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.
Files changed (3) hide show
  1. package/README.md +847 -76
  2. package/helium.js +25 -34
  3. 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 text inputs, textareas, checkboxes, radio buttons, and select elements.
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, open: false }"></div>
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. For example, this will create a reference called `$list` to this element:
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`, for example:
182
+ This element can then be accessed in other JavaScript expressions as `$list`:
121
183
 
122
184
  ```html
123
- <button @click="appendTo($list)">Add Task</button>
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., `@keydown.enter`, `@keyup.esc`)
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
- <button @click.prevent="submitForm()">Save</button>
189
- <button @click.once="initialize()">Initialize</button>
190
- <div @click.outside="closeModal()">Modal</div>
191
- <input @input.debounce:500="search()">
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.ctrl.s.prevent="save()">
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 (click for buttons, submit for forms, input for inputs, etc.).
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
- <form @post="/api/users">Submit</form>
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 Options
400
+ ### HTTP Request Attributes
218
401
 
219
402
  Configure requests using these additional attributes:
220
403
 
221
- - **@target** or **data-he-target** - Where to insert the response (CSS selector or ref)
222
- - **@action** or **data-he-action** - How to insert: `replace`, `append`, `prepend`, `before`, `after`
223
- - **@params** or **data-he-params** - Request parameters (object or FormData)
224
- - **@options** or **data-he-options** - Additional fetch options
225
- - **@template** or **data-he-template** - Template function to transform response
226
- - **@loading** or **data-he-loading** - Loading state content to show during request
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
- @action="replace">
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
- <form
456
+ **Object Literal:**
457
+ ```html
458
+ <button
237
459
  @post="/api/users"
238
- @params="{ name: username, email: email }"
239
- @target="#message"
240
- @loading="Saving...">
241
- <input @bind="username">
242
- <input @bind="email">
243
- <button>Save</button>
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
- - Handles JSON and HTML responses
252
- - Works with FormData for file uploads
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="{ active: isActive, disabled: !isEnabled }"></div>
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="{ color: textColor, fontSize: size + 'px' }"></div>
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
- - **$** - Alias for `document.querySelector`
287
- - **$el** - Reference to the current element
288
- - **$event** - The event object (in event handlers)
289
- - **$data** - The reactive data object containing all Helium variables
290
- - **$html** - Helper function to create HTML elements from strings
291
- - **$get, $post, $put, $patch, $delete** - HTTP request functions
292
- - **$refs** - All elements marked with `@ref`
293
-
294
- Examples:
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
- For example, the following will set the count variable to an initial value of 29 and the name variable to "Helium":
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
- The following example shows how a function can be added into Helium and then used by event listeners:
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
- This function can then be called from an event handler, such as `@click`:
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. However, you can pass them as arguments and they will then be available in the function.
757
+ **Magic variables and Helium variables are not available inside these functions by default.** However, you can pass them as arguments.
338
758
 
339
- If you pass Helium variables, they will be passed as values and updating them inside the function will **not** trigger a reactive update. A solution is to pass the magic `$data` attribute as an argument, then updating the properties of this inside the function **will** trigger a reactive update.
759
+ ❌ **This won't work as expected:**
340
760
 
341
- So instead of this:
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(count)">Increment Count</button>
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(count, n = 1) {
350
- count += n // Won't trigger reactivity
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($data)">Increment Count</button>
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 Idiomorph is available, Helium will use it for efficient DOM updates when updating innerHTML, preserving element state and reducing flicker.
819
+ If you include [Idiomorph](https://github.com/bigskysoftware/idiomorph), Helium will automatically use it for efficient DOM updates:
374
820
 
375
- ### List Rendering
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
- When rendering arrays with `@html`, Helium can efficiently update lists by using `key` or `data-key` attributes on child elements for tracking.
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
- <ul @html="items.map(item => `<li key='${item.id}'>${item.name}</li>`)"></ul>
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, making it work seamlessly with dynamically inserted content.
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 from `<meta name="csrf-token">` elements in same-origin requests for enhanced security.
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
- ## Browser Compatibility
1162
+ ## License
392
1163
 
393
- Helium uses modern JavaScript features including Proxy, MutationObserver, and ES6 syntax. It works in all modern browsers that support ES6 modules.
1164
+ MIT License - feel free to use Helium in personal and commercial projects.
package/helium.js CHANGED
@@ -45,35 +45,35 @@ window.helium = function() {
45
45
  return newTargets
46
46
  }
47
47
 
48
- const ajax = (u,m,o={},p={}) => {
49
- if(o.loading) o.target = update(o.loading,o.target,o.action) || o.target;
50
- const fd = p instanceof FormData, t = document.querySelector('meta[name="csrf-token"]')?.content;
51
- const url = new URL(u, window.location.href);
52
- const sameOrigin = url.origin === window.location.origin;
53
- fetch(u, {
54
- method: m,
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,
55
55
  headers: {
56
56
  Accept:"text/vnd.turbo-stream.html,application/json,text/html",
57
- ...(!fd && m !== "GET" && {"Content-Type":"application/json"}),
58
- ...(sameOrigin && t ? {"X-CSRF-Token": t} : {})
57
+ ...(!fd && method !== "GET" && {"Content-Type":"application/json"}),
58
+ ...(sameOrigin && token ? {"X-CSRF-Token": token} : {})
59
59
  },
60
- body: m === "GET" ? null : (fd ? p : JSON.stringify(p)),
60
+ body: method === "GET" ? null : (fd ? params : JSON.stringify(parmas)),
61
61
  credentials: sameOrigin ? "same-origin" : "omit"
62
62
  })
63
- .then(r => {
64
- const type = r.headers.get("content-type") || "";
65
- return (type.includes("turbo-stream") ? r.text().then(d => ({ t: true, d })) :
66
- type.includes("json") ? r.json() :
67
- r.text());
68
- }).then(d =>
69
- d.t && "Turbo"
70
- ? Turbo.renderStreamMessage(d.d)
71
- : update(d, o.target, o.loading ? o.action.map(a => a && "replace") : o.action, o.template)
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 = (u,t) => ajax(u,"GET",t);
76
- const [post, put, patch, del] = ["POST","PUT","PATCH","DELETE"].map(m => (u, d, t) => ajax(u, m, t, d));
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
- return el.style = Object.entries(r).filter(([,v])=>v)
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
- if (excludeChanged && !accessed.has(prop)) {
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
- if (excludeChanged) {
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
- const fn = compile(`${he(name, "hidden") ? "!" : ""}!(${value})`, true);
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)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {