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```