Binding
How do you connect state to the DOM?
The problem
HTML doesn't know about your JavaScript objects. When you write <span></span>, the browser just sees the literal text "".
Something has to find that placeholder, figure out what count refers to, and replace it with the actual value. That something is the binding system.
Two philosophies
Compile-time
Transform the template before it runs. Turn <span>{count}</span> into createElement('span', count) during the build step.
Runtime
Parse the DOM at load time. Find special attributes, wire them up to state on the fly. No build step needed.
Compile-time is faster (less work at runtime). Runtime is simpler (no tooling required). qrazy uses runtime binding because we're optimizing for understanding, not performance.
qrazy's approach
Four steps:
-
Find scopes:
querySelectorAll('[q-data]')returns every element that defines reactive state. -
Parse state: Take the attribute value
"{ count: 0 }"and turn it into an actual object usingnew Function(). -
Walk the tree: Use
TreeWalkerto visit every descendant element, looking forq-text,q-click, etc. - Bind: For each directive found, create an updater function and store it on the element.
The TreeWalker API
You could use querySelectorAll('*') to get all descendants, but TreeWalker is more efficient and gives you control over the traversal.
const walker = document.createTreeWalker(
root, // starting element
NodeFilter.SHOW_ELEMENT // only visit elements (not text nodes)
);
while (walker.nextNode()) {
const node = walker.currentNode;
// Check for q-* attributes
if (node.hasAttribute('q-text')) {
bindText(node, state);
}
}
qrazy uses this to walk each q-data scope, collecting all bindable elements. We also skip nested q-data scopes—they get their own state.
Expression evaluation
When you write q-text="count + 1", qrazy needs to evaluate that expression in the context of the state object. The trick: new Function().
function evaluate(expr, state) {
const keys = Object.keys(state); // ['count', 'name', ...]
const vals = Object.values(state); // [0, 'Alice', ...]
// Build: new Function('count', 'name', 'return count + 1')
return new Function(...keys, `return ${expr}`)(...vals);
}
// Example:
evaluate('count + 1', { count: 5 }); // → 6
evaluate('name.toUpperCase()', { name: 'alice' }); // → 'ALICE'
This is how Alpine does it too. You're essentially creating a tiny function whose parameters are the state keys, then calling it with the state values.
eval-like behavior. It's safe here because the expressions come from your own HTML—same-origin code. Never evaluate user-provided strings this way.
The scope chain
What if you have nested q-data scopes?
<div q-data="{ theme: 'dark' }">
<div q-data="{ count: 0 }">
<!-- Can this access theme? -->
<span q-text="theme"></span>
</div>
</div>
In qrazy's current design: no. Each q-data scope is independent. The inner div only sees count, not theme.
This is a simplification. Alpine supports scope inheritance. Vue has provide/inject. For qrazy, we kept it simple—one scope, one state object.
Storing updaters
When state changes, we need to update the DOM. But we don't want to re-walk the entire tree every time. Solution: store updater functions on the elements themselves.
if (el.hasAttribute('q-text')) {
const expr = el.getAttribute('q-text');
// Create an updater function
const update = () => {
el.textContent = evaluate(expr, state);
};
// Run it once (initial render)
update();
// Store it for later
el._qUpdaters = el._qUpdaters || [];
el._qUpdaters.push(update);
}
Now when state changes, we just loop through elements and call their stored updaters:
const state = reactive(data, () => {
for (const el of elements) {
if (el._qUpdaters) {
el._qUpdaters.forEach(fn => fn());
}
}
});
This is "coarse-grained" reactivity—any change triggers all updaters. Fine-grained systems (like Solid) track dependencies per-expression so they only run affected updaters.
Interactive demo
Watch the binding system connect state to DOM:
Try it: Live binding
The input uses q-model for two-way binding. The output uses q-text with an expression. The checkbox uses q-click to toggle state.
First principles
What is a binding? It's a contract: "when this state changes, update that DOM node." The binding system is just a registry of these contracts.
Some frameworks make bindings implicit (React's re-render). Others make them explicit (qrazy's attributes). Either way, you're mapping state → DOM.
The key insight: the DOM and your state are two representations of the same truth. Binding keeps them synchronized.