@daz4126/helium 1.0.0-rc.1 → 1.0.0-rc.2
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 +355 -82
- package/dist/helium-csp-sse.min.js +1 -0
- package/dist/helium-csp.min.js +1 -0
- package/dist/helium-lite.min.js +1 -0
- package/dist/helium-sse.min.js +1 -0
- package/dist/helium.min.js +1 -0
- package/helium-csp-sse.js +6 -0
- package/helium-csp.js +102 -23
- package/helium-lite.js +528 -407
- package/helium-sse.js +61 -0
- package/helium.js +587 -198
- package/jexpr.js +14 -0
- package/package.json +32 -4
- package/.claude/settings.local.json +0 -18
- package/CLAUDE.md +0 -135
- package/LLM_GUIDE.md +0 -138
- package/examples/index-lite.html +0 -396
- package/examples/index.html +0 -486
- package/examples/password-utils.js +0 -80
- package/examples/sse-demo.html +0 -125
- package/examples/sse-server.js +0 -138
- package/examples/sse-simple.html +0 -49
- package/examples/test-password.html +0 -251
- package/examples/todo.css +0 -109
- package/examples/todo.html +0 -46
- package/examples/todo.js +0 -36
- package/helium-csp.test.js +0 -776
- package/helium-lite.test.js +0 -120
- package/helium.test.js +0 -870
- package/jexpr.test.js +0 -551
- package/test-csp.html +0 -50
- package/vitest.config.js +0 -8
package/README.md
CHANGED
|
@@ -18,24 +18,24 @@ 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** -
|
|
21
|
+
- **Lightweight** - About 3.9–10.9KB 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
|
|
25
25
|
|
|
26
26
|
## Versions
|
|
27
27
|
|
|
28
|
-
Helium comes in three versions so you can pick the right balance of features and size for your project:
|
|
28
|
+
Helium comes in three core versions, with SSE available as an optional add-on, so you can pick the right balance of features and size for your project:
|
|
29
29
|
|
|
30
30
|
### Helium (Standard)
|
|
31
31
|
|
|
32
32
|
The full-featured version with everything included.
|
|
33
33
|
|
|
34
|
-
- All directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`, etc.)
|
|
34
|
+
- All directives (`@text`, `@html`, `@for`, `@bind`, `@data`, `@scope`, `@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`)
|
|
38
|
-
- Server-Sent Events (SSE)
|
|
38
|
+
- Optional Server-Sent Events (SSE) add-on
|
|
39
39
|
- Turbo/Hotwire integration
|
|
40
40
|
|
|
41
41
|
**Best for:** Most projects where you want the full power of Helium without worrying about CSP restrictions.
|
|
@@ -50,7 +50,8 @@ A slimmed-down version with just the core reactivity features.
|
|
|
50
50
|
|
|
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 `@scope`, keyed `@for`, HTTP requests, imports, SSE, or Turbo integration
|
|
54
55
|
|
|
55
56
|
**Best for:** Simple interactive pages where you only need reactivity and don't need server communication or module imports.
|
|
56
57
|
|
|
@@ -60,11 +61,38 @@ import helium from "@daz4126/helium/lite"
|
|
|
60
61
|
|
|
61
62
|
### Helium CSP
|
|
62
63
|
|
|
63
|
-
A Content Security Policy safe version that doesn't require `unsafe-eval`. It uses [jexpr](https://github.com/nicolo-ribaudo/jexpr) as its expression engine instead of `new Function()
|
|
64
|
+
A Content Security Policy safe version that doesn't require `unsafe-eval`. It uses [jexpr](https://github.com/nicolo-ribaudo/jexpr) as its expression engine instead of `new Function()`.
|
|
64
65
|
|
|
65
|
-
- All the same features as the standard version
|
|
66
|
+
- All the same directives and HTTP/Turbo/`@import` features as the standard version
|
|
67
|
+
- Optional Server-Sent Events (SSE) add-on
|
|
68
|
+
- `&&` / `||` / `??` short-circuit correctly, and optional chaining (`a?.b`) is supported
|
|
66
69
|
- CSP-compliant — no `unsafe-eval` needed
|
|
67
70
|
|
|
71
|
+
Because expressions run through a custom parser rather than the JS engine, a couple of syntax features differ from the standard build:
|
|
72
|
+
|
|
73
|
+
- No `new` keyword — use the `$Date(...)` and `$FormData(...)` helpers instead
|
|
74
|
+
- Expressions are single expressions, not arbitrary statements
|
|
75
|
+
|
|
76
|
+
Expressions also run in a **restricted scope**: only side-effect-free builtins
|
|
77
|
+
(`Math`, `Date`, `JSON`, `Object`, `Array`, `parseInt`, `URL`, and friends) are
|
|
78
|
+
available. Capability-bearing globals — `window`, `document`, `fetch`,
|
|
79
|
+
`localStorage`, `sessionStorage`, `navigator`, `location`, `history`, timers,
|
|
80
|
+
and dialogs — are not reachable from expressions, so markup injected by an
|
|
81
|
+
attacker can't use them to read cookies or storage. If a page legitimately
|
|
82
|
+
needs one, opt in explicitly by passing the reference:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
import helium, { allowGlobals } from "@daz4126/helium/csp";
|
|
86
|
+
|
|
87
|
+
allowGlobals({ localStorage, fetch }); // expose to expressions
|
|
88
|
+
allowGlobals({ fetch: undefined }); // revoke again
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
(`helium.allowGlobals(...)` works too when using the global script build.)
|
|
92
|
+
Note this reduces the attack surface but is not a sandbox: the `$get`/`$post`
|
|
93
|
+
helpers and `$`/`$el`/`$html` magics remain available to any expression that
|
|
94
|
+
can run at all.
|
|
95
|
+
|
|
68
96
|
**Best for:** Projects with strict Content Security Policies that prohibit `unsafe-eval`.
|
|
69
97
|
|
|
70
98
|
```javascript
|
|
@@ -76,12 +104,23 @@ import helium from "@daz4126/helium/csp"
|
|
|
76
104
|
| Feature | Standard | Lite | CSP |
|
|
77
105
|
|---------|----------|------|-----|
|
|
78
106
|
| Core directives | ✅ | ✅ | ✅ |
|
|
107
|
+
| Element-local `@scope` | ✅ | ❌ | ✅ |
|
|
108
|
+
| Keyed `@for` | ✅ | ❌ | ✅ |
|
|
79
109
|
| Event handlers | ✅ | ✅ | ✅ |
|
|
80
110
|
| HTTP requests | ✅ | ❌ | ✅ |
|
|
81
111
|
| `@import` | ✅ | ❌ | ✅ |
|
|
82
|
-
| SSE |
|
|
112
|
+
| SSE | Add-on | ❌ | Add-on |
|
|
83
113
|
| Turbo integration | ✅ | ❌ | ✅ |
|
|
84
114
|
| CSP safe | ❌ | ❌ | ✅ |
|
|
115
|
+
| Approx. min+gzip size | 6.49KB | 3.93KB | 10.56KB |
|
|
116
|
+
|
|
117
|
+
Sizes are enforced against the production Terser builds with gzip level 9. The
|
|
118
|
+
CSP figure includes its expression parser. Adding SSE produces 6.86KB Standard
|
|
119
|
+
and 10.94KB CSP bundles.
|
|
120
|
+
|
|
121
|
+
Lite is generated from the same global-state reactive core as Standard. It has
|
|
122
|
+
identical deep/shared reactivity, lifecycle, binding, and cleanup behavior, but
|
|
123
|
+
intentionally excludes element-local scopes and Standard's other extensions.
|
|
85
124
|
|
|
86
125
|
## Installation
|
|
87
126
|
|
|
@@ -104,6 +143,11 @@ Just import from the CDN in a script tag directly in your HTML page:
|
|
|
104
143
|
<script type="module">
|
|
105
144
|
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-csp.js';
|
|
106
145
|
</script>
|
|
146
|
+
|
|
147
|
+
<!-- Standard + SSE -->
|
|
148
|
+
<script type="module">
|
|
149
|
+
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-sse.js';
|
|
150
|
+
</script>
|
|
107
151
|
```
|
|
108
152
|
|
|
109
153
|
### NPM
|
|
@@ -115,14 +159,57 @@ npm install @daz4126/helium
|
|
|
115
159
|
Then import the version you need:
|
|
116
160
|
|
|
117
161
|
```javascript
|
|
118
|
-
import helium from "@daz4126/helium"
|
|
119
|
-
import helium from "@daz4126/helium/lite"
|
|
120
|
-
import helium from "@daz4126/helium/csp"
|
|
162
|
+
import helium from "@daz4126/helium" // Standard
|
|
163
|
+
import helium from "@daz4126/helium/lite" // Lite
|
|
164
|
+
import helium from "@daz4126/helium/csp" // CSP
|
|
165
|
+
import helium from "@daz4126/helium/sse" // Standard + SSE
|
|
166
|
+
import helium from "@daz4126/helium/csp/sse" // CSP + SSE
|
|
121
167
|
```
|
|
122
168
|
|
|
169
|
+
For a pre-minified, self-contained bundle, append `/min` to an entry point:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
import helium from "@daz4126/helium/min"
|
|
173
|
+
import helium from "@daz4126/helium/sse/min"
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The source entry points remain browser-ready ES modules, so consuming Helium
|
|
177
|
+
does not require a build step.
|
|
178
|
+
|
|
123
179
|
### Automatic Initialization
|
|
124
180
|
|
|
125
|
-
Helium automatically initializes on `DOMContentLoaded`, so you typically don't need to call `helium()` manually
|
|
181
|
+
Helium automatically initializes on `DOMContentLoaded`, so you typically don't need to call `helium()` manually. Calling it manually to provide initial values or functions suppresses the pending automatic initialization, so your state is not replaced when the DOM becomes ready.
|
|
182
|
+
|
|
183
|
+
### Explicit Mounting
|
|
184
|
+
|
|
185
|
+
Use `helium.mount()` when a page has multiple independent Helium roots. It
|
|
186
|
+
accepts a selector or an element and returns the root's state plus an idempotent
|
|
187
|
+
`unmount()` function:
|
|
188
|
+
|
|
189
|
+
```html
|
|
190
|
+
<section id="counter-a">
|
|
191
|
+
<button @click="count++">Increment</button>
|
|
192
|
+
<span @text="count"></span>
|
|
193
|
+
</section>
|
|
194
|
+
|
|
195
|
+
<section id="counter-b">
|
|
196
|
+
<button @click="count++">Increment</button>
|
|
197
|
+
<span @text="count"></span>
|
|
198
|
+
</section>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
const a = await helium.mount("#counter-a", { count: 0 })
|
|
203
|
+
const b = await helium.mount("#counter-b", { count: 10 })
|
|
204
|
+
|
|
205
|
+
a.state.count++ // Updates only #counter-a
|
|
206
|
+
a.unmount() // Safe to call more than once
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Explicit roots have independent state, bindings, observers, listeners, refs,
|
|
210
|
+
and cleanup. Calling `mount()` suppresses the pending automatic initialization;
|
|
211
|
+
existing single-root pages can continue relying on auto-initialization or
|
|
212
|
+
calling `helium(initialState)` directly.
|
|
126
213
|
|
|
127
214
|
## Helium Attributes
|
|
128
215
|
|
|
@@ -130,7 +217,10 @@ Helium uses custom attributes to add interactivity to HTML elements. To identify
|
|
|
130
217
|
|
|
131
218
|
### @helium
|
|
132
219
|
|
|
133
|
-
This attribute sets the root element. Helium
|
|
220
|
+
This attribute sets the automatically initialized root element. Helium
|
|
221
|
+
attributes are processed on that element and its children. If it is omitted,
|
|
222
|
+
automatic initialization defaults to `document.body`. Explicit
|
|
223
|
+
`helium.mount()` roots do not need this attribute.
|
|
134
224
|
|
|
135
225
|
```html
|
|
136
226
|
<div @helium>
|
|
@@ -173,6 +263,43 @@ Similar to `@text`, but inserts HTML content into the element's innerHTML. Suppo
|
|
|
173
263
|
|
|
174
264
|
**Alias:** `data-he-html`
|
|
175
265
|
|
|
266
|
+
### @for with :key
|
|
267
|
+
|
|
268
|
+
Standard and CSP can render arrays and other iterables as keyed DOM rows. Put
|
|
269
|
+
`@for` and `:key` on a `<template>`; Lite deliberately excludes this feature.
|
|
270
|
+
|
|
271
|
+
```html
|
|
272
|
+
<ul>
|
|
273
|
+
<template @for="item, index in items" :key="item.id">
|
|
274
|
+
<li>
|
|
275
|
+
<span @text="index + 1"></span>.
|
|
276
|
+
<span @text="item.name"></span>
|
|
277
|
+
<button @click="item.done = !item.done">Toggle</button>
|
|
278
|
+
</li>
|
|
279
|
+
</template>
|
|
280
|
+
</ul>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The optional second alias is the current zero-based index:
|
|
284
|
+
|
|
285
|
+
```html
|
|
286
|
+
<template @for="item in items" :key="item.id">
|
|
287
|
+
<!-- item is available to every Helium expression in this row -->
|
|
288
|
+
</template>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Keys must be stable and unique. When the collection changes, Helium moves and
|
|
292
|
+
reuses rows with matching keys, creates new rows, and removes missing rows. This
|
|
293
|
+
preserves element identity, focus, listeners, and `@init` lifecycle state while
|
|
294
|
+
still updating `item`, `index`, and normal root-state dependencies. Cleanup
|
|
295
|
+
functions returned by `@init` run when their keyed row is removed.
|
|
296
|
+
|
|
297
|
+
`@html="items.map(...).join('')"` remains useful for simple generated markup,
|
|
298
|
+
but keyed `@for` avoids HTML-string interpolation and lets each row use normal
|
|
299
|
+
`@text`, dynamic attributes, events, and other directives.
|
|
300
|
+
|
|
301
|
+
**Alias:** `data-he-for` (the key remains `:key`)
|
|
302
|
+
|
|
176
303
|
### @bind
|
|
177
304
|
|
|
178
305
|
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:
|
|
@@ -187,6 +314,13 @@ Works with:
|
|
|
187
314
|
- Radio buttons (binds to `value`, checking the one that matches)
|
|
188
315
|
- Select elements (binds to `value`)
|
|
189
316
|
|
|
317
|
+
Nested paths work too — the parent object must already exist in state:
|
|
318
|
+
|
|
319
|
+
```html
|
|
320
|
+
<input @bind="user.name">
|
|
321
|
+
<p @text="user.name"></p>
|
|
322
|
+
```
|
|
323
|
+
|
|
190
324
|
**Examples:**
|
|
191
325
|
```html
|
|
192
326
|
<!-- Text input -->
|
|
@@ -209,7 +343,38 @@ Works with:
|
|
|
209
343
|
</select>
|
|
210
344
|
```
|
|
211
345
|
|
|
212
|
-
|
|
346
|
+
**`.number` modifier:** append `.number` to coerce the bound value to a number
|
|
347
|
+
(handy for `type="number"` inputs, which otherwise bind as strings). An empty
|
|
348
|
+
field stays `""`, and non-numeric input is left untouched so partial typing
|
|
349
|
+
still works.
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<input type="number" @bind.number="age">
|
|
353
|
+
<p @text="age + 1"></p> <!-- numeric addition, not string concatenation -->
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
**`.trim` modifier:** trim leading and trailing whitespace before storing user
|
|
357
|
+
input in state:
|
|
358
|
+
|
|
359
|
+
```html
|
|
360
|
+
<input @bind.trim="name">
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**`.lazy` modifier:** update state on the element's `change` event rather than
|
|
364
|
+
on every `input` event. For text fields this normally means when the edit is
|
|
365
|
+
committed or the field loses focus:
|
|
366
|
+
|
|
367
|
+
```html
|
|
368
|
+
<input @bind.lazy="search">
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Modifiers can be combined in any order:
|
|
372
|
+
|
|
373
|
+
```html
|
|
374
|
+
<input @bind.lazy.trim.number="amount">
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Alias:** `data-he-bind` (for example `data-he-bind.lazy.trim`)
|
|
213
378
|
|
|
214
379
|
### @hidden & @visible
|
|
215
380
|
|
|
@@ -241,6 +406,50 @@ You can then use these variables in other Helium attributes:
|
|
|
241
406
|
|
|
242
407
|
**Alias:** `data-he-data`
|
|
243
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()`:
|
|
413
|
+
|
|
414
|
+
```html
|
|
415
|
+
<main @data="{ count: 100 }">
|
|
416
|
+
<section @scope="{ count: 0, open: false }">
|
|
417
|
+
<button @click="count++">Increment local count</button>
|
|
418
|
+
<button @click="open = !open">Toggle</button>
|
|
419
|
+
<p @text="count"></p>
|
|
420
|
+
<p @visible="open">Local content</p>
|
|
421
|
+
</section>
|
|
422
|
+
|
|
423
|
+
<p @text="count"></p> <!-- Still 100 -->
|
|
424
|
+
</main>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
The scope expression runs once in its parent context, so defaults can use outer
|
|
428
|
+
values. Nested scopes inherit outer names, and assignment updates the nearest
|
|
429
|
+
scope that declares that name:
|
|
430
|
+
|
|
431
|
+
```html
|
|
432
|
+
<div @scope="{ count: startingCount, label: 'outer' }">
|
|
433
|
+
<div @scope="{ label: 'inner' }">
|
|
434
|
+
<button @click="count++">Updates the outer local count</button>
|
|
435
|
+
<span @text="label"></span> <!-- inner -->
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Declare every name that should remain local. Assigning an undeclared name keeps
|
|
441
|
+
the existing Helium behavior and writes to root state. The same applies to
|
|
442
|
+
`@bind` and `@calculate` targets: declare their target names in `@scope` when
|
|
443
|
+
they should be local. Inside the subtree, `$data` is the scoped state view.
|
|
444
|
+
Local values are not included in root `@local-storage` persistence.
|
|
445
|
+
|
|
446
|
+
`@data` continues to merge into root state; it has not changed semantics.
|
|
447
|
+
|
|
448
|
+
**Alias:** `data-he-scope`
|
|
449
|
+
|
|
450
|
+
`@scope` is intentionally excluded from Lite, which always uses one global
|
|
451
|
+
state namespace per mounted root.
|
|
452
|
+
|
|
244
453
|
### @ref
|
|
245
454
|
|
|
246
455
|
Creates a reference to the element that can be used in JavaScript expressions. References are prefixed with `$` when accessed.
|
|
@@ -266,6 +475,22 @@ A JavaScript expression that will run once when Helium initializes. Useful for s
|
|
|
266
475
|
<div @init="console.log('Helium initialized!')"></div>
|
|
267
476
|
```
|
|
268
477
|
|
|
478
|
+
If the expression returns a function, Helium calls it when the element is removed or when Helium is torn down. This is useful for releasing timers, observers, subscriptions, and other resources:
|
|
479
|
+
|
|
480
|
+
```html
|
|
481
|
+
<div @init="start_clock($el, $data)"></div>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
```js
|
|
485
|
+
function start_clock(element, data) {
|
|
486
|
+
const timer = setInterval(() => {
|
|
487
|
+
element.textContent = new Date().toLocaleTimeString()
|
|
488
|
+
}, 1000)
|
|
489
|
+
|
|
490
|
+
return () => clearInterval(timer)
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
269
494
|
**Alias:** `data-he-init`
|
|
270
495
|
|
|
271
496
|
### @calculate
|
|
@@ -459,7 +684,7 @@ You can add modifiers by appending them with a dot (`.`) after the event name:
|
|
|
459
684
|
<div @keydown.document.esc="closeModal()">Press ESC anywhere</div>
|
|
460
685
|
```
|
|
461
686
|
|
|
462
|
-
**Alias:** Prepend the event name with `data-he
|
|
687
|
+
**Alias:** Prepend the event name with `data-he-`, for example `data-he-click="count++"`
|
|
463
688
|
|
|
464
689
|
## HTTP Requests
|
|
465
690
|
|
|
@@ -643,6 +868,44 @@ Additional fetch options (as an object):
|
|
|
643
868
|
- `data-he-loading`
|
|
644
869
|
- `data-he-options`
|
|
645
870
|
|
|
871
|
+
### Server-Sent Events (optional)
|
|
872
|
+
|
|
873
|
+
SSE parsing is kept out of the core build. Select the add-on entry point when a
|
|
874
|
+
page needs it:
|
|
875
|
+
|
|
876
|
+
```javascript
|
|
877
|
+
import helium from "@daz4126/helium/sse"
|
|
878
|
+
// Strict CSP: import helium from "@daz4126/helium/csp/sse"
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
Use the normal HTTP directives. An explicit `@target` is applied to every SSE
|
|
882
|
+
message:
|
|
883
|
+
|
|
884
|
+
```html
|
|
885
|
+
<button @get="/api/events" @target="#events:append">Connect</button>
|
|
886
|
+
<div id="events"></div>
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
When `@target` is omitted, an SSE `event:` field can name a CSS selector or a
|
|
890
|
+
state property:
|
|
891
|
+
|
|
892
|
+
```text
|
|
893
|
+
event: #status
|
|
894
|
+
data: Connected
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Set `retryMode` and an optional initial `retry` delay through `@options` to
|
|
898
|
+
reconnect after the stream closes or fails. A server-supplied `retry:` field
|
|
899
|
+
updates the delay and `id:` is sent back as `Last-Event-ID` on reconnection.
|
|
900
|
+
|
|
901
|
+
```html
|
|
902
|
+
<button @get="/api/events"
|
|
903
|
+
@target="#events:append"
|
|
904
|
+
@options="{ retryMode: 'error', retry: 3000 }">
|
|
905
|
+
Connect
|
|
906
|
+
</button>
|
|
907
|
+
```
|
|
908
|
+
|
|
646
909
|
### Complete Example
|
|
647
910
|
|
|
648
911
|
```html
|
|
@@ -795,8 +1058,10 @@ HTTP request functions that can be called programmatically:
|
|
|
795
1058
|
|
|
796
1059
|
The arguments are `url`,`params` (not for `$get`) and `options`. `options` is an object that can include the properties `loading`,`target`, `template`
|
|
797
1060
|
|
|
798
|
-
###
|
|
799
|
-
|
|
1061
|
+
### Named refs
|
|
1062
|
+
|
|
1063
|
+
Each element marked with `@ref="name"` is available as `$name`. There is no
|
|
1064
|
+
separate `$refs` object:
|
|
800
1065
|
|
|
801
1066
|
```html
|
|
802
1067
|
<input @ref="username">
|
|
@@ -1004,7 +1269,11 @@ The token is automatically included in POST, PUT, PATCH, and DELETE requests to
|
|
|
1004
1269
|
|
|
1005
1270
|
### Content Security Policy
|
|
1006
1271
|
|
|
1007
|
-
|
|
1272
|
+
The standard and lite builds use `new Function()` and therefore require `unsafe-eval`. For a strict Content Security Policy, use the CSP build instead:
|
|
1273
|
+
|
|
1274
|
+
```js
|
|
1275
|
+
import helium from "@daz4126/helium/csp"
|
|
1276
|
+
```
|
|
1008
1277
|
|
|
1009
1278
|
## Best Practices
|
|
1010
1279
|
|
|
@@ -1093,22 +1362,19 @@ If you're using a Content Security Policy, note that Helium uses `new Function()
|
|
|
1093
1362
|
|
|
1094
1363
|
### Common Pitfalls
|
|
1095
1364
|
|
|
1096
|
-
|
|
1365
|
+
**Arrays and objects are reactive:**
|
|
1097
1366
|
|
|
1098
|
-
```
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
items.push(item); // ❌ Won't trigger updates
|
|
1102
|
-
}
|
|
1103
|
-
})
|
|
1367
|
+
```html
|
|
1368
|
+
<button @click="items.push(newItem)">Add item</button>
|
|
1369
|
+
<span @text="items.length"></span>
|
|
1104
1370
|
```
|
|
1105
1371
|
|
|
1106
|
-
|
|
1372
|
+
Nested mutations made through `$data` are reactive too:
|
|
1107
1373
|
|
|
1108
1374
|
```javascript
|
|
1109
1375
|
helium({
|
|
1110
1376
|
addItem(data, item) {
|
|
1111
|
-
data.items.push(item); //
|
|
1377
|
+
data.items.push(item); // Triggers updates
|
|
1112
1378
|
}
|
|
1113
1379
|
})
|
|
1114
1380
|
```
|
|
@@ -1141,58 +1407,11 @@ helium({
|
|
|
1141
1407
|
<button @click="goodFunction($data)">Works!</button>
|
|
1142
1408
|
```
|
|
1143
1409
|
|
|
1144
|
-
## Security Considerations
|
|
1145
|
-
|
|
1146
|
-
### XSS Protection
|
|
1147
|
-
|
|
1148
|
-
**Always sanitize user input when using @html:**
|
|
1149
|
-
|
|
1150
|
-
```html
|
|
1151
|
-
<!-- ❌ Dangerous if userInput contains scripts -->
|
|
1152
|
-
<div @html="userInput"></div>
|
|
1153
|
-
|
|
1154
|
-
<!-- ✅ Sanitize first -->
|
|
1155
|
-
<div @html="sanitize(userInput)"></div>
|
|
1156
|
-
```
|
|
1157
|
-
|
|
1158
|
-
Consider using a sanitization library like [DOMPurify](https://github.com/cure53/DOMPurify):
|
|
1159
|
-
|
|
1160
|
-
```javascript
|
|
1161
|
-
import DOMPurify from 'dompurify';
|
|
1162
|
-
|
|
1163
|
-
helium({
|
|
1164
|
-
sanitize(html) {
|
|
1165
|
-
return DOMPurify.sanitize(html);
|
|
1166
|
-
}
|
|
1167
|
-
});
|
|
1168
|
-
```
|
|
1169
|
-
|
|
1170
|
-
**Use @text for plain text:**
|
|
1171
|
-
|
|
1172
|
-
```html
|
|
1173
|
-
<!-- ✅ Safe - automatically escapes HTML -->
|
|
1174
|
-
<div @text="userInput"></div>
|
|
1175
|
-
```
|
|
1176
|
-
|
|
1177
|
-
### CSRF Protection
|
|
1178
|
-
|
|
1179
|
-
Helium automatically includes CSRF tokens in same-origin requests. Add this meta tag to your HTML:
|
|
1180
|
-
|
|
1181
|
-
```html
|
|
1182
|
-
<meta name="csrf-token" content="your-csrf-token">
|
|
1183
|
-
```
|
|
1184
|
-
|
|
1185
|
-
All POST, PUT, PATCH, and DELETE requests will include the `X-CSRF-Token` header automatically.
|
|
1186
|
-
|
|
1187
|
-
### Content Security Policy
|
|
1188
|
-
|
|
1189
|
-
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.
|
|
1190
|
-
|
|
1191
1410
|
## Error Handling
|
|
1192
1411
|
|
|
1193
1412
|
### JavaScript Expression Errors
|
|
1194
1413
|
|
|
1195
|
-
If
|
|
1414
|
+
If a binding expression throws at runtime, Helium logs the error and continues without updating that binding. Standard and lite retain malformed expressions as literal text for compatibility; quote intentional string constants, for example `@text="'Ready'"`.
|
|
1196
1415
|
|
|
1197
1416
|
```html
|
|
1198
1417
|
<!-- If items is undefined, this won't crash the page -->
|
|
@@ -1201,13 +1420,12 @@ If an expression throws an error, Helium catches it silently and continues. Chec
|
|
|
1201
1420
|
|
|
1202
1421
|
### HTTP Request Errors
|
|
1203
1422
|
|
|
1204
|
-
|
|
1423
|
+
HTTP directives log failed requests to the console. Programmatic helpers return
|
|
1424
|
+
promises, so you can handle failures explicitly:
|
|
1205
1425
|
|
|
1206
1426
|
```html
|
|
1207
|
-
<button
|
|
1208
|
-
|
|
1209
|
-
@params="{ data: formData }"
|
|
1210
|
-
@target="#message">
|
|
1427
|
+
<button @click="$post('/api/save', { data: formData }, '#message')
|
|
1428
|
+
.catch(error => saveError = error.message)">
|
|
1211
1429
|
Save
|
|
1212
1430
|
</button>
|
|
1213
1431
|
|
|
@@ -1216,8 +1434,54 @@ Failed requests log errors to the console. Handle them in your expressions:
|
|
|
1216
1434
|
|
|
1217
1435
|
### Invalid Attribute Syntax
|
|
1218
1436
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1437
|
+
Standard and Lite retain expressions that cannot be compiled as literal values.
|
|
1438
|
+
The CSP build logs the parser error and leaves the binding undefined.
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
## Roadmap & Known Limitations
|
|
1442
|
+
|
|
1443
|
+
Helium aims to stay tiny, so some conveniences found in larger frameworks are
|
|
1444
|
+
intentionally absent (or still on the roadmap). Current gaps:
|
|
1445
|
+
|
|
1446
|
+
### Not yet supported
|
|
1447
|
+
|
|
1448
|
+
- **No DOM-removing `@if`** — only `@hidden`/`@visible`, which toggle visibility
|
|
1449
|
+
but keep elements in the DOM.
|
|
1450
|
+
- **No `$watch`/`$nextTick`, and no cross-root shared store** — each mounted
|
|
1451
|
+
instance has its own isolated state namespace.
|
|
1452
|
+
|
|
1453
|
+
### Roadmap
|
|
1454
|
+
|
|
1455
|
+
Ordered by recommended implementation sequence:
|
|
1456
|
+
|
|
1457
|
+
1. **Improve expression-compilation diagnostics** — emit a useful `console.warn`
|
|
1458
|
+
containing the expression and build/engine context instead of silently
|
|
1459
|
+
treating every compile failure as a literal value.
|
|
1460
|
+
2. **Add real-browser CI** covering CSP, Turbo, SSE, focus preservation, late
|
|
1461
|
+
imports, generated Lite output, and package exports before adding more DOM
|
|
1462
|
+
lifecycle behavior.
|
|
1463
|
+
3. **Document function and `this` context limitations** alongside the existing
|
|
1464
|
+
magic-variable guidance, with correct patterns for passing `$data`, `$el`,
|
|
1465
|
+
and other context explicitly.
|
|
1466
|
+
4. **Add opt-in required-CSRF validation for POST/PUT/PATCH** — fail before the
|
|
1467
|
+
request when an application declares that a token is mandatory, without
|
|
1468
|
+
breaking tokenless APIs and cross-origin requests.
|
|
1469
|
+
5. **Add request cancellation and explicit success/error hooks** using
|
|
1470
|
+
`AbortController`; define the request lifecycle before expanding loading and
|
|
1471
|
+
error-state APIs.
|
|
1472
|
+
6. **Document a form-validation pattern first**, using native constraint
|
|
1473
|
+
validation and Helium state; add helpers only if examples reveal recurring
|
|
1474
|
+
boilerplate.
|
|
1475
|
+
7. **Create a complete error-state example** covering validation, HTTP failure,
|
|
1476
|
+
retry, accessible messaging, and clearing stale errors.
|
|
1477
|
+
8. **Complete HTTP loading-state lifecycle behavior** — `@loading` already
|
|
1478
|
+
exists, so define consistent success, error, cancellation, and restoration
|
|
1479
|
+
behavior rather than adding a second loading mechanism.
|
|
1480
|
+
9. **Add debounced two-way binding updates if needed** after reproducing a real
|
|
1481
|
+
feedback/caret loop; event-handler debouncing already covers ordinary input
|
|
1482
|
+
throttling.
|
|
1483
|
+
10. **Add a minimal watch/middleware system** together with a clear size budget;
|
|
1484
|
+
avoid adopting Alpine-scale surface area without demonstrated use cases.
|
|
1221
1485
|
|
|
1222
1486
|
## Contributing
|
|
1223
1487
|
|
|
@@ -1227,6 +1491,15 @@ Helium is open source! Contributions, issues, and feature requests are welcome.
|
|
|
1227
1491
|
- Report issues: Create an issue on GitHub
|
|
1228
1492
|
- Suggest features: Open a discussion on GitHub
|
|
1229
1493
|
|
|
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 size budget is
|
|
1496
|
+
exceeded. `npm run size` also reports raw, gzip, and Brotli sizes.
|
|
1497
|
+
|
|
1498
|
+
`helium.js` contains the shared runtime plus sections marked Standard-only.
|
|
1499
|
+
`npm run generate` derives `helium-lite.js`; do not edit the generated Lite file
|
|
1500
|
+
directly. Standard keeps keyed lists, HTTP, imports, and Turbo, while SSE remains
|
|
1501
|
+
an optional module.
|
|
1502
|
+
|
|
1230
1503
|
## License
|
|
1231
1504
|
|
|
1232
1505
|
MIT License - feel free to use Helium in personal and commercial projects.
|