a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# VoltX Bindings 2 3Bindings connect reactive state to the DOM using `data-volt-*` attributes. Each binding evaluates expressions and updates the DOM when dependencies change. 4 5All bindings support the full expression syntax documented in [Expression Evaluation](./expressions), including property access, operators, function calls, and signal unwrapping. 6 7## Content 8 9### Text Content 10 11The `data-volt-text` binding updates an element's text content: 12 13```html 14<p data-volt-text="message">Fallback text</p> 15``` 16 17Text content is automatically escaped for security. Use this binding for any user-generated content to prevent XSS attacks. 18 19The fallback text (between the opening and closing tags) is displayed until the framework mounts and evaluates the expression. 20 21### HTML Content 22 23The `data-volt-html` binding updates an element's innerHTML: 24 25```html 26<div data-volt-html="richContent"></div> 27``` 28 29This binding renders raw HTML without escaping. Only use it with trusted content. Never use `data-volt-html` with user-generated content unless it has been sanitized. 30 31The binding removes existing children before inserting new content. Event listeners on removed elements are not automatically cleaned up—prefer using `data-volt-if` for conditional content with event handlers. 32 33## Attributes 34 35### Generic Attributes 36 37The `data-volt-bind:*` syntax binds any HTML attribute: 38 39```html 40<img data-volt-bind:src="imageUrl" data-volt-bind:alt="imageAlt"> 41<a data-volt-bind:href="linkUrl" data-volt-bind:target="linkTarget">Link</a> 42<input data-volt-bind:disabled="isDisabled" data-volt-bind:placeholder="placeholderText"> 43``` 44 45The attribute name follows the colon. The expression value is converted to a string and set as the attribute value. 46 47For boolean attributes (`disabled`, `checked`, `required`, etc.), the attribute is added when the expression is truthy and removed when falsy. 48 49### Class Binding 50 51The `data-volt-class` binding toggles CSS classes based on an object expression: 52 53```html 54<div data-volt-class="{ active: isActive, disabled: !canInteract, 'has-error': hasError }"> 55``` 56 57Each key in the object is a class name. When the corresponding value is truthy, the class is added; when falsy, the class is removed. 58 59Class names with hyphens or spaces must be quoted. The binding preserves existing classes not managed by VoltX.js. 60 61## Event Bindings 62 63### Event Listeners 64 65The `data-volt-on-*` syntax attaches event listeners where the wildcard is the event name: 66 67```html 68<button data-volt-on-click="handleClick">Click me</button> 69<input data-volt-on-input="query.set($event.target.value)"> 70<form data-volt-on-submit="handleSubmit($event)"> 71``` 72 73Event handlers receive two special scope variables: 74 75- `$event`: The native browser event object 76- `$el`: The element that has the binding 77 78Event expressions commonly call functions or set signal values. The event's default behavior can be prevented by calling `$event.preventDefault()` in the expression. 79 80### Supported Events 81 82Any valid DOM event name works: 83 84- Mouse: `click`, `dblclick`, `mousedown`, `mouseup`, `mouseenter`, `mouseleave`, `mousemove` 85- Keyboard: `keydown`, `keyup`, `keypress` 86- Form: `input`, `change`, `submit`, `focus`, `blur` 87- Touch: `touchstart`, `touchmove`, `touchend` 88- Drag: `dragstart`, `dragover`, `drop` 89- Media: `play`, `pause`, `ended`, `timeupdate` 90 91Event names are case-insensitive in HTML but case-sensitive in XHTML. Use lowercase for consistency. 92 93## Form Bindings 94 95### Two-Way Binding 96 97The `data-volt-model` binding creates two-way synchronization between form inputs and signals: 98 99```html 100<input data-volt-model="username"> 101<textarea data-volt-model="bio"></textarea> 102<select data-volt-model="country"> 103 <option value="us">United States</option> 104 <option value="uk">United Kingdom</option> 105</select> 106``` 107 108The binding works with text inputs, textareas, select dropdowns, checkboxes, and radio buttons. 109 110For checkboxes, the signal value is a boolean. For radio buttons, all inputs with the same `data-volt-model` should share the same signal, and each input should have a unique `value` attribute. 111 112The binding listens for `input` events and updates the signal with the current value. It also sets the initial value from the signal when mounting. 113 114### Checkbox Arrays 115 116Multiple checkboxes can bind to an array signal: 117 118```html 119<input type="checkbox" value="red" data-volt-model="colors"> 120<input type="checkbox" value="green" data-volt-model="colors"> 121<input type="checkbox" value="blue" data-volt-model="colors"> 122``` 123 124When checked, the checkbox value is added to the array. When unchecked, it's removed. The signal must be initialized as an array. 125 126## Conditional Rendering 127 128### If/Else 129 130The `data-volt-if` binding conditionally renders elements based on expression truthiness: 131 132```html 133<p data-volt-if="isLoggedIn">Welcome back!</p> 134<p data-volt-else>Please log in.</p> 135``` 136 137When the condition is falsy, the element is removed from the DOM entirely. When truthy, it's inserted back. 138 139The `data-volt-else` binding must immediately follow a `data-volt-if` sibling element. It renders when the preceding `if` condition is falsy. 140 141Removed elements and their children are completely disposed, including event listeners and nested bindings. This prevents memory leaks and ensures clean teardown. 142 143### Show/Hide Alternative 144 145For toggling visibility without removing elements from the DOM, use `data-volt-class` with a `hidden` class: 146 147```html 148<style> 149 .hidden { display: none; } 150</style> 151<p data-volt-class="{ hidden: !isVisible }">Toggle me</p> 152``` 153 154This approach is more performant for frequently toggled content since elements remain in the DOM. 155 156## List Rendering 157 158### For Loop 159 160The `data-volt-for` binding repeats elements for each item in an array: 161 162```html 163<ul> 164 <li data-volt-for="item in items" data-volt-text="item.name"></li> 165</ul> 166``` 167 168The syntax is `item in array` where `item` is the loop variable name and `array` is an expression that resolves to an array. 169 170Each iteration creates a new scope with: 171 172- `item`: The current array element 173- `$index`: The zero-based index (number) 174 175The binding tracks array changes and efficiently updates the DOM: 176 177- New items are appended 178- Removed items are disposed 179- Reordered items are moved 180 181For optimal performance with large lists, ensure array items have stable identities. Mutating the array in place triggers re-renders for affected items only. 182 183### Nested Loops 184 185Loops can be nested to render multidimensional data: 186 187```html 188<div data-volt-for="category in categories"> 189 <h2 data-volt-text="category.name"></h2> 190 <ul> 191 <li data-volt-for="product in category.products" data-volt-text="product.name"></li> 192 </ul> 193</div> 194``` 195 196Each loop creates its own scope. Inner loops can access outer loop variables. 197 198### Index Access 199 200Use the `$index` variable to access the current iteration index: 201 202```html 203<ul> 204 <li data-volt-for="item in items"> 205 <span data-volt-text="$index + 1"></span>: <span data-volt-text="item.name"></span> 206 </li> 207</ul> 208``` 209 210## HTTP 211 212HTTP bindings enable declarative AJAX requests without writing JavaScript. They integrate with hypermedia patterns for server-rendered HTML fragments. 213 214### HTTP Methods 215 216Each HTTP method has a corresponding binding: 217 218```html 219<button data-volt-get="/api/users">Fetch Users</button> 220<form data-volt-post="/api/users">...</form> 221<button data-volt-put="/api/users/1">Update</button> 222<button data-volt-patch="/api/users/1">Patch</button> 223<button data-volt-delete="/api/users/1">Delete</button> 224``` 225 226The binding value is the URL. When the element is activated (clicked for buttons, submitted for forms), the request is sent. 227 228### Target and Swap 229 230Control where and how the response HTML is inserted using `data-volt-target` and `data-volt-swap`: 231 232```html 233<button 234 data-volt-get="/partials/content" 235 data-volt-target="#main" 236 data-volt-swap="innerHTML"> 237 Load Content 238</button> 239``` 240 241**Target** is a CSS selector identifying the element to update. If omitted, the element with the HTTP binding is the target. 242 243**Swap** strategies determine how content is inserted: 244 245- `innerHTML`: Replace target's content (default) 246- `outerHTML`: Replace target itself 247- `beforebegin`: Insert before target 248- `afterbegin`: Insert as target's first child 249- `beforeend`: Insert as target's last child 250- `afterend`: Insert after target 251- `delete`: Remove target from DOM 252- `none`: Make request but don't modify DOM 253 254### Form Serialization 255 256Forms with HTTP bindings automatically serialize input values: 257 258```html 259<form data-volt-post="/api/login"> 260 <input name="username" data-volt-model="username"> 261 <input name="password" type="password" data-volt-model="password"> 262 <button type="submit">Login</button> 263</form> 264``` 265 266The framework serializes form data based on the HTTP method: 267 268- GET/DELETE: Query parameters in URL 269- POST/PUT/PATCH: Request body as `application/x-www-form-urlencoded` or `multipart/form-data` for file uploads 270 271### Loading Indicators 272 273Show loading states during requests with `data-volt-indicator`: 274 275```html 276<button data-volt-get="/api/data" data-volt-indicator="#spinner"> 277 Load Data 278</button> 279<div id="spinner" class="hidden">Loading...</div> 280``` 281 282The indicator element (selected by CSS selector) has a `loading` class added during the request and removed when complete. 283 284### Retry Logic 285 286Enable automatic retry with exponential backoff using `data-volt-retry`: 287 288```html 289<button 290 data-volt-get="/api/unreliable" 291 data-volt-retry="3"> 292 Fetch with Retry 293</button> 294``` 295 296The binding value is the maximum number of retry attempts. The framework automatically retries failed requests with increasing delays (1s, 2s, 4s, etc.). 297 298Retries only occur for network failures and 5xx server errors. Client errors (4xx) are not retried. 299 300## Plugins 301 302Plugins extend the framework with additional bindings. Register plugins before mounting to make their bindings available. 303 304### Persist 305 306The `data-volt-persist` binding synchronizes signals with browser storage: 307 308```html 309<div 310 data-volt 311 data-volt-state='{"theme": "light"}' 312 data-volt-persist:theme="localStorage"> 313</div> 314``` 315 316The binding syntax is `data-volt-persist:signalName="storageType"` where storage type is: 317 318- `localStorage`: Persists across browser sessions 319- `sessionStorage`: Persists for the current tab session 320- `indexedDB`: For large datasets (async) 321 322The signal value is serialized to JSON and stored. On mount, stored values override initial state. 323 324### Scroll 325 326Scroll bindings manage scroll position and behavior: 327 328```html 329<!-- Restore scroll position on navigation --> 330<div data-volt-scroll-restore></div> 331 332<!-- Scroll to element --> 333<button data-volt-scroll-to="#target">Scroll to Target</button> 334 335<!-- Scroll spy (add class when in viewport) --> 336<section data-volt-scroll-spy="active"></section> 337 338<!-- Smooth scrolling --> 339<div data-volt-scroll-smooth></div> 340``` 341 342**Scroll restore** saves scroll position before navigation and restores it when returning. 343 344**Scroll to** scrolls the viewport to bring the target element into view when activated. 345 346**Scroll spy** adds a class when the element enters the viewport and removes it when leaving. 347 348**Smooth scrolling** enables CSS `scroll-behavior: smooth` for the element. 349 350### URL Synchronization 351 352The `data-volt-url` binding syncs signals with URL query parameters or hash: 353 354```html 355<div 356 data-volt 357 data-volt-state='{"page": 1, "query": ""}' 358 data-volt-url:page="query" 359 data-volt-url:query="query"> 360</div> 361``` 362 363The binding syntax is `data-volt-url:signalName="urlPart"` where URL part is: 364 365- `query`: Sync with query parameter (e.g., `?page=1`) 366- `hash`: Sync with URL hash (e.g., `#section`) 367- `history`: Sync with the full pathname + search (e.g., `data-volt-url:route="history:/app"`) 368 369Signal changes update the URL, and URL changes (back/forward navigation) update signals. This enables client-side routing without additional libraries. 370 371## Custom Bindings 372 373Register custom bindings for domain-specific behavior using the plugin API: 374 375```js 376import { registerPlugin } from 'voltx.js'; 377// or: import { registerPlugin } from '@voltx/core'; 378 379registerPlugin('tooltip', (ctx) => { 380 const message = ctx.evaluate(ctx.element.getAttribute('data-volt-tooltip')); 381 const tooltip = document.createElement('div'); 382 tooltip.className = 'tooltip'; 383 tooltip.textContent = message; 384 385 ctx.element.addEventListener('mouseenter', () => { 386 document.body.appendChild(tooltip); 387 }); 388 389 ctx.element.addEventListener('mouseleave', () => { 390 tooltip.remove(); 391 }); 392 393 ctx.addCleanup(() => tooltip.remove()); 394}); 395``` 396 397The plugin context provides: 398 399- `element`: The DOM element with the binding 400- `scope`: Signal scope for the mounted component 401- `evaluate(expression)`: Evaluate expressions in the current scope 402- `findSignal(path)`: Find signals by property path 403- `addCleanup(fn)`: Register cleanup callbacks 404 405Custom bindings should always register cleanup to prevent memory leaks. 406 407## Lifecycle 408 409All bindings follow a consistent lifecycle: 410 4111. **Mount**: The `charge()` or `mount()` function discovers elements with `data-volt-*` attributes 4122. **Evaluation**: Expressions are parsed and evaluated in the current scope 4133. **Subscription**: Bindings subscribe to referenced signals 4144. **Update**: When signals change, bindings re-evaluate and update the DOM 4155. **Cleanup**: When unmounted, subscriptions are disposed and DOM changes are reverted 416 417Cleanup is automatic for all built-in bindings. Custom bindings must explicitly register cleanup using `ctx.addCleanup()`.