@daz4126/helium 1.0.0-beta.4 → 1.0.0-beta.5

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.
@@ -11,7 +11,8 @@
11
11
  "Bash(npm install)",
12
12
  "Bash(npm link helium)",
13
13
  "Bash(ls:*)",
14
- "Bash(wc:*)"
14
+ "Bash(wc:*)",
15
+ "WebFetch(domain:codepen.io)"
15
16
  ]
16
17
  }
17
18
  }
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 include it in your JavaScript file and call the helium() function:
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daz4126/helium",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "main": "helium.js",
5
5
  "type": "module",
6
6
  "exports": {