a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
1# Counter (Example) 2 3This tutorial walks through building a simple counter application to demonstrate VoltX.js fundamentals: reactive state, event handling, computed values, and declarative markup. 4 5## Basic Counter (Declarative) 6 7The simplest way to build a counter is using declarative state and bindings directly in HTML. 8 9Create an HTML file with this structure: 10 11```html 12<!DOCTYPE html> 13<html lang="en"> 14<head> 15 <meta charset="UTF-8"> 16 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 17 <title>Counter - VoltX.js</title> 18</head> 19<body> 20 <div data-volt data-volt-state='{"count": 0}'> 21 <h1 data-volt-text="count">0</h1> 22 <button data-volt-on-click="count.set(count.get() + 1)">Increment</button> 23 </div> 24 25 <script type="module"> 26 import { charge } from 'https://unpkg.com/voltx.js@latest/dist/volt.js'; 27 charge(); 28 </script> 29</body> 30</html> 31``` 32 33**How it works:** 34 35The `data-volt` attribute marks the root element for mounting. Inside, `data-volt-state` declares initial state as inline JSON. 36The framework converts `count` into a reactive signal automatically. 37 38The `data-volt-text` binding displays the current count value. When the signal changes, the text content updates automatically. 39 40The `data-volt-on-click` binding attaches a click handler that increments the count. We call `count.get()` to read the current value and `count.set()` to update it. 41 42Finally, `charge()` discovers all `[data-volt]` elements and mounts them with their declared state. 43 44## Adding Decrement 45 46Extend the counter with both increment and decrement buttons: 47 48```html 49<div data-volt data-volt-state='{"count": 0}'> 50 <h1 data-volt-text="count">0</h1> 51 <button data-volt-on-click="count.set(count.get() - 1)">-</button> 52 <button data-volt-on-click="count.set(count.get() + 1)">+</button> 53</div> 54``` 55 56Each button calls `count.set()` with a different expression. The decrement button subtracts 1, while increment adds 1. 57 58## Computed Values 59 60Add derived state using `data-volt-computed` to show whether the count is positive, negative, or zero: 61 62```html 63<div data-volt 64 data-volt-state='{"count": 0}' 65 data-volt-computed:status="count > 0 ? 'positive' : count < 0 ? 'negative' : 'zero'"> 66 <h1 data-volt-text="count">0</h1> 67 <p>Status: <span data-volt-text="status">zero</span></p> 68 69 <button data-volt-on-click="count.set(count.get() - 1)">-</button> 70 <button data-volt-on-click="count.set(count.get() + 1)">+</button> 71</div> 72``` 73 74The `data-volt-computed:status` attribute creates a computed signal named `status`. It uses a ternary expression to classify the count. When `count` changes, `status` recalculates automatically. 75 76## Conditional Rendering 77 78Show different messages based on the count value using conditional bindings: 79 80```html 81<div data-volt data-volt-state='{"count": 0}'> 82 <h1 data-volt-text="count">0</h1> 83 84 <p data-volt-if="count === 0">The count is zero</p> 85 <p data-volt-if="count > 0" data-volt-text="'Positive: ' + count"></p> 86 <p data-volt-if="count < 0" data-volt-text="'Negative: ' + count"></p> 87 88 <button data-volt-on-click="count.set(count.get() - 1)">-</button> 89 <button data-volt-on-click="count.set(count.get() + 1)">+</button> 90 <button data-volt-on-click="count.set(0)">Reset</button> 91</div> 92``` 93 94The `data-volt-if` binding conditionally renders elements. Only one paragraph displays at a time based on the count value. A reset button sets the count back to zero. 95 96## Styling with Classes 97 98Apply dynamic CSS classes based on state: 99 100```html 101<style> 102 .counter { 103 padding: 2rem; 104 text-align: center; 105 font-family: system-ui, sans-serif; 106 } 107 108 .display { 109 font-size: 4rem; 110 margin: 1rem 0; 111 } 112 113 .positive { color: #22c55e; } 114 .negative { color: #ef4444; } 115 .zero { color: #6b7280; } 116 117 button { 118 font-size: 1.5rem; 119 padding: 0.5rem 1.5rem; 120 margin: 0.25rem; 121 cursor: pointer; 122 } 123</style> 124``` 125 126```html 127<div class="counter" 128 data-volt 129 data-volt-state='{"count": 0}'> 130 <h1 class="display" 131 data-volt-text="count" 132 data-volt-class="{ positive: count > 0, negative: count < 0, zero: count === 0 }"> 133 0 134 </h1> 135 136 <div> 137 <button data-volt-on-click="count.set(count.get() - 1)">-</button> 138 <button data-volt-on-click="count.set(0)">Reset</button> 139 <button data-volt-on-click="count.set(count.get() + 1)">+</button> 140 </div> 141</div> 142``` 143 144The `data-volt-class` binding takes an object where keys are class names and values are conditions. When `count` is positive, the `positive` class applies. When negative, the `negative` class applies. When zero, the `zero` class applies. 145 146## Persisting State 147 148Use the persist plugin to save the count across page reloads: 149 150```html 151<div data-volt 152 data-volt-state='{"count": 0}' 153 data-volt-persist:count="localStorage"> 154 <h1 data-volt-text="count">0</h1> 155 156 <button data-volt-on-click="count.set(count.get() - 1)">-</button> 157 <button data-volt-on-click="count.set(0)">Reset</button> 158 <button data-volt-on-click="count.set(count.get() + 1)">+</button> 159</div> 160 161<script type="module"> 162 import { charge, registerPlugin, persistPlugin } from 'https://unpkg.com/voltx.js@latest/dist/volt.js'; 163 164 registerPlugin('persist', persistPlugin); 165 charge(); 166</script> 167``` 168 169The `data-volt-persist:count="localStorage"` binding synchronizes the `count` signal with browser localStorage. When the count changes, it's saved automatically. When the page loads, the saved value is restored. 170 171## Step Counter 172 173Build a counter that increments by a configurable step value: 174 175```html 176<div data-volt data-volt-state='{"count": 0, "step": 1}'> 177 <h1 data-volt-text="count">0</h1> 178 179 <label> 180 Step: 181 <input type="number" data-volt-model="step" min="1" value="1"> 182 </label> 183 184 <div> 185 <button data-volt-on-click="count.set(count.get() - step)">-</button> 186 <button data-volt-on-click="count.set(0)">Reset</button> 187 <button data-volt-on-click="count.set(count.get() + step)">+</button> 188 </div> 189</div> 190``` 191 192The `data-volt-model` binding creates two-way synchronization between the input and the `step` signal. As you type, the step value updates. The increment and decrement buttons use the current step value. 193 194## Bounded Counter 195 196Add minimum and maximum bounds with disabled button states: 197 198```html 199<div data-volt 200 data-volt-state='{"count": 0, "min": -10, "max": 10}'> 201 <h1 data-volt-text="count">0</h1> 202 203 <div> 204 <button 205 data-volt-on-click="count.set(count.get() - 1)" 206 data-volt-bind:disabled="count <= min"> 207 - 208 </button> 209 <button data-volt-on-click="count.set(0)">Reset</button> 210 <button 211 data-volt-on-click="count.set(count.get() + 1)" 212 data-volt-bind:disabled="count >= max"> 213 + 214 </button> 215 </div> 216 217 <p>Range: <span data-volt-text="min"></span> to <span data-volt-text="max"></span></p> 218</div> 219``` 220 221The `data-volt-bind:disabled` binding disables buttons when the count reaches the minimum or maximum. The decrement button disables at the minimum, and the increment button disables at the maximum. 222 223## Programmatic Counter 224 225For applications requiring initialization logic or custom functions, use the programmatic API: 226 227```html 228<script type="module"> 229 import { mount, signal, computed } from 'https://unpkg.com/voltx.js@latest/dist/volt.js'; 230 231 const count = signal(0); 232 const message = computed(() => { 233 const value = count.get(); 234 if (value === 0) return 'Start counting!'; 235 if (value > 0) return `Up by ${value}`; 236 return `Down by ${Math.abs(value)}`; 237 }, [count]); 238 239 const increment = () => { 240 count.set(count.get() + 1); 241 }; 242 243 const decrement = () => { 244 count.set(count.get() - 1); 245 }; 246 247 const reset = () => { 248 count.set(0); 249 }; 250 251 mount(document.querySelector('#app'), { 252 count, 253 message, 254 increment, 255 decrement, 256 reset 257 }); 258</script> 259``` 260 261This approach creates signals explicitly using `signal()` and `computed()`. Functions are defined for event handlers and passed to the scope object. The `mount()` function attaches bindings to the element. 262 263Use programmatic mounting when you need: 264 265- Complex initialization logic 266- Integration with external libraries 267- Signals shared across multiple components 268- Custom validation or transformation 269 270## Summary 271 272This counter demonstrates core VoltX.js concepts: 273 274- Reactive state with signals 275- Event handling with `data-volt-on-*` 276- Computed values deriving from state 277- Conditional rendering with `data-volt-if` 278- Two-way form binding with `data-volt-model` 279- Attribute binding with `data-volt-bind:*` 280- Dynamic classes with `data-volt-class` 281- State persistence with plugins 282 283## Further Reading 284 285- [State Management](./state) for advanced signal patterns 286- [Bindings](./bindings) for complete binding reference 287- [Expressions](./expressions) for template syntax details 288- [Lifecycle](./lifecycle) for mount/unmount hooks 289- [Server-Side Rendering](./ssr) for hydration strategies