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.

React JSX, Svelte, Vue SFC

Runtime

Parse the DOM at load time. Find special attributes, wire them up to state on the fly. No build step needed.

Alpine, htmx, qrazy

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:

  1. Find scopes: querySelectorAll('[q-data]') returns every element that defines reactive state.
  2. Parse state: Take the attribute value "{ count: 0 }" and turn it into an actual object using new Function().
  3. Walk the tree: Use TreeWalker to visit every descendant element, looking for q-text, q-click, etc.
  4. 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.

Security note: This uses 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.