You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/internals/proxies.md
+17-2Lines changed: 17 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,7 +33,7 @@ When a consumer reads `proxy.key`:
33
33
If it’s an object/function, we recursively call `reactive()` so nested access stays reactive.
34
34
Otherwise we return `signal.get()` which unwraps the value.
35
35
36
-
This layered approach means `reactive()` objects are safe to embed in evaluator scopes—the same dangerous keys are filtered and every nested property remains reactive.
36
+
This layered approach means `reactive()` objects are safe to embed in evaluator scopes. The same dangerous keys are filtered and every nested property remains reactive.
37
37
38
38
## Property Mutation (set trap)
39
39
@@ -64,7 +64,7 @@ Methods that do not mutate (e.g. `slice`) pass through unwrapped.
64
64
65
65
## Integration with Signals
66
66
67
-
Every reactive property is backed by a `signal`. This keeps the proxy layer thin—core logic lives in `signal.ts`, and the proxy simply orchestrates reads/writes against those signals.
67
+
Every reactive property is backed by a `signal`. This keeps the proxy layer thin. Core logic lives in `signal.ts`, and the proxy simply orchestrates reads/writes against those signals.
68
68
Because signals already integrate with dependency tracking, reactive object reads automatically wire into computeds, effects, and DOM bindings without extra bookkeeping.
69
69
70
70
## Interop Utilities
@@ -82,6 +82,21 @@ Bindings and expressions run via the hardened evaluator. When it encounters a re
82
82
- Signals returned from proxy properties expose `get`, `set`, and `subscribe`, but property reads on the signal proxy delegate back to the underlying value.
83
83
- Primitive coercion works because the wrapper defines `valueOf`, `toString`, and `Symbol.toPrimitive` on demand.
84
84
- Boolean negation (`!signal`) is rewritten to `!$unwrap(signal)` before compilation so reactive values behave like plain booleans.
85
+
-**Iteration support** - Signal wrappers implement `Symbol.iterator` to enable spread operations on signals containing iterable values.
86
+
87
+
### Spread Operator Support
88
+
89
+
When a signal contains an iterable value (like an array), the wrapper proxy delegates the `Symbol.iterator` property to the unwrapped value.
90
+
This enables the JavaScript spread operator to work transparently:
Copy file name to clipboardExpand all lines: docs/internals/reactivity.md
+30-5Lines changed: 30 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,6 +1,6 @@
1
1
# Reactivity Architecture
2
2
3
-
VoltX’s reactivity system is built around a small set of primitives—signals, computed signals, and effects—that coordinate via an explicit dependency tracker.
3
+
VoltX’s reactivity system is built around a small set of primitives: signals, computed signals, and effects, that coordinate via an explicit dependency tracker.
4
4
This document explains how those pieces fit together, how updates flow through the system, and the trade-offs we made while hardening the implementation.
5
5
6
6
## Signals
@@ -88,6 +88,33 @@ The evaluator uses a scope proxy that wraps signal objects differently based on
88
88
89
89
This dual behavior is controlled by the `opts.unwrapSignals` parameter passed to `evaluate()`.
90
90
91
+
### Object Literal Unwrapping
92
+
93
+
A subtle challenge arises when event handlers create object literals using signal values. Consider this common pattern:
Without special handling, the object literal `{id: todoId, text: newText, done: false}` would capture **wrapped signal proxies** as property values instead of their unwrapped values. This breaks equality comparisons later when trying to match todos by ID.
102
+
103
+
To solve this, the `transformExpr` function applies a compile-time transformation: it automatically unwraps signal identifiers used directly as object property values. The expression above is rewritten to:
- Only applies to simple identifiers after `:` in object literals (e.g., `{key: identifier}`)
112
+
- Does not affect method calls (e.g., `{text: newText.trim()}` remains unchanged)
113
+
- Does not affect property access or computed values (e.g., `{id: obj.id}` remains unchanged)
114
+
- Ensures object literals created in write-mode contexts contain primitive values, not wrapper proxies
115
+
116
+
This keeps the mental model simple: users write natural JavaScript object literals and the evaluator ensures signal values are materialized correctly, regardless of whether `unwrapSignals` is true or false.
117
+
91
118
## Scope Helpers
92
119
93
120
When a scope is mounted, VoltX injects several helpers that lean on the reactive core:
@@ -114,13 +141,11 @@ When batching is needed, use `$pulse` or wrap updates in a custom queue.
114
141
## Challenges & Trade-offs
115
142
116
143
-**Minimal core vs features** - The system intentionally avoids hidden mutation queues or scheduler magic.
117
-
This keeps mental models simple but means users must explicitly batch when necessary.
144
+
This keeps mental models simple but means users must explicitly batch when necessary.
118
145
-**Signal identity** - Equality checks are referential.
119
146
While fast, it means that mutating nested objects without cloning can bypass change detection unless you touch the signal again.
120
147
We emphasises immutable patterns or explicit `set()` calls with copies.
We balance accuracy with performance by focusing on common patterns and falling back to runtime evaluation if static analysis fails.
123
150
-**Error resilience** - Subscriber callbacks, cleanup functions, and recompute bodies are wrapped in try/catch to prevent one failure from derailing the reactive loop.
124
-
The trade-off is noisy console logs, but the alternative—silently swallowing issues—was harder to debug.
125
-
126
-
Despite the lightweight implementation, these primitives provide deterministic, traceable update flows that underpin VoltX’s declarative bindings and plugin ecosystem.
151
+
The trade-off is noisy console logs, but the alternative (silent errors & no observability) was harder to debug.
Copy file name to clipboardExpand all lines: docs/usage/bindings.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -364,6 +364,7 @@ The binding syntax is `data-volt-url:signalName="urlPart"` where URL part is:
364
364
365
365
-`query`: Sync with query parameter (e.g., `?page=1`)
366
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"`)
367
368
368
369
Signal changes update the URL, and URL changes (back/forward navigation) update signals. This enables client-side routing without additional libraries.
Copy file name to clipboardExpand all lines: docs/usage/state.md
+3Lines changed: 3 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -64,6 +64,7 @@ The name becomes a signal in the scope, and the attribute value is the computati
64
64
```
65
65
66
66
Computed values defined this way follow the same rules as programmatic computed signals: they track dependencies and update automatically.
67
+
For multi-word signal names, prefer kebab-case in the attribute (e.g., `data-volt-computed:active-todos`) — HTML lowercases attribute names and Volt converts kebab-case back to camelCase (`activeTodos`) automatically.
67
68
68
69
## Programmatic State
69
70
@@ -105,6 +106,7 @@ VoltX automatically unwraps signals in read contexts, making expressions simpler
105
106
```
106
107
107
108
**Read Contexts** (signals auto-unwrapped):
109
+
108
110
-`data-volt-text`, `data-volt-html`
109
111
-`data-volt-if`, `data-volt-else`
110
112
-`data-volt-for`
@@ -113,6 +115,7 @@ VoltX automatically unwraps signals in read contexts, making expressions simpler
113
115
-`data-volt-computed:*` expressions
114
116
115
117
**Write Contexts** (signals not auto-unwrapped):
118
+
116
119
-`data-volt-on-*` event handlers
117
120
-`data-volt-init` initialization code
118
121
-`data-volt-model` (handles both read and write automatically)
Copy file name to clipboardExpand all lines: lib/README.md
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -66,7 +66,9 @@ Plugins are opt-in and can be combined declaratively or registered programmatica
66
66
67
67
## VoltX.css
68
68
69
-
VoltX ships with an optional classless CSS framework inspired by Pico CSS and Tufte CSS. It provides beautiful, semantic styling without requiring any CSS classes—just write semantic HTML and it looks great. It's perfect for prototyping.
69
+
VoltX ships with an optional classless CSS framework inspired by Pico CSS and Tufte CSS.
70
+
It provides beautiful, semantic styling without requiring any CSS classes.
71
+
Just write semantic HTML and it looks great. It's perfect for prototyping.
0 commit comments