a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1# Global State
2
3VoltX provides built-in global state management through special variables and a globally available store.
4These features enable sharing state across components, accessing metadata, and coordinating behavior without external dependencies.
5
6## Overview
7
8Every Volt scope automatically receives special variables (prefixed with `$`) that provide access to:
9
10- **Global Store** - Shared reactive state across all scopes
11- **Scope Metadata** - Information about the current reactive context
12- **Element References** - Access to pinned DOM elements
13- **Utility Functions** - Helper functions for common tasks
14
15## Special Variables
16
17### `$store`
18
19Access globally shared reactive state across all Volt roots.
20
21**Declarative API:**
22
23```html
24<!-- Define global store -->
25<script type="application/json" data-volt-store>
26{
27 "theme": "dark",
28 "user": { "name": "Alice" }
29}
30</script>
31
32<!-- Use in any Volt root -->
33<div data-volt>
34 <p data-volt-text="$store.get('theme')"></p>
35 <button data-volt-on-click="$store.set('theme', 'light')">Toggle</button>
36</div>
37```
38
39**Programmatic API:**
40
41```typescript
42import { registerStore, getStore } from 'voltx.js';
43
44// Register store with signals or raw values
45registerStore({
46 theme: signal('dark'),
47 count: 0 // Auto-wrapped in signal
48});
49
50// Access store
51const store = getStore();
52store.set('count', 5);
53console.log(store.get('count')); // 5
54```
55
56**Methods:**
57
58- `$store.get(key)` - Get signal value
59- `$store.set(key, value)` - Update signal value
60- `$store.has(key)` - Check if key exists
61- `$store[key]` - Direct signal access (auto-unwrapped in read contexts)
62
63**Note on Signal Unwrapping:**
64
65When accessing store values via `$store[key]` in read contexts (like `data-volt-text` or `data-volt-if`), the signal is automatically unwrapped. In event handlers, use `.get()` and `.set()` methods for explicit control:
66
67```html
68<!-- Read context: signal auto-unwrapped -->
69<p data-volt-if="$store.theme === 'dark'">Dark mode active</p>
70
71<!-- Event handler: use methods -->
72<button data-volt-on-click="$store.theme.set('light')">Switch to Light</button>
73
74<!-- Or use the store's convenience methods -->
75<button data-volt-on-click="$store.set('theme', 'light')">Switch to Light</button>
76```
77
78### `$origin`
79
80Reference to the root element of the current reactive scope.
81
82```html
83<div data-volt id="app-root">
84 <p data-volt-text="'Root ID: ' + $origin.id"></p>
85 <!-- Displays: "Root ID: app-root" -->
86</div>
87```
88
89### `$scope`
90
91Direct access to the raw scope object containing all signals and context.
92
93```html
94<div data-volt data-volt-state='{"count": 0}'>
95 <p data-volt-text="Object.keys($scope).length"></p>
96 <!-- Shows number of scope properties -->
97</div>
98```
99
100### `$pins`
101
102Access DOM elements registered with `data-volt-pin`.
103
104```html
105<div data-volt>
106 <input data-volt-pin="username" />
107 <input data-volt-pin="password" type="password" />
108
109 <button data-volt-on-click="$pins.username.focus()">
110 Focus Username
111 </button>
112
113 <button data-volt-on-click="$pins.password.value = ''">
114 Clear Password
115 </button>
116</div>
117```
118
119**Notes:**
120
121- Pins are scoped to their root element
122- Each root maintains its own pin registry
123- Pins are accessible immediately after registration
124
125### `$pulse(callback)`
126
127Defers callback execution to the next microtask, ensuring DOM updates have completed.
128
129```html
130<div data-volt data-volt-state='{"count": 0}'>
131 <button data-volt-on-click="count.set(count.get() + 1); $pulse(() => console.log('Updated!'))">
132 Increment
133 </button>
134</div>
135```
136
137**Use Cases:**
138
139- Run code after DOM updates
140- Coordinate async operations
141- Batch multiple updates
142
143### `$uid(prefix?)`
144
145Generates unique, deterministic IDs within the scope.
146
147```html
148<div data-volt>
149 <input data-volt-bind:id="$uid('field')" />
150 <!-- id="volt-field-1" -->
151
152 <input data-volt-bind:id="$uid('field')" />
153 <!-- id="volt-field-2" -->
154
155 <input data-volt-bind:id="$uid()" />
156 <!-- id="volt-3" -->
157</div>
158```
159
160**Notes:**
161
162- IDs are unique within the scope
163- Counter increments on each call
164- Different scopes have independent counters
165
166### `$arc(eventName, detail?)`
167
168Dispatches a CustomEvent from the current element.
169
170```html
171<div data-volt data-volt-on-user:save="console.log('Saved:', $event.detail)">
172 <button data-volt-on-click="$arc('user:save', { id: 123, name: 'Alice' })">
173 Save User
174 </button>
175</div>
176```
177
178**Event Properties:**
179
180- `bubbles: true` - Event bubbles up the DOM
181- `composed: true` - Crosses shadow DOM boundaries
182- `cancelable: true` - Can be prevented
183- `detail` - Custom data payload
184
185### `$probe(expression, callback)`
186
187Observes a reactive expression and calls a callback when dependencies change.
188
189```html
190<div data-volt data-volt-state='{"count": 0}' data-volt-init="$probe('count', v => console.log('Count:', v))">
191 <button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
192 <!-- Logs: "Count: 0" immediately, then "Count: 1", "Count: 2", etc. -->
193</div>
194```
195
196**Parameters:**
197
198- `expression` (string) - Reactive expression to observe
199- `callback` (function) - Called with expression value on changes
200
201**Returns:**
202
203- Cleanup function to stop observing
204
205**Example:**
206
207```html
208<div data-volt
209 data-volt-state='{"x": 0, "y": 0}'
210 data-volt-init="const cleanup = $probe('x + y', sum => console.log('Sum:', sum))">
211
212 <button data-volt-on-click="x.set(x.get() + 1)">+X</button>
213 <button data-volt-on-click="y.set(y.get() + 1)">+Y</button>
214
215 <!-- Logs: "Sum: 0" initially, then on every change -->
216</div>
217```
218
219## `data-volt-init`
220
221Run initialization code once when an element is mounted.
222
223**Basic Usage:**
224
225```html
226<div data-volt
227 data-volt-state='{"initialized": false}'
228 data-volt-init="initialized.set(true)">
229 <p data-volt-text="initialized"></p>
230 <!-- Displays: true -->
231</div>
232```
233
234**Setting Up Observers:**
235
236```html
237<div data-volt
238 data-volt-state='{"count": 0, "log": []}'
239 data-volt-init="$probe('count', v => log.push(v))">
240 <button data-volt-on-click="count.set(count.get() + 1)">Increment</button>
241 <p data-volt-text="log.join(', ')"></p>
242 <!-- Displays: "0, 1, 2, ..." -->
243</div>
244```
245
246**Accessing Special Variables:**
247
248```html
249<div data-volt
250 id="main"
251 data-volt-state='{"rootId": ""}'
252 data-volt-init="rootId.set($origin.id)">
253 <p data-volt-text="rootId"></p>
254 <!-- Displays: "main" -->
255</div>
256```
257
258## Global Store Patterns
259
260### Shared Application State
261
262```html
263<!-- Define global state once -->
264<script type="application/json" data-volt-store>
265{
266 "theme": "light",
267 "user": null,
268 "authenticated": false
269}
270</script>
271
272<!-- Header component -->
273<div data-volt>
274 <div data-volt-class="$store.get('theme')">
275 <button data-volt-on-click="$store.set('theme', $store.get('theme') === 'light' ? 'dark' : 'light')">
276 Toggle Theme
277 </button>
278 </div>
279</div>
280
281<!-- User profile -->
282<div data-volt>
283 <div data-volt-if="$store.get('authenticated')">
284 <p data-volt-text="'Welcome, ' + $store.get('user').name"></p>
285 </div>
286</div>
287```
288
289### Cross-Component Communication
290
291```html
292<script type="application/json" data-volt-store>
293{
294 "selectedId": null,
295 "items": []
296}
297</script>
298
299<!-- Item list -->
300<div data-volt>
301 <div data-volt-for="item in $store.get('items')">
302 <button data-volt-on-click="$store.set('selectedId', item.id)" data-volt-text="item.name"></button>
303 </div>
304</div>
305
306<!-- Item details -->
307<div data-volt>
308 <div data-volt-if="$store.get('selectedId')">
309 <p data-volt-text="'Selected: ' + $store.get('selectedId')"></p>
310 </div>
311</div>
312```
313
314### Persistent Global State
315
316```typescript
317import { registerStore, getStore } from 'voltx.js';
318import { registerPlugin, persistPlugin } from 'voltx.js';
319
320// Register persist plugin
321registerPlugin('persist', persistPlugin);
322
323// Initialize store with persisted values
324const saved = localStorage.getItem('app-store');
325const initialState = saved ? JSON.parse(saved) : { theme: 'light', user: null };
326
327registerStore(initialState);
328
329// Save on changes
330const store = getStore();
331const originalSet = store.set.bind(store);
332store.set = (key, value) => {
333 originalSet(key, value);
334 localStorage.setItem('app-store', JSON.stringify({
335 theme: store.get('theme'),
336 user: store.get('user')
337 }));
338};
339```
340
341## Best Practices
342
343### Use `$store` for Shared State
344
345Global state should live in `$store`:
346
347```html
348<!-- Good: Shared theme in store -->
349<script type="application/json" data-volt-store>
350{ "theme": "dark" }
351</script>
352
353<div data-volt>
354 <p data-volt-class="$store.get('theme')">Content</p>
355</div>
356```
357
358### Use `$pins` for Element Access
359
360Access DOM elements through pins instead of `querySelector`:
361
362```html
363<!-- Good: Using pins -->
364<div data-volt>
365 <input data-volt-pin="username" />
366 <button data-volt-on-click="$pins.username.focus()">Focus</button>
367</div>
368
369<!-- Avoid: Manual querySelector -->
370<div data-volt>
371 <input id="username" />
372 <button data-volt-on-click="document.querySelector('#username').focus()">Focus</button>
373</div>
374```
375
376### Use `data-volt-init` for Setup
377
378Initialize observers and one-time setup in `data-volt-init`:
379
380```html
381<div data-volt
382 data-volt-state='{"count": 0}'
383 data-volt-init="$probe('count', v => console.log('Count:', v))">
384
385 <!-- Component content -->
386</div>
387```
388
389### Scope Pin Names Appropriately
390
391Use descriptive pin names and avoid collisions:
392
393```html
394<!-- Good: Descriptive names -->
395<div data-volt>
396 <input data-volt-pin="searchInput" />
397 <input data-volt-pin="filterInput" />
398</div>
399
400<!-- Avoid: Generic names that might collide -->
401<div data-volt>
402 <input data-volt-pin="input" />
403 <input data-volt-pin="input2" />
404</div>
405```
406
407### Clean Up Observers
408
409Always clean up `$probe` observers when no longer needed:
410
411```html
412<div data-volt
413 data-volt-state='{"active": true}'
414 data-volt-init="const cleanup = $probe('active', v => console.log(v))">
415
416 <button data-volt-on-click="active.set(false); cleanup()">Deactivate</button>
417</div>
418```
419
420## Examples
421
422### Todo App with Global State
423
424```html
425<script type="application/json" data-volt-store>
426{
427 "todos": [],
428 "filter": "all"
429}
430</script>
431
432<!-- Add todo form -->
433<div data-volt data-volt-state='{"newTodo": ""}'>
434 <input data-volt-model="newTodo" data-volt-pin="todoInput" />
435 <button data-volt-on-click="$store.set('todos', [...$store.get('todos'), { text: newTodo.get(), done: false }]); newTodo.set(''); $pins.todoInput.focus()">
436 Add
437 </button>
438</div>
439
440<!-- Filter buttons -->
441<div data-volt>
442 <button data-volt-on-click="$store.set('filter', 'all')">All</button>
443 <button data-volt-on-click="$store.set('filter', 'active')">Active</button>
444 <button data-volt-on-click="$store.set('filter', 'done')">Done</button>
445</div>
446
447<!-- Todo list -->
448<div data-volt>
449 <div data-volt-for="todo in $store.get('todos')">
450 <div data-volt-if="$store.get('filter') === 'all' || ($store.get('filter') === 'done' && todo.done) || ($store.get('filter') === 'active' && !todo.done)">
451 <input type="checkbox" data-volt-bind:checked="todo.done" />
452 <span data-volt-text="todo.text"></span>
453 </div>
454 </div>
455</div>
456```
457
458### Multi-Step Form
459
460```html
461<script type="application/json" data-volt-store>
462{
463 "step": 1,
464 "formData": { "name": "", "email": "", "phone": "" }
465}
466</script>
467
468<div data-volt>
469 <!-- Step indicator -->
470 <p data-volt-text="'Step ' + $store.get('step') + ' of 3'"></p>
471
472 <!-- Step 1: Name -->
473 <div data-volt-if="$store.get('step') === 1">
474 <input data-volt-model="$store.get('formData').name" placeholder="Name" />
475 <button data-volt-on-click="$store.set('step', 2)">Next</button>
476 </div>
477
478 <!-- Step 2: Email -->
479 <div data-volt-if="$store.get('step') === 2">
480 <input data-volt-model="$store.get('formData').email" placeholder="Email" />
481 <button data-volt-on-click="$store.set('step', 1)">Back</button>
482 <button data-volt-on-click="$store.set('step', 3)">Next</button>
483 </div>
484
485 <!-- Step 3: Phone -->
486 <div data-volt-if="$store.get('step') === 3">
487 <input data-volt-model="$store.get('formData').phone" placeholder="Phone" />
488 <button data-volt-on-click="$store.set('step', 2)">Back</button>
489 <button data-volt-on-click="console.log('Submit:', $store.get('formData'))">Submit</button>
490 </div>
491</div>
492```
493
494## API Reference
495
496### `registerStore(state)`
497
498Register global store state programmatically.
499
500```typescript
501import { registerStore } from 'voltx.js';
502import { signal } from 'voltx.js';
503
504registerStore({
505 theme: signal('dark'), // Existing signal
506 count: 0 // Auto-wrapped
507});
508```
509
510### `getStore()`
511
512Get the global store instance.
513
514```typescript
515import { getStore } from 'voltx.js';
516
517const store = getStore();
518store.set('theme', 'light');
519console.log(store.get('theme')); // 'light'
520console.log(store.has('theme')); // true
521```
522
523### `getScopeMetadata(scope)`
524
525Get metadata for a scope (advanced use).
526
527```typescript
528import { getScopeMetadata } from 'voltx.js';
529
530const metadata = getScopeMetadata(scope);
531console.log(metadata.origin); // Root element
532console.log(metadata.pins); // Pin registry
533console.log(metadata.uidCounter); // Current UID counter
534```