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()`.