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