@daz4126/helium 1.0.0-beta.4 → 1.0.0-rc.1
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/.claude/settings.local.json +2 -1
- package/README.md +78 -5
- package/helium-lite.js +4 -4
- package/helium.js +4 -3
- package/helium.test.js +79 -0
- package/package.json +1 -1
- package/helium (# Edit conflict 2026-02-04 FmuZMse #).js +0 -629
- package/helium (# Edit conflict 2026-02-04 j7PIppO #).js +0 -629
- package/helium (# Edit conflict 2026-02-04 mqpbJka #).js +0 -629
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ 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/ogxGMX)
|
|
15
|
+
[See more examples here](https://codepen.io/daz4126/pen/ogxGMX) | [TodoMVC Example](https://codepen.io/daz4126/pen/gbMjdZO)
|
|
16
16
|
|
|
17
17
|
## Why Helium?
|
|
18
18
|
|
|
@@ -20,8 +20,68 @@ Helium is designed for developers who want:
|
|
|
20
20
|
|
|
21
21
|
- **Lightweight** - Just over 3KB minified and gzipped
|
|
22
22
|
- **Powerful** - Declarative JavaScript in your HTML
|
|
23
|
-
- **Zero build step** Works directly in the browser with no compiling
|
|
24
|
-
- **Easy to learn** If you know HTML and basic JavaScript, you're ready
|
|
23
|
+
- **Zero build step** - Works directly in the browser with no compiling
|
|
24
|
+
- **Easy to learn** - If you know HTML and basic JavaScript, you're ready
|
|
25
|
+
|
|
26
|
+
## Versions
|
|
27
|
+
|
|
28
|
+
Helium comes in three versions so you can pick the right balance of features and size for your project:
|
|
29
|
+
|
|
30
|
+
### Helium (Standard)
|
|
31
|
+
|
|
32
|
+
The full-featured version with everything included.
|
|
33
|
+
|
|
34
|
+
- All directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`, etc.)
|
|
35
|
+
- Event handlers with modifiers
|
|
36
|
+
- HTTP requests (`@get`, `@post`, `@put`, `@patch`, `@delete`)
|
|
37
|
+
- Module imports (`@import`)
|
|
38
|
+
- Server-Sent Events (SSE)
|
|
39
|
+
- Turbo/Hotwire integration
|
|
40
|
+
|
|
41
|
+
**Best for:** Most projects where you want the full power of Helium without worrying about CSP restrictions.
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import helium from "@daz4126/helium"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Helium Lite
|
|
48
|
+
|
|
49
|
+
A slimmed-down version with just the core reactivity features.
|
|
50
|
+
|
|
51
|
+
- Core directives (`@text`, `@html`, `@bind`, `@data`, `@ref`, `@calculate`, `@effect`, `@init`)
|
|
52
|
+
- Event handlers with modifiers
|
|
53
|
+
- No HTTP requests, imports, SSE, or Turbo integration
|
|
54
|
+
|
|
55
|
+
**Best for:** Simple interactive pages where you only need reactivity and don't need server communication or module imports.
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
import helium from "@daz4126/helium/lite"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Helium CSP
|
|
62
|
+
|
|
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()`, while keeping the full feature set.
|
|
64
|
+
|
|
65
|
+
- All the same features as the standard version
|
|
66
|
+
- CSP-compliant — no `unsafe-eval` needed
|
|
67
|
+
|
|
68
|
+
**Best for:** Projects with strict Content Security Policies that prohibit `unsafe-eval`.
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
import helium from "@daz4126/helium/csp"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Comparison
|
|
75
|
+
|
|
76
|
+
| Feature | Standard | Lite | CSP |
|
|
77
|
+
|---------|----------|------|-----|
|
|
78
|
+
| Core directives | ✅ | ✅ | ✅ |
|
|
79
|
+
| Event handlers | ✅ | ✅ | ✅ |
|
|
80
|
+
| HTTP requests | ✅ | ❌ | ✅ |
|
|
81
|
+
| `@import` | ✅ | ❌ | ✅ |
|
|
82
|
+
| SSE | ✅ | ❌ | ✅ |
|
|
83
|
+
| Turbo integration | ✅ | ❌ | ✅ |
|
|
84
|
+
| CSP safe | ❌ | ❌ | ✅ |
|
|
25
85
|
|
|
26
86
|
## Installation
|
|
27
87
|
|
|
@@ -30,9 +90,20 @@ Helium is designed for developers who want:
|
|
|
30
90
|
Just import from the CDN in a script tag directly in your HTML page:
|
|
31
91
|
|
|
32
92
|
```html
|
|
93
|
+
<!-- Standard -->
|
|
33
94
|
<script type="module">
|
|
34
95
|
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium.js';
|
|
35
96
|
</script>
|
|
97
|
+
|
|
98
|
+
<!-- Lite -->
|
|
99
|
+
<script type="module">
|
|
100
|
+
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-lite.js';
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<!-- CSP -->
|
|
104
|
+
<script type="module">
|
|
105
|
+
import helium from 'https://cdn.jsdelivr.net/gh/daz-codes/helium/helium-csp.js';
|
|
106
|
+
</script>
|
|
36
107
|
```
|
|
37
108
|
|
|
38
109
|
### NPM
|
|
@@ -41,10 +112,12 @@ Just import from the CDN in a script tag directly in your HTML page:
|
|
|
41
112
|
npm install @daz4126/helium
|
|
42
113
|
```
|
|
43
114
|
|
|
44
|
-
Then
|
|
115
|
+
Then import the version you need:
|
|
45
116
|
|
|
46
117
|
```javascript
|
|
47
|
-
import helium from "@daz4126/helium"
|
|
118
|
+
import helium from "@daz4126/helium" // Standard
|
|
119
|
+
import helium from "@daz4126/helium/lite" // Lite
|
|
120
|
+
import helium from "@daz4126/helium/csp" // CSP
|
|
48
121
|
```
|
|
49
122
|
|
|
50
123
|
### Automatic Initialization
|
package/helium-lite.js
CHANGED
|
@@ -120,10 +120,6 @@ async function helium(initialState = {}) {
|
|
|
120
120
|
};
|
|
121
121
|
|
|
122
122
|
const state = new Proxy({}, handler);
|
|
123
|
-
if (storageKey)
|
|
124
|
-
try {
|
|
125
|
-
Object.assign(state, JSON.parse(localStorage.getItem(storageKey)));
|
|
126
|
-
} catch {}
|
|
127
123
|
Object.assign(state, initialState);
|
|
128
124
|
|
|
129
125
|
const createScope = (el, event = {}) => {
|
|
@@ -455,7 +451,11 @@ async function helium(initialState = {}) {
|
|
|
455
451
|
});
|
|
456
452
|
HELIUM.observer.observe(root, { childList: true, subtree: true });
|
|
457
453
|
|
|
454
|
+
// Read localStorage before processElements (which sets @data defaults and triggers saves via proxy)
|
|
455
|
+
const savedState = storageKey ? (() => { try { return JSON.parse(localStorage.getItem(storageKey)); } catch { return null; } })() : null;
|
|
458
456
|
const initialBindings = await processElements(root);
|
|
457
|
+
// Merge saved values after @data defaults, so localStorage takes priority
|
|
458
|
+
if (savedState) Object.assign(state, savedState);
|
|
459
459
|
initialBindings.forEach(applyBinding);
|
|
460
460
|
inits.forEach(({ compiled, el }) => {
|
|
461
461
|
const scope = createScope(el);
|
package/helium.js
CHANGED
|
@@ -238,9 +238,6 @@ export function createHelium(options = {}) {
|
|
|
238
238
|
// Initialize state if it doesn't exist
|
|
239
239
|
const state = new Proxy({}, handler);
|
|
240
240
|
|
|
241
|
-
// Load from localStorage if key exists
|
|
242
|
-
if (storageKey) try { Object.assign(state, JSON.parse(localStorage.getItem(storageKey))); } catch {}
|
|
243
|
-
|
|
244
241
|
// Merge initial state
|
|
245
242
|
Object.assign(state, initialState);
|
|
246
243
|
|
|
@@ -569,7 +566,11 @@ export function createHelium(options = {}) {
|
|
|
569
566
|
});
|
|
570
567
|
HELIUM.observer.observe(root, { childList: true, subtree: true });
|
|
571
568
|
|
|
569
|
+
// Read localStorage before processElements (which sets @data defaults and triggers saves via proxy)
|
|
570
|
+
const savedState = storageKey ? (() => { try { return JSON.parse(localStorage.getItem(storageKey)); } catch { return null; } })() : null;
|
|
572
571
|
const initialBindings = await processElements(root);
|
|
572
|
+
// Merge saved values after @data defaults, so localStorage takes priority
|
|
573
|
+
if (savedState) Object.assign(state, savedState);
|
|
573
574
|
initialBindings.forEach(applyBinding);
|
|
574
575
|
inits.forEach(({ compiled, el }) => {
|
|
575
576
|
const scope = createScope(el);
|
package/helium.test.js
CHANGED
|
@@ -788,4 +788,83 @@ describe('@calculate', () => {
|
|
|
788
788
|
await new Promise(r => setTimeout(r, 10));
|
|
789
789
|
expect(container.querySelector('#result').textContent).toBe('Strong');
|
|
790
790
|
});
|
|
791
|
+
|
|
792
|
+
describe('@local-storage', () => {
|
|
793
|
+
afterEach(() => {
|
|
794
|
+
localStorage.clear();
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
it('should not overwrite localStorage values with @data defaults', async () => {
|
|
798
|
+
// Simulate saved state from a previous session
|
|
799
|
+
localStorage.setItem('test-todos', JSON.stringify({ todos: ['buy milk', 'walk dog'], filter: 'active', nextId: 3 }));
|
|
800
|
+
|
|
801
|
+
container.setAttribute('data-he-local-storage', 'test-todos');
|
|
802
|
+
container.innerHTML = `
|
|
803
|
+
<div @data="{ todos: [], filter: 'all', nextId: 1 }">
|
|
804
|
+
<span id="count" @text="todos.length"></span>
|
|
805
|
+
<span id="filter" @text="filter"></span>
|
|
806
|
+
<span id="nextId" @text="nextId"></span>
|
|
807
|
+
</div>
|
|
808
|
+
`;
|
|
809
|
+
await helium();
|
|
810
|
+
await new Promise(r => setTimeout(r, 10));
|
|
811
|
+
|
|
812
|
+
// localStorage values should be preserved, not overwritten by @data defaults
|
|
813
|
+
expect(container.querySelector('#count').textContent).toBe('2');
|
|
814
|
+
expect(container.querySelector('#filter').textContent).toBe('active');
|
|
815
|
+
expect(container.querySelector('#nextId').textContent).toBe('3');
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
it('should set @data values that are not in localStorage', async () => {
|
|
819
|
+
// Saved state only has some keys
|
|
820
|
+
localStorage.setItem('test-partial', JSON.stringify({ count: 42 }));
|
|
821
|
+
|
|
822
|
+
container.setAttribute('data-he-local-storage', 'test-partial');
|
|
823
|
+
container.innerHTML = `
|
|
824
|
+
<div @data="{ count: 0, name: 'default' }">
|
|
825
|
+
<span id="count" @text="count"></span>
|
|
826
|
+
<span id="name" @text="name"></span>
|
|
827
|
+
</div>
|
|
828
|
+
`;
|
|
829
|
+
await helium();
|
|
830
|
+
await new Promise(r => setTimeout(r, 10));
|
|
831
|
+
|
|
832
|
+
// count should come from localStorage, name should come from @data
|
|
833
|
+
expect(container.querySelector('#count').textContent).toBe('42');
|
|
834
|
+
expect(container.querySelector('#name').textContent).toBe('default');
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('should save state changes to localStorage', async () => {
|
|
838
|
+
container.setAttribute('data-he-local-storage', 'test-save');
|
|
839
|
+
container.innerHTML = `
|
|
840
|
+
<div @data="{ count: 0 }">
|
|
841
|
+
<button @click="count++">Inc</button>
|
|
842
|
+
<span @text="count"></span>
|
|
843
|
+
</div>
|
|
844
|
+
`;
|
|
845
|
+
await helium();
|
|
846
|
+
|
|
847
|
+
container.querySelector('button').click();
|
|
848
|
+
await new Promise(r => setTimeout(r, 10));
|
|
849
|
+
|
|
850
|
+
const saved = JSON.parse(localStorage.getItem('test-save'));
|
|
851
|
+
expect(saved.count).toBe(1);
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('should preserve functions from @data when localStorage is active', async () => {
|
|
855
|
+
localStorage.setItem('test-funcs', JSON.stringify({ count: 5 }));
|
|
856
|
+
|
|
857
|
+
container.setAttribute('data-he-local-storage', 'test-funcs');
|
|
858
|
+
container.innerHTML = `
|
|
859
|
+
<div @data="{ count: 0, double(n) { return n * 2 } }">
|
|
860
|
+
<span id="result" @text="double(count)"></span>
|
|
861
|
+
</div>
|
|
862
|
+
`;
|
|
863
|
+
await helium();
|
|
864
|
+
await new Promise(r => setTimeout(r, 10));
|
|
865
|
+
|
|
866
|
+
// count from localStorage (5), function from @data
|
|
867
|
+
expect(container.querySelector('#result').textContent).toBe('10');
|
|
868
|
+
});
|
|
869
|
+
});
|
|
791
870
|
});
|