How Binding Works

How a data-* attribute connects a DOM element to your state, stays in sync, and cleans up after itself, all without a virtual DOM.

Core Concept: A binding links a DOM element to the state it reads. When you add data-bind="username" to an element, WildflowerJS creates a small reactive effect for it: a function that reads state.username and writes the value to the element. Reading the state records a dependency edge, so when state.username changes, the framework wakes exactly that effect and updates exactly that element. Nothing else is scanned, diffed, or re-rendered.

Every reactive attribute works the same way. The attribute names what to read and how to apply it; the effect does the reading and writing; the dependency graph connects the two. The sections below cover the kinds of bindings, how one is born and cleaned up, and how to bind across entities.

Binding Types

Each reactive attribute creates an effect tuned to a particular job:

Attribute Purpose What the effect does
data-bind Display reactive data Reads a value and writes it to the element's text or value
data-action Handle events Invokes a method when the bound DOM event fires
data-show, data-render Conditional visibility (data-show) or full mount/unmount (data-render) Reads a condition and toggles visibility, or mounts/unmounts the element
data-list Array rendering Reconciles the array by identity and registers each row's bound fields on a per-list update dispatcher
data-model Two-way binding Writes state to the input and writes input changes back to state
📝 Namespace Prefix: All data-* attributes also support a data-wf-* prefix (e.g., data-wf-bind, data-wf-action). Use the wf prefix when integrating with third-party libraries that may conflict with standard data-* attributes. Both prefixes are functionally identical.

The Life of a Binding

A binding goes through three phases, and none of them involve a central registry or a cleanup timer.

1. Creation

When the framework scans the DOM (during component initialization, or after dynamic content is added), it finds elements with reactive attributes and creates the matching effect for each:

  • data-bind="username" → an effect that reads state.username and writes it to the element
  • data-action="save" → a handler that calls save() when the element is clicked
  • data-list="items" → a list reconciler that renders one row per item and registers each row's bound fields for targeted updates
  • data-show="isVisible" → an effect that reads isVisible and toggles the element's visibility
  • data-model="email" → a two-way binding between the input and state.email

On its first run, each effect reads the state it needs. Those reads record dependency edges in the graph, so the effect is now linked to exactly the values it used.

2. Active

Once created, effects respond to change through their edges:

  • Display bindings re-run when a value they read changes, and write the new value to their element
  • Actions wait for their DOM event and invoke the bound method
  • Lists reconcile by identity on array change, touching the DOM only for items that were added, removed, or moved. An in-place property change (items[3].qty = 5) routes through the list's dispatcher to exactly the bindings that read that field: for a field with a single plain binding, that is one direct write to one element, with no re-render of the row and no scan of its siblings
  • Conditionals re-evaluate when their condition's inputs change and show, hide, mount, or unmount accordingly
  • Models sync both ways: input changes write state, state changes update the input

Because each effect only depends on the values it actually read, a change to state.username wakes the username binding and nothing else.

3. Cleanup

Bindings are owned by the component or list that created them. When that owner goes away, its registrations and their edges go with it, in a single pass:

  • Component destruction: destroying a component disposes every effect it owns
  • List item removal: removing an item clears that row's registrations from the list's dispatcher; the list itself owns one dispatcher, not one effect per row
  • Dynamic content replacement: replacing an element's content disposes the bindings attached to the old content

There is no registry to scan and no periodic garbage collection. Cleanup is deterministic because the scope owns the binding: a binding is destroyed with the element it was created for.

Update timing: the guarantee to build on is simple: by the time your await, timeout, or next event handler runs, the DOM reflects your state, and within any frame the user sees a consistent picture. Some updates apply immediately at the moment of the write and others coalesce on the microtask; which is which is an internal optimization detail that may change between releases, so don't build on intra-tick DOM timing.
Note: data-show keeps an element's effect alive when hiding; it only toggles CSS visibility, which is efficient for frequently toggled content. For content that should fully unmount and dispose its effects, use data-render instead.

Binding Across Entities

To read another entity's state from a template, use the $ universal accessor: data-bind="$entity.path". For JavaScript-level access, use subscribe with this.stores for stores, or wildflower.getComponent() for components. All three record a cross-entity dependency, so the binding updates when the source changes.

<!-- HTML: bind directly to another entity's state -->
<span data-bind="$user-session.user.name"></span>
<div data-show="$auth.isLoggedIn">Welcome!</div>
<span data-bind="$cart.itemCount"></span>
// JavaScript: use subscribe + this.stores for store access
wildflower.component('cart-badge', {
    subscribe: {
        cart: ['items']
    },

    computed: {
        itemCount() {
            return this.stores.cart.items.length
        }
    }
})

// JavaScript: use getComponent() for component access
wildflower.component('themed-panel', {
    computed: {
        themeClass() {
            const theme = wildflower.getComponent('theme-manager')
            return theme ? 'theme-' + theme.mode : 'theme-light'
        }
    }
})
✅ Automatic dependency tracking:
  • Bindings that read another entity via $entity.path, subscribe, or getComponent() register as dependents automatically
  • A change in the source updates only the bindings that read it
  • Cross-entity cycles are detected and broken rather than looping forever
  • Cleanup happens automatically when the dependent is destroyed

The one rule: always reach across entities through one of these tracking surfaces. If you capture a raw reference to another entity's state in a closure, you bypass the tracking proxy and the dependency is silently lost, so the update never fires. See Communication for the full set of cross-entity patterns.

Best Practices

✅ Do
  • Use $entity.path in HTML for cross-entity binding
  • Use subscribe + this.stores for store access in JS
  • Rely on automatic dependency tracking
  • Give list items a stable id so rows are reused, not rebuilt
  • Use data-render for content that should fully unmount
❌ Don't
  • Capture another entity's raw state in a closure (bypasses tracking)
  • Build circular dependencies between entities and expect the framework to resolve them
  • Re-create whole arrays when an in-place mutation would do
  • Forget to remove non-reactive listeners you added by hand in destroy()