@daz4126/helium 1.0.0-rc.2 → 1.0.0-rc.4
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 +340 -93
- package/dist/helium-csp-sse.min.js +1 -1
- package/dist/helium-csp.min.js +1 -1
- package/dist/helium-lite.min.js +1 -1
- package/dist/helium-sse.min.js +1 -1
- package/dist/helium.min.js +1 -1
- package/helium-csp.js +9 -3
- package/helium-lite.js +395 -64
- package/helium.js +607 -111
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ It's really simple to use - just sprinkle the magic @attributes into your HTML a
|
|
|
18
18
|
|
|
19
19
|
Helium is designed for developers who want:
|
|
20
20
|
|
|
21
|
-
- **Lightweight** - About
|
|
21
|
+
- **Lightweight** - About 6.2–14.2KB minified and gzipped, depending on the build
|
|
22
22
|
- **Powerful** - Declarative JavaScript in your HTML
|
|
23
23
|
- **Zero build step** - Works directly in the browser with no compiling
|
|
24
24
|
- **Easy to learn** - If you know HTML and basic JavaScript, you're ready
|
|
@@ -31,7 +31,7 @@ Helium comes in three core versions, with SSE available as an optional add-on, s
|
|
|
31
31
|
|
|
32
32
|
The full-featured version with everything included.
|
|
33
33
|
|
|
34
|
-
- All directives (`@text`, `@html`, `@for`, `@
|
|
34
|
+
- All directives (`@text`, `@html`, `@for`, `@if`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`, etc.)
|
|
35
35
|
- Event handlers with modifiers
|
|
36
36
|
- HTTP requests (`@get`, `@post`, `@put`, `@patch`, `@delete`)
|
|
37
37
|
- Module imports (`@import`)
|
|
@@ -51,7 +51,7 @@ A slimmed-down version with just the core reactivity features.
|
|
|
51
51
|
- Core directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`)
|
|
52
52
|
- Event handlers with modifiers
|
|
53
53
|
- One global state namespace
|
|
54
|
-
- No `@
|
|
54
|
+
- No local `@data`, keyed `@for`, HTTP requests, imports, SSE, or Turbo integration
|
|
55
55
|
|
|
56
56
|
**Best for:** Simple interactive pages where you only need reactivity and don't need server communication or module imports.
|
|
57
57
|
|
|
@@ -104,23 +104,25 @@ import helium from "@daz4126/helium/csp"
|
|
|
104
104
|
| Feature | Standard | Lite | CSP |
|
|
105
105
|
|---------|----------|------|-----|
|
|
106
106
|
| Core directives | ✅ | ✅ | ✅ |
|
|
107
|
-
| Element-local `@
|
|
107
|
+
| Element-local `@data` | ✅ | ❌ | ✅ |
|
|
108
108
|
| Keyed `@for` | ✅ | ❌ | ✅ |
|
|
109
|
+
| Conditional `@if` | ✅ | ❌ | ✅ |
|
|
109
110
|
| Event handlers | ✅ | ✅ | ✅ |
|
|
110
111
|
| HTTP requests | ✅ | ❌ | ✅ |
|
|
111
112
|
| `@import` | ✅ | ❌ | ✅ |
|
|
112
113
|
| SSE | Add-on | ❌ | Add-on |
|
|
113
114
|
| Turbo integration | ✅ | ❌ | ✅ |
|
|
114
115
|
| CSP safe | ❌ | ❌ | ✅ |
|
|
115
|
-
| Approx. min+gzip size |
|
|
116
|
+
| Approx. min+gzip size | 9.62KB | 6.18KB | 13.75KB |
|
|
116
117
|
|
|
117
118
|
Sizes are enforced against the production Terser builds with gzip level 9. The
|
|
118
|
-
CSP figure includes its expression parser. Adding SSE produces
|
|
119
|
-
and
|
|
119
|
+
CSP figure includes its expression parser. Adding SSE produces 10.00KB Standard
|
|
120
|
+
and 14.12KB CSP bundles.
|
|
120
121
|
|
|
121
122
|
Lite is generated from the same global-state reactive core as Standard. It has
|
|
122
|
-
identical deep/shared reactivity, lifecycle, binding, and cleanup behavior
|
|
123
|
-
intentionally excludes element-local
|
|
123
|
+
identical deep/shared reactivity, lifecycle, binding, and cleanup behavior for
|
|
124
|
+
root state, but intentionally excludes element-local data and Standard's other
|
|
125
|
+
extensions.
|
|
124
126
|
|
|
125
127
|
## Installation
|
|
126
128
|
|
|
@@ -300,6 +302,32 @@ but keyed `@for` avoids HTML-string interpolation and lets each row use normal
|
|
|
300
302
|
|
|
301
303
|
**Alias:** `data-he-for` (the key remains `:key`)
|
|
302
304
|
|
|
305
|
+
### @if
|
|
306
|
+
|
|
307
|
+
Standard and CSP can conditionally render a `<template>`'s content. Unlike
|
|
308
|
+
`@hidden`/`@visible`, which only toggle visibility, `@if` adds the content to
|
|
309
|
+
the DOM when the expression is truthy and fully removes it — bindings,
|
|
310
|
+
listeners, and `@init` cleanup included — when it becomes falsy:
|
|
311
|
+
|
|
312
|
+
```html
|
|
313
|
+
<template @if="loggedIn">
|
|
314
|
+
<p>Welcome back, <span @text="user.name"></span>!</p>
|
|
315
|
+
<button @click="logout($data)">Log out</button>
|
|
316
|
+
</template>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Content that is switched off costs nothing: its expressions never evaluate and
|
|
320
|
+
its listeners don't exist. Use `@if` for anything conditional-but-expensive
|
|
321
|
+
(modals, tab panels, admin-only markup) and `@hidden`/`@visible` for cheap
|
|
322
|
+
show/hide toggles where you want to preserve element state such as form input.
|
|
323
|
+
|
|
324
|
+
`@if` requires a `<template>` element (like `@for`, and like Alpine's `x-if`)
|
|
325
|
+
because template content is inert until inserted — that's what makes the "off"
|
|
326
|
+
state free. It cannot be combined with `@for` on the same template; nest one
|
|
327
|
+
template inside the other instead. Lite excludes this feature.
|
|
328
|
+
|
|
329
|
+
**Alias:** `data-he-if`
|
|
330
|
+
|
|
303
331
|
### @bind
|
|
304
332
|
|
|
305
333
|
Creates a 2-way binding between an input element's value and a variable. Whatever is entered in the following input field will be stored as a variable called name:
|
|
@@ -389,31 +417,23 @@ Makes the element hidden or visible depending on the result of a JavaScript expr
|
|
|
389
417
|
|
|
390
418
|
### @data
|
|
391
419
|
|
|
392
|
-
Initializes variables that can be used in JavaScript expressions.
|
|
420
|
+
Initializes variables that can be used in JavaScript expressions. In Standard
|
|
421
|
+
and CSP, `@data` behaves like Alpine's `x-data`: root-level `@data` initializes
|
|
422
|
+
the mounted root state, while nested `@data` creates local state for that
|
|
423
|
+
element and its descendants. Lite always uses one global state namespace, so
|
|
424
|
+
every `@data` merges into root state.
|
|
393
425
|
|
|
394
426
|
```html
|
|
395
|
-
<
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
You can then use these variables in other Helium attributes:
|
|
399
|
-
|
|
400
|
-
```html
|
|
401
|
-
<div @data="{ count: 0 }">
|
|
402
|
-
<button @click="count++">Increment</button>
|
|
403
|
-
<p @text="count"></p>
|
|
404
|
-
</div>
|
|
427
|
+
<main @helium @data="{ count: 100, name: 'Helium' }">
|
|
428
|
+
<p @text="name + ': ' + count"></p>
|
|
429
|
+
</main>
|
|
405
430
|
```
|
|
406
431
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
### @scope (Standard/CSP)
|
|
410
|
-
|
|
411
|
-
Creates reactive state owned by an element and its descendants. Scoped names
|
|
412
|
-
shadow root state without changing the object returned by `helium()`:
|
|
432
|
+
Nested `@data` is local in Standard/CSP:
|
|
413
433
|
|
|
414
434
|
```html
|
|
415
|
-
<main @data="{ count: 100 }">
|
|
416
|
-
<section @
|
|
435
|
+
<main @helium @data="{ count: 100 }">
|
|
436
|
+
<section @data="{ count: 0, open: false }">
|
|
417
437
|
<button @click="count++">Increment local count</button>
|
|
418
438
|
<button @click="open = !open">Toggle</button>
|
|
419
439
|
<p @text="count"></p>
|
|
@@ -424,41 +444,49 @@ shadow root state without changing the object returned by `helium()`:
|
|
|
424
444
|
</main>
|
|
425
445
|
```
|
|
426
446
|
|
|
427
|
-
The
|
|
428
|
-
values. Nested
|
|
429
|
-
|
|
447
|
+
The object is evaluated once in its parent context, so local defaults can use
|
|
448
|
+
outer values. Nested data objects inherit outer names, and assignment updates
|
|
449
|
+
the nearest data object that declares that name:
|
|
430
450
|
|
|
431
451
|
```html
|
|
432
|
-
<div @
|
|
433
|
-
<div @
|
|
452
|
+
<div @data="{ count: startingCount, label: 'outer' }">
|
|
453
|
+
<div @data="{ label: 'inner' }">
|
|
434
454
|
<button @click="count++">Updates the outer local count</button>
|
|
435
455
|
<span @text="label"></span> <!-- inner -->
|
|
436
456
|
</div>
|
|
437
457
|
</div>
|
|
438
458
|
```
|
|
439
459
|
|
|
440
|
-
Declare every name that should remain local. Assigning an undeclared name
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
`@data` continues to merge into root state; it has not changed semantics.
|
|
460
|
+
Declare every name that should remain local. Assigning an undeclared name writes
|
|
461
|
+
to root state. The same applies to `@bind` and `@calculate` targets: declare
|
|
462
|
+
their target names in local `@data` when they should be local. Inside the
|
|
463
|
+
subtree, `$data` is the scoped state view. Local values are not returned from
|
|
464
|
+
`helium()` and are not included in root `@local-storage` persistence.
|
|
447
465
|
|
|
448
|
-
**Alias:** `data-he-
|
|
449
|
-
|
|
450
|
-
`@scope` is intentionally excluded from Lite, which always uses one global
|
|
451
|
-
state namespace per mounted root.
|
|
466
|
+
**Alias:** `data-he-data` (the legacy bare `data-he` alias is also accepted)
|
|
452
467
|
|
|
453
468
|
### @ref
|
|
454
469
|
|
|
455
|
-
Creates a reference to
|
|
470
|
+
Creates a scoped reference to an element. The first matching element is
|
|
471
|
+
available through `$name`, and every matching element is available as the
|
|
472
|
+
always-array `$refs.name` in current DOM order:
|
|
456
473
|
|
|
457
474
|
```html
|
|
458
|
-
<
|
|
475
|
+
<section @data="{ items: [] }">
|
|
476
|
+
<ul @ref="list"></ul>
|
|
477
|
+
<div @ref="row"></div>
|
|
478
|
+
<div @ref="row"></div>
|
|
479
|
+
<button @click="console.log($list, $refs.row)">Inspect refs</button>
|
|
480
|
+
</section>
|
|
459
481
|
```
|
|
460
482
|
|
|
461
|
-
|
|
483
|
+
Ref lookup starts in the nearest explicit `@data` scope and falls back through
|
|
484
|
+
parent scopes. A ref with the same name in a nested scope shadows the outer
|
|
485
|
+
name. Lite has one root scope, so all of its refs belong to the mounted root.
|
|
486
|
+
When refs are removed or reordered, singular and plural lookup follows the
|
|
487
|
+
current DOM.
|
|
488
|
+
|
|
489
|
+
This element can be accessed in other JavaScript expressions as `$list`:
|
|
462
490
|
|
|
463
491
|
```html
|
|
464
492
|
<button @click="$list.appendChild($html('<li>New item</li>'))">Add Task</button>
|
|
@@ -648,11 +676,22 @@ You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
|
648
676
|
- **once** - Only runs the event handler once, then removes the listener
|
|
649
677
|
- **outside** - Only fires when the event happens outside the element
|
|
650
678
|
- **document** - Attaches the listener to the document instead of the element
|
|
679
|
+
- **window** - Attaches the listener to `window` instead of the element
|
|
680
|
+
- **self** - Only fires when the element itself is the event target
|
|
681
|
+
- **capture** - Handles the event during the capture phase
|
|
682
|
+
- **passive** - Registers a native passive listener
|
|
651
683
|
- **debounce** - Debounces the event handler (default 300ms)
|
|
652
684
|
- **debounce:500** - Debounces with custom delay in milliseconds
|
|
685
|
+
- **throttle** - Runs immediately, then at most once per interval (default 300ms)
|
|
686
|
+
- **throttle:500** - Throttles with a custom interval in milliseconds
|
|
653
687
|
- **shift, ctrl, alt, meta** - Only fires if the modifier key is pressed
|
|
654
688
|
- **Key names** - For keyboard events, specify which key (e.g., `enter`, `esc`, `space`)
|
|
655
689
|
|
|
690
|
+
Throttle uses leading and trailing calls: the first matching event runs
|
|
691
|
+
immediately, and events received during the interval are coalesced into one
|
|
692
|
+
final call using the latest event. `.passive.prevent` is invalid because a
|
|
693
|
+
passive listener cannot cancel an event; Helium warns and does not register it.
|
|
694
|
+
|
|
656
695
|
**Examples:**
|
|
657
696
|
|
|
658
697
|
```html
|
|
@@ -682,6 +721,12 @@ You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
|
682
721
|
|
|
683
722
|
<!-- Listen on document level -->
|
|
684
723
|
<div @keydown.document.esc="closeModal()">Press ESC anywhere</div>
|
|
724
|
+
|
|
725
|
+
<!-- Window listener with throttled updates -->
|
|
726
|
+
<div @resize.window.throttle:100="measureLayout()"></div>
|
|
727
|
+
|
|
728
|
+
<!-- Ignore clicks that originated in descendants -->
|
|
729
|
+
<button @click.self="select()"><span>Select</span></button>
|
|
685
730
|
```
|
|
686
731
|
|
|
687
732
|
**Alias:** Prepend the event name with `data-he-`, for example `data-he-click="count++"`
|
|
@@ -1000,13 +1045,26 @@ You can also use a string:
|
|
|
1000
1045
|
These special variables are available in all JavaScript expressions:
|
|
1001
1046
|
|
|
1002
1047
|
### $
|
|
1003
|
-
|
|
1048
|
+
The `$` query helper preserves `document.querySelector` as its default and adds
|
|
1049
|
+
array-returning plural and scoped forms:
|
|
1004
1050
|
|
|
1005
1051
|
```html
|
|
1006
1052
|
<div @click="$('#header').classList.add('active')">Activate Header!</div>
|
|
1007
1053
|
<button @click="$('.sidebar').style.display = 'none'">Hide Sidebar</button>
|
|
1008
1054
|
```
|
|
1009
1055
|
|
|
1056
|
+
```js
|
|
1057
|
+
$(selector) // first match in document
|
|
1058
|
+
$.all(selector) // all matches in document
|
|
1059
|
+
$.root(selector) // first descendant match in $root
|
|
1060
|
+
$.root.all(selector) // all descendant matches in $root
|
|
1061
|
+
$.scope(selector) // first descendant match in $scope
|
|
1062
|
+
$.scope.all(selector) // all descendant matches in $scope
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
The plural forms return normal arrays. Like `querySelector`, the root and scope
|
|
1066
|
+
forms search descendants and do not include `$root` or `$scope` themselves.
|
|
1067
|
+
|
|
1010
1068
|
### $el
|
|
1011
1069
|
Reference to the current element:
|
|
1012
1070
|
|
|
@@ -1016,15 +1074,41 @@ Reference to the current element:
|
|
|
1016
1074
|
<input @input="console.log($el.value)">
|
|
1017
1075
|
```
|
|
1018
1076
|
|
|
1077
|
+
### $root
|
|
1078
|
+
|
|
1079
|
+
The actual root element owned by the current Helium instance. This is the
|
|
1080
|
+
element selected by `@helium`/`data-helium`, the element passed to
|
|
1081
|
+
`helium.mount()`, or `document.body` for the default fallback root.
|
|
1082
|
+
|
|
1083
|
+
### $scope
|
|
1084
|
+
|
|
1085
|
+
The nearest element that declares local `@data` state, falling back to `$root`.
|
|
1086
|
+
Lite has no local data scopes, so `$scope` and `$root` are always the same.
|
|
1087
|
+
|
|
1019
1088
|
### $event
|
|
1020
1089
|
The event object (available in event handlers):
|
|
1021
1090
|
|
|
1022
1091
|
```html
|
|
1023
1092
|
<div @click="console.log($event.timeStamp)">Log the timestamp</div>
|
|
1024
1093
|
<input @keydown="$event.key === 'Enter' && submit()">
|
|
1025
|
-
<form @submit="
|
|
1094
|
+
<form @submit.prevent="handleSubmit()">
|
|
1026
1095
|
```
|
|
1027
1096
|
|
|
1097
|
+
### $dispatch
|
|
1098
|
+
|
|
1099
|
+
Dispatch a bubbling, cancelable `CustomEvent` from the current element. The
|
|
1100
|
+
event is returned, so callers can inspect `defaultPrevented`:
|
|
1101
|
+
|
|
1102
|
+
```html
|
|
1103
|
+
<section @cart:add="total += $event.detail.price">
|
|
1104
|
+
<button @click="$dispatch('cart:add', { price: 12 })">Add</button>
|
|
1105
|
+
</section>
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
The signature is `$dispatch(name, detail = {}, options = {})`. Options can set
|
|
1109
|
+
`target` (default `$el`), `bubbles` (default `true`), and `cancelable` (default
|
|
1110
|
+
`true`).
|
|
1111
|
+
|
|
1028
1112
|
### $data
|
|
1029
1113
|
The reactive data object containing all Helium variables:
|
|
1030
1114
|
|
|
@@ -1060,14 +1144,49 @@ The arguments are `url`,`params` (not for `$get`) and `options`. `options` is an
|
|
|
1060
1144
|
|
|
1061
1145
|
### Named refs
|
|
1062
1146
|
|
|
1063
|
-
|
|
1064
|
-
|
|
1147
|
+
The first element marked with `@ref="name"` in the nearest defining scope is
|
|
1148
|
+
available as `$name`. `$refs.name` is an array of every matching ref in that
|
|
1149
|
+
scope, in current DOM order:
|
|
1065
1150
|
|
|
1066
1151
|
```html
|
|
1067
1152
|
<input @ref="username">
|
|
1068
1153
|
<button @click="console.log($username.value)">Log Username</button>
|
|
1154
|
+
|
|
1155
|
+
<div @ref="item"></div>
|
|
1156
|
+
<div @ref="item"></div>
|
|
1157
|
+
<button @click="console.log($refs.item.length)">Log item count</button>
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
## Expression Contract
|
|
1161
|
+
|
|
1162
|
+
Directive values are **single expressions**, in both Standard and CSP. Common
|
|
1163
|
+
expressions include property reads, assignments, function calls, object and
|
|
1164
|
+
array literals, ternaries, logical operators, and arrow functions:
|
|
1165
|
+
|
|
1166
|
+
```html
|
|
1167
|
+
<button @click="open = !open">Toggle</button>
|
|
1168
|
+
<button @click="save($data, $el)">Save</button>
|
|
1169
|
+
<p @text="loading ? 'Loading…' : message"></p>
|
|
1069
1170
|
```
|
|
1070
1171
|
|
|
1172
|
+
Top-level statement lists, declarations, and control-flow statements are not
|
|
1173
|
+
directive syntax. Prefer event modifiers and move multi-step behavior into a
|
|
1174
|
+
named function:
|
|
1175
|
+
|
|
1176
|
+
```html
|
|
1177
|
+
<!-- Not supported: two top-level statements -->
|
|
1178
|
+
<form @submit="$event.preventDefault(); save()"></form>
|
|
1179
|
+
|
|
1180
|
+
<!-- Preferred -->
|
|
1181
|
+
<form @submit.prevent="save($data)"></form>
|
|
1182
|
+
```
|
|
1183
|
+
|
|
1184
|
+
The Standard build uses the JavaScript expression grammar. The CSP build uses
|
|
1185
|
+
its restricted expression parser and additionally excludes `new` and some
|
|
1186
|
+
JavaScript expression forms; see the CSP build notes above. Functions declared inside
|
|
1187
|
+
an `@data` object or imported from a module are ordinary JavaScript functions
|
|
1188
|
+
and may contain statements internally.
|
|
1189
|
+
|
|
1071
1190
|
## Functions
|
|
1072
1191
|
|
|
1073
1192
|
Functions can be imported using `@import` or defined using the `@data` attribute.
|
|
@@ -1164,6 +1283,29 @@ This is the recommended approach when you need to update variables:
|
|
|
1164
1283
|
|
|
1165
1284
|
## Advanced Features
|
|
1166
1285
|
|
|
1286
|
+
### Batched Updates
|
|
1287
|
+
|
|
1288
|
+
State changes are batched: a burst of synchronous mutations triggers a single
|
|
1289
|
+
DOM update on the next microtask, and each affected binding runs once no matter
|
|
1290
|
+
how many mutations fed it. This keeps loops over state cheap — updating 100
|
|
1291
|
+
items in a keyed list re-renders the list once, not 100 times:
|
|
1292
|
+
|
|
1293
|
+
```javascript
|
|
1294
|
+
// One DOM update, not 100
|
|
1295
|
+
for (let i = 0; i < 100; i++) state.items[i].done = true;
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
Two practical consequences:
|
|
1299
|
+
|
|
1300
|
+
- **Reading the DOM right after a mutation shows the old value.** Await a
|
|
1301
|
+
microtask first: `state.count = 5; await Promise.resolve();` (in tests, a
|
|
1302
|
+
`setTimeout(0)` tick also covers content inserted by `@if`/`@for`, which is
|
|
1303
|
+
processed asynchronously by the MutationObserver).
|
|
1304
|
+
- **Assigning a value that is already current is a no-op** — it doesn't
|
|
1305
|
+
re-trigger bindings, so effects that write unchanged state settle instead of
|
|
1306
|
+
looping. A binding cascade that *never* stops writing new values (e.g.
|
|
1307
|
+
`@effect:*="count++"`) is detected and reported after 100 passes.
|
|
1308
|
+
|
|
1167
1309
|
### DOM Morphing with Idiomorph
|
|
1168
1310
|
|
|
1169
1311
|
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.
|
|
@@ -1206,7 +1348,28 @@ Without keys, the entire list is re-rendered. With keys, only changed items are
|
|
|
1206
1348
|
|
|
1207
1349
|
### MutationObserver
|
|
1208
1350
|
|
|
1209
|
-
Helium automatically observes the DOM
|
|
1351
|
+
Helium automatically observes the DOM, processes new elements as they're added,
|
|
1352
|
+
and reconciles Helium directives when attributes on retained elements change.
|
|
1353
|
+
Adding, replacing, or removing an event, binding, ref, effect, initializer, or
|
|
1354
|
+
structural directive installs the new behavior and tears down the old behavior
|
|
1355
|
+
without rebuilding unrelated parts of the subtree.
|
|
1356
|
+
|
|
1357
|
+
For programmatic changes, use the `data-he-*` aliases: DOM `setAttribute()`
|
|
1358
|
+
rejects names such as `@click` in some browsers and DOM implementations even
|
|
1359
|
+
though the HTML parser accepts them.
|
|
1360
|
+
|
|
1361
|
+
```js
|
|
1362
|
+
button.setAttribute("data-he-click", "save()")
|
|
1363
|
+
button.setAttribute("data-he-click", "archive()") // replaces save
|
|
1364
|
+
button.removeAttribute("data-he-click") // removes the listener
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
Changing local `data-he-data` recreates that local scope and re-tracks its
|
|
1368
|
+
descendant bindings. HTTP option attributes such as `data-he-target` are read
|
|
1369
|
+
when the request runs, so their current values are used automatically. Root
|
|
1370
|
+
selection and `@local-storage` remain mount-time configuration.
|
|
1371
|
+
|
|
1372
|
+
This means Helium works seamlessly with:
|
|
1210
1373
|
|
|
1211
1374
|
- Dynamically inserted content
|
|
1212
1375
|
- Content loaded via AJAX
|
|
@@ -1306,6 +1469,18 @@ import helium from "@daz4126/helium/csp"
|
|
|
1306
1469
|
<div @effect:page="analytics.track('page_view', { page })"></div>
|
|
1307
1470
|
```
|
|
1308
1471
|
|
|
1472
|
+
**Benchmarking:**
|
|
1473
|
+
|
|
1474
|
+
The repo includes a head-to-head runtime benchmark against Alpine.js, React,
|
|
1475
|
+
and Datastar (keyed list create/update/swap/clear plus rapid counter updates,
|
|
1476
|
+
run in real Chromium; Datastar only runs the counter test since it renders
|
|
1477
|
+
lists server-side). Run it with:
|
|
1478
|
+
|
|
1479
|
+
```bash
|
|
1480
|
+
npm run bench # defaults: 1000 rows, 7 runs
|
|
1481
|
+
node scripts/benchmark.mjs --rows 5000 --runs 5
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1309
1484
|
### Structuring Larger Apps
|
|
1310
1485
|
|
|
1311
1486
|
**Organize state at the root:**
|
|
@@ -1411,7 +1586,11 @@ helium({
|
|
|
1411
1586
|
|
|
1412
1587
|
### JavaScript Expression Errors
|
|
1413
1588
|
|
|
1414
|
-
If a binding expression throws at runtime, Helium logs the error and continues
|
|
1589
|
+
If a binding expression throws at runtime, Helium logs the error and continues
|
|
1590
|
+
without updating that binding. Failed compilation keeps Helium's literal-string
|
|
1591
|
+
ergonomics: plain text such as `@text="Loading..."` stays quiet, while
|
|
1592
|
+
expression-looking failures such as `@text="user.name."` or
|
|
1593
|
+
`@click="count +"` emit a `console.warn` with the attribute and expression.
|
|
1415
1594
|
|
|
1416
1595
|
```html
|
|
1417
1596
|
<!-- If items is undefined, this won't crash the page -->
|
|
@@ -1434,8 +1613,10 @@ promises, so you can handle failures explicitly:
|
|
|
1434
1613
|
|
|
1435
1614
|
### Invalid Attribute Syntax
|
|
1436
1615
|
|
|
1437
|
-
|
|
1438
|
-
|
|
1616
|
+
Helium keeps plain malformed value expressions as literal values. If a failed
|
|
1617
|
+
expression looks like broken JavaScript, Helium emits a warning with directive
|
|
1618
|
+
context. Event handlers, HTTP options, `@data`, `@for`, `@if`, `@calculate`,
|
|
1619
|
+
`@effect`, and `@init` are treated strictly and warn on compile failure.
|
|
1439
1620
|
|
|
1440
1621
|
|
|
1441
1622
|
## Roadmap & Known Limitations
|
|
@@ -1445,43 +1626,106 @@ intentionally absent (or still on the roadmap). Current gaps:
|
|
|
1445
1626
|
|
|
1446
1627
|
### Not yet supported
|
|
1447
1628
|
|
|
1448
|
-
- **No DOM-removing `@if`** — only `@hidden`/`@visible`, which toggle visibility
|
|
1449
|
-
but keep elements in the DOM.
|
|
1450
1629
|
- **No `$watch`/`$nextTick`, and no cross-root shared store** — each mounted
|
|
1451
1630
|
instance has its own isolated state namespace.
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1631
|
+
- **No transition helper** — use CSS transitions/classes directly with
|
|
1632
|
+
`@hidden`, `@visible`, or `@if`.
|
|
1633
|
+
|
|
1634
|
+
### Stimulus-parity roadmap
|
|
1635
|
+
|
|
1636
|
+
The goal is not to copy Stimulus's controller syntax. Simple behavior should
|
|
1637
|
+
remain local (`@click="open = !open"`, `@bind="name"`); the roadmap supplies the
|
|
1638
|
+
lifecycle and JavaScript escape hatches needed when behavior becomes complex.
|
|
1639
|
+
|
|
1640
|
+
#### P0 — 1.0 release foundations
|
|
1641
|
+
|
|
1642
|
+
- [x] **Define the expression and function-context contract.** Directive values
|
|
1643
|
+
are expressions; multi-step work belongs in named functions with `$data`,
|
|
1644
|
+
`$el`, refs, and other required context passed explicitly.
|
|
1645
|
+
- [x] **Reconcile directive attributes on retained elements.** Attribute add,
|
|
1646
|
+
replace, and removal now install and tear down bindings, events, refs,
|
|
1647
|
+
initializers, local data, and structural directives independently.
|
|
1648
|
+
- [x] **Run the production browser suite in Chromium, Firefox, and WebKit.** Keep
|
|
1649
|
+
the same CSP, Turbo, SSE, keyed identity, dynamic-directive, late-import,
|
|
1650
|
+
Lite, and entry-point checks in every engine.
|
|
1651
|
+
|
|
1652
|
+
#### P1 — complex-behavior escape hatches
|
|
1653
|
+
|
|
1654
|
+
1. **Formalize lifecycle modules over `@init`.** Document the existing
|
|
1655
|
+
`@import` + `@init` setup-function convention: imported setup receives its
|
|
1656
|
+
required element, state, refs, and dispatch context explicitly and returns
|
|
1657
|
+
cleanup. Keep ordinary behavior inline and move only complex imperative work
|
|
1658
|
+
into a lifecycle module.
|
|
1659
|
+
|
|
1660
|
+
```js
|
|
1661
|
+
export function connectFeature(element, data, refs) {
|
|
1662
|
+
// setup
|
|
1663
|
+
return () => {
|
|
1664
|
+
// cleanup
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
2. **Formally support and test namespaced initializers.** Allow independent
|
|
1670
|
+
behaviors such as `@init:editor` and `@init:tooltip` on one element, with
|
|
1671
|
+
isolated replacement and cleanup when either attribute changes.
|
|
1672
|
+
|
|
1673
|
+
```html
|
|
1674
|
+
<div
|
|
1675
|
+
@init:editor="connectEditor($el, $data, $refs)"
|
|
1676
|
+
@init:tooltip="connectTooltip($el)"
|
|
1677
|
+
></div>
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
3. **Add unified lifecycle error handling.** Route initializer setup, async
|
|
1681
|
+
rejection, cleanup, binding, event, import, and request failures through an
|
|
1682
|
+
instance-level `handleError(error, context)` hook and optional debug mode.
|
|
1683
|
+
4. **Add an initializer-scoped `AbortSignal`.** Give each initializer a signal
|
|
1684
|
+
that aborts when its attribute is replaced, removed, its element disconnects,
|
|
1685
|
+
or its Helium root unmounts, so listeners and pending external work can use
|
|
1686
|
+
native cancellation.
|
|
1687
|
+
5. **Support asynchronous setup and cleanup safely.** Recognize setup promises,
|
|
1688
|
+
avoid registering stale results after disconnection or replacement, run a
|
|
1689
|
+
cleanup returned asynchronously, and report rejected setup/cleanup through
|
|
1690
|
+
the lifecycle error hook.
|
|
1691
|
+
6. **Add programmatic `watch()`.** Let imported setup functions observe reactive
|
|
1692
|
+
state without exporting update functions or storing external instances in a
|
|
1693
|
+
module-level `WeakMap`; include current/previous values and lifecycle-bound
|
|
1694
|
+
cleanup. Keep middleware separate and lower priority.
|
|
1695
|
+
7. **Publish TypeScript declarations** for mounting, factories, expression
|
|
1696
|
+
engines, CSP globals, SSE, lifecycle errors, initializer context, signals,
|
|
1697
|
+
and watching.
|
|
1698
|
+
|
|
1699
|
+
#### P2 — lifecycle depth and application maturity
|
|
1700
|
+
|
|
1701
|
+
8. **Observe ref collections** so complex behaviors can react when matching
|
|
1702
|
+
refs connect, disconnect, or reorder; this covers Stimulus target callbacks
|
|
1703
|
+
without changing simple `$ref`/`$refs` usage.
|
|
1704
|
+
9. **Complete the HTTP lifecycle:** request cancellation with `AbortController`,
|
|
1705
|
+
explicit success/error hooks, consistent `@loading` restoration, and opt-in
|
|
1706
|
+
required-CSRF validation for state-changing same-origin requests.
|
|
1707
|
+
10. **Evaluate behavior lookup only after lifecycle modules are established.**
|
|
1708
|
+
Prefer `$dispatch` for loose coordination; add outlet-like instance lookup
|
|
1709
|
+
only if real applications require direct behavior-to-behavior calls.
|
|
1710
|
+
|
|
1711
|
+
#### P3 — evidence-driven conveniences
|
|
1712
|
+
|
|
1713
|
+
11. **Document complete validation and error-state patterns** covering native
|
|
1714
|
+
constraint validation, HTTP failure, retry, accessible messages, and stale
|
|
1715
|
+
error clearing before adding validation helpers.
|
|
1716
|
+
12. **Consider extensible event modifiers and structured action parameters.**
|
|
1717
|
+
Expressions already cover most cases, so add APIs only for repeated needs.
|
|
1718
|
+
13. **Revisit `@use`, debounced binding, middleware, transitions, and a shared
|
|
1719
|
+
store** only with concrete use cases and explicit size budgets. Event
|
|
1720
|
+
debouncing, CSS transitions, custom events, and independent roots cover the
|
|
1721
|
+
common forms today. Add `@use` only if lifecycle modules reveal a need for
|
|
1722
|
+
behavior registration, private instances, or package interoperability
|
|
1723
|
+
beyond namespaced `@init`.
|
|
1724
|
+
|
|
1725
|
+
This ordering moves the former HTTP/form-heavy roadmap behind lifecycle,
|
|
1726
|
+
diagnostics, watching, and types. The HTTP, CSRF, loading, validation, and error
|
|
1727
|
+
example work remains planned, but it does not by itself close the most important
|
|
1728
|
+
Stimulus-parity gaps.
|
|
1485
1729
|
|
|
1486
1730
|
## Contributing
|
|
1487
1731
|
|
|
@@ -1491,9 +1735,12 @@ Helium is open source! Contributions, issues, and feature requests are welcome.
|
|
|
1491
1735
|
- Report issues: Create an issue on GitHub
|
|
1492
1736
|
- Suggest features: Open a discussion on GitHub
|
|
1493
1737
|
|
|
1494
|
-
Before submitting a change, run `npm run check`. It executes the full test
|
|
1495
|
-
suite, creates the production bundles, and fails if any gzip
|
|
1496
|
-
exceeded. `npm run
|
|
1738
|
+
Before submitting a change, run `npm run check`. It executes the full jsdom test
|
|
1739
|
+
suite, creates the production bundles, smoke-tests them, and fails if any gzip
|
|
1740
|
+
size budget is exceeded. `npm run test:browser` runs the cross-browser smoke suite
|
|
1741
|
+
used by CI for CSP, Turbo, SSE, keyed focus preservation, late imports, Lite
|
|
1742
|
+
generation, and browser entry imports. `npm run check:ci` runs both paths.
|
|
1743
|
+
`npm run size` also reports raw, gzip, and Brotli sizes.
|
|
1497
1744
|
|
|
1498
1745
|
`helium.js` contains the shared runtime plus sections marked Standard-only.
|
|
1499
1746
|
`npm run generate` derives `helium-lite.js`; do not edit the generated Lite file
|