@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.
Files changed (3) hide show
  1. package/README.md +855 -77
  2. package/helium.js +29 -38
  3. 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/YPwwdBK)
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 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,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., `@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.
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 (click for buttons, submit for forms, input for inputs, etc.).
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
- <form @post="/api/users">Submit</form>
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 Options
402
+ ### HTTP Request Attributes
218
403
 
219
404
  Configure requests using these additional attributes:
220
405
 
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
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
- @action="replace">
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
- <form
458
+ **Object Literal:**
459
+ ```html
460
+ <button
237
461
  @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>
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
- - Handles JSON and HTML responses
252
- - Works with FormData for file uploads
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="{ active: isActive, disabled: !isEnabled }"></div>
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="{ color: textColor, fontSize: size + 'px' }"></div>
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
- - **$** - 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:
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
- For example, the following will set the count variable to an initial value of 29 and the name variable to "Helium":
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
- The following example shows how a function can be added into Helium and then used by event listeners:
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
- This function can then be called from an event handler, such as `@click`:
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. However, you can pass them as arguments and they will then be available in the function.
764
+ **Magic variables and Helium variables are not available inside these functions by default.** However, you can pass them as arguments.
338
765
 
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.
766
+ ❌ **This won't work as expected:**
340
767
 
341
- So instead of this:
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(count)">Increment Count</button>
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(count, n = 1) {
350
- count += n // Won't trigger reactivity
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($data)">Increment Count</button>
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
- If Idiomorph is available, Helium will use it for efficient DOM updates when updating innerHTML, preserving element state and reducing flicker.
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 arrays with `@html`, Helium can efficiently update lists by using `key` or `data-key` attributes on child elements for tracking.
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 => `<li key='${item.id}'>${item.name}</li>`)"></ul>
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, making it work seamlessly with dynamically inserted content.
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 from `<meta name="csrf-token">` elements in same-origin requests for enhanced security.
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
- ## Browser Compatibility
1169
+ ## License
392
1170
 
393
- Helium uses modern JavaScript features including Proxy, MutationObserver, and ES6 syntax. It works in all modern browsers that support ES6 modules.
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.forEach((target,i) => {
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 = (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) 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(params)),
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)});
@@ -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 = (getAttr('target') || "").split(",").map(p => p.split(":").map(s => s.trim()));
291
- const target = pairs.map(([target]) => target);
292
- const action = pairs.map(([, action]) => action);
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 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "scripts": {