WildflowerJS for AI Code Assistants

A condensed reference designed specifically for AI assistants helping developers write WildflowerJS code.

For AI Assistants: This page contains patterns, syntax, and anti-patterns in a format optimized for code generation. Scan this entire document before generating WildflowerJS code.
For complete reference, fetch https://www.wildflowerjs.com/llms.txt. This page is a quick-start subset. WildflowerJS supports the llms.txt convention; the full machine-readable reference lives at the absolute URL above. If you are reading this page in isolation (downloaded copy, no base URL context), the absolute URL is the one to fetch.

Framework Overview

  • No build step required - works directly in browser via script tag
  • Standard HTML/JS only - the browser parses what you write, with nothing added to the language
  • Attribute-based binding - uses HTML data attributes
  • Proxy-based reactivity - automatic DOM updates on state change
  • Full bundle - includes routing, SSR, and stores
  • Multiple build variants - nano, mini-pool, mini, lite, core, spa, and full (see Choosing a Build below)

Choosing a Build

WildflowerJS ships seven builds with one API, so code written against a smaller build runs unchanged on any larger one. Every build includes components, stores (with localStorage persistence), computed properties, form validation, templates, error boundaries, and CSP-safe mode. Pick the smallest build whose features you need; when unsure, use full, which is always correct.

BuildFileAdds over the previous tierSize (brotli)
nanowildflower.nano.min.jsthe baseline above, without data-list or pools51 KB
mini-poolwildflower.mini-pool.min.jspools (per-frame entities), still no data-list55 KB
miniwildflower.mini.min.jsdata-list rendering (in place of pools)74 KB
litewildflower.lite.min.jslists and pools together78 KB
corewildflower.min.jsplugins, portals, transitions82 KB
spawildflower.spa.min.jsrouting86 KB
fullwildflower.full.min.jsdata-query, SSR99 KB

CDN: https://cdn.jsdelivr.net/npm/wildflowerjs@1/dist/wildflower.<build>.min.js (for core the file is just wildflower.min.js). During development substitute .dev.js for .min.js to get diagnostic warnings with error codes; switch to .min.js for production.

Developing with the Dev Builds

Load the .dev.js build while building and testing. Dev builds diagnose mistakes in the console as they happen: each warning states the actual cause, often includes the corrected markup, and carries a stable WF-xxx error code linking to https://www.wildflowerjs.com/docs/error-codes?code=WF-xxx. If a page misbehaves (an empty list, a binding that never updates, an action that does not fire), read the console first, since the framework usually names the mistake and the fix.

Production builds strip console.log, console.info, and console.debug entirely. Debug output that must survive in a production build needs console.warn or console.error. Ship .min.js once the console is clean in dev.

Reactivity Rules

Nested mutations ARE reactive at any depth. If a binding seems to need an object spread or replacement to update, the binding is the bug, not the mutation.

WildflowerJS reactivity is proxy-based. The proxy SET trap fires on every assignment to state at any nesting depth, and the corresponding effect re-runs. There is no Vue 2-style Vue.set requirement, immutability requirement, or object-identity rule.

✅ Triggers reactivity
  • this.count = 5
  • this.user.name = 'Ana'
  • this.shares.m5 = { ... }
  • this.shares['m5'] = { ... }
  • delete this.shares.m5
  • this.items[3].priority = 'high'
  • this.items.push(item)
  • this.items.splice(1, 1)
  • this.items[0] = newItem
  • this.shares = { ...this.shares, m5: ... } (also fine, just unnecessary)
❌ Won't trigger
  • Mutating a copy: const s = this.shares; s.m5 = ... on a copy that's not actually a reference into state
  • Replacing the entire this.state object reference (don't do this)
  • Mutating after the component has been destroyed

If a binding doesn't update on a direct mutation that should work, treat it as a binding bug, not a mutation issue. File it.

Source proof: src/state/reactive-graph/core.js, the set and deleteProperty traps of the reactive proxy. Tests: tests/direct-mutation.test.js exercises nested object property mutation, array index assignment, nested-object-in-array property mutation, and array methods. The proxy traps fire and the dependency graph marks the affected bindings dirty in every case.

The this.X Shortcut (Idiomatic)

Inside component methods and computeds, this.shares resolves to this.state.shares automatically. And this.savedCount resolves to a computed of the same name. The this.state / this.computed longhand also works, but the shortcut is the idiomatic form. Resolution order: own property → computed → state.

wildflower.component('cart', {
    state: { items: [], discount: 0 },
    computed: {
        // Use this.items directly, not this.state.items
        total() { return this.items.reduce((s, i) => s + i.price, 0); },
        // Use this.total directly, not this.computed.total
        finalPrice() { return this.total * (1 - this.discount); }
    },
    addItem(item) {
        // Direct nested mutation: triggers reactivity
        this.items.push(item);
    }
});

Source: src/state/ContextProxy.js: "Makes this.count in methods resolve identically to data-bind="count" in templates." This applies inside the component definition's own methods/computeds, item-level computeds in lists, watcher callbacks, store methods, and lifecycle hooks. The longer this.state.X form remains valid and is sometimes useful for disambiguation when a state property name collides with a method name.

Pool entities follow a different set of rules. The reactivity rules and the this.X shortcut described above apply to component / store / item-level-computed scope. Pool entities are plain objects with property descriptors, not proxies. There is no this.state, this.stores, or automatic dep tracking on entity reads. Plain descriptors keep per-frame updates cheap for high-frequency rendering (games, dashboards, simulations). For reactive per-item state in a regular list, use a data-list with item-level computeds (the section below). For pool-entity binding rules, see the data-pool section.
A computed depends on what it actually read, not on what it could have read. The tracker records reads that execute. A value skipped by a short-circuit (&&, ||, a ternary, an early return) is never read, so it is never tracked, and the binding will not re-evaluate when that value later changes. Nothing errors and nothing warns: the only symptom is an update that does not happen, usually intermittently, depending on which branch ran first. Read every field that could matter at the top of the computed, then branch.
// ❌ When `enabled` is false, `rules` is never read, so adding a rule
//    changes nothing on screen. Looks like broken reactivity; is not.
computed: {
    hidden(item) {
        const f = this.stores.filters;
        return f.enabled ? f.rules.some(r => r.match === item.kind) : false;
    }
}

// ✅ Read both, then decide.
computed: {
    hidden(item) {
        const f = this.stores.filters;
        const on = f.enabled;      // always read, always tracked
        const rules = f.rules;     // always read, always tracked
        return on ? rules.some(r => r.match === item.kind) : false;
    }
}

Applies to component-level and item-level computeds alike, and to reads of state, props, stores and queries. Same rule in every runtime-proxy reactive system (Vue, Solid, MobX, Preact Signals). The full treatment, with the underlying mechanism, is on the Computed Properties page under "Eager dependency reading for conditional computeds".

Always reach across entities through a tracking surface. Those are $entity.path in markup, and subscribe + this.stores, getStore() or getComponent() in JavaScript. Capturing another entity's state object in a closure or a field bypasses the tracking proxy and the dependency is silently lost: the value reads correctly once, and the update never fires afterwards. The framework cannot detect this and cannot warn about it.
// ❌ Captured once. Later changes to the cart never reach the binding.
init() { this._cart = wildflower.getStore('cart').state; },
computed: { total() { return this._cart.total; } }

// ✅ Read through the tracking surface on every evaluation.
computed: { total() { return wildflower.getStore('cart').total; } }

Quick Reference: Reactivity

OperationReactive?Idiomatic form
Scalar reassignmentthis.count = 5
Nested object propertythis.user.name = 'Ana'
Object map by keythis.shares.m5 = obj or this.shares['m5'] = obj
Object map key deletedelete this.shares.m5
Array push / pop / splicethis.items.push(x)
Array index assignmentthis.items[0] = x
Property of array itemthis.items[3].priority = 'high'
Whole-object replacement✅ (also fine, just unnecessary)this.shares = { ...this.shares, m5: x }
Replacing this.state reference❌ (don't)(no, mutate properties, don't reassign state)
Mutating after destroy(framework has cleaned up; mutations no-op)

Crossing serialization boundaries (wildflower.toRaw)

Reactive state is a Proxy. It JSON-serializes and iterates like a plain object, but APIs that use the browser's structured-clone algorithm throw DataCloneError on it. Use wildflower.toRaw(value) to get a deep plain-JS snapshot.

Boundaries that need the unwrap:

  • indexedDB reads and writes
  • postMessage (worker, iframe, window)
  • BroadcastChannel.postMessage
  • Cache.put / Cache.add
  • history.pushState / replaceState state objects
// IndexedDB
await store.put(wildflower.toRaw(this.items));

// postMessage to a worker
worker.postMessage({ payload: wildflower.toRaw(this.config) });

// History state (router = your wildflower.createRouter(...) instance)
router.navigate('/results', {
    state: wildflower.toRaw(this.filters)
});

fetch with a JSON body works directly on a proxy; toRaw is only needed for structured-clone-using APIs. The snapshot is a one-time copy: re-call when a fresh copy is needed.

Optimal Page Structure

When generating HTML pages, always place scripts in <head> with defer for best Lighthouse scores:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My App</title>
    <link rel="stylesheet" href="styles.css">

    <!-- All scripts in head with defer -->
    <script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
    <script defer src="app.js"></script>
</head>
<body>
    <div data-component="my-app">
        <!-- App content -->
    </div>
</body>
</html>
Critical: Always use defer on external scripts. This enables parallel downloading during HTML parsing and executes scripts in order after DOM is ready. This approach improves Lighthouse scores by ~15 points compared to scripts at end of body.

Why defer in <head> is best:

  • Scripts download in parallel with HTML parsing (not sequentially)
  • Execution order is preserved (framework loads before app.js)
  • No DOMContentLoaded wrapper needed for external scripts
  • Results in ~50% faster First Contentful Paint and ~35% lower Total Blocking Time
The loading rule. For best performance, put the framework and your app in external files, both in <head> with defer, and do not inline your component definitions. If the app code must be inline, load the framework tag without defer, placed before the inline block. Browsers ignore defer on inline scripts, so a deferred framework tag followed by inline registrations runs the registrations first and the page dies with wildflower is not defined. Keeping defer and wrapping every inline registration in DOMContentLoaded also works; it is easier to get wrong.

Single self-contained file (inline app code): no defer on the framework tag.

<!-- No defer: the inline registrations below need `wildflower` to exist already -->
<script src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script>
wildflower.component('my-app', {
    state: { count: 0 }
});
</script>

Preventing Flash of Unstyled Content

When using defer scripts, add this CSS to prevent modals from briefly flashing:

/* Hide cloaked elements until framework processes them */
[data-cloak] { display: none !important; }

Add the data-cloak attribute to elements that should be hidden until the framework initializes (e.g., data-show elements starting false, data-portal elements). The framework removes data-cloak after initialization. The !important is load-bearing. [data-cloak] has the same specificity as a single class, so any component rule that sets display would win the tie and the element would flash until the framework hides it.

Important: Do not use data-cloak inside <template> elements (i.e., inside data-list templates). Template content is inert and invisible until the framework clones it, so there is no FOUC risk. Using data-cloak inside templates causes bugs with dynamically added list items.

Inline Scripts with Deferred Dependencies

If inline scripts depend on deferred libraries, wrap them in DOMContentLoaded:

document.addEventListener('DOMContentLoaded', function() {
    // Deferred scripts are now loaded
    externalLibrary.init();
});

Inlined Pages, Blob URLs, Single-File Distributions

When an entire HTML page (framework + app code) is inlined into a single file or fetched as a Blob URL, browsers can re-order parsing such that the defer ordering guarantee doesn't hold. Symptoms: wildflower is not defined, components never mount, "Component not registered" warnings.

Fix: wrap your wildflower.component(...) registrations in DOMContentLoaded:

document.addEventListener('DOMContentLoaded', () => {
    wildflower.component('my-component', { /* ... */ });
    // wildflower.scan() runs automatically after DOMContentLoaded
});

Normal page loads with separate <script defer> tags don't need this. The wrapper only applies to bundled / inlined / blob-URL distributions.

Strict CSP Environments

WildflowerJS runs under a strict Content Security Policy that forbids both unsafe-eval and unsafe-inline, and it does so with no build step: templates are real <template> elements and expressions fall back to a built-in AST parser. When generating pages for a CSP-constrained target (enterprise, extensions, government), follow three rules:

<!-- 1. Add data-csp-safe so the framework never attempts eval
        (zero CSP violations and zero report-uri noise) -->
<script src="wildflower.min.js" data-csp-safe defer></script>

<!-- 2. App code in a same-origin file, never inline
        (script-src 'self' blocks inline scripts) -->
<script src="app.js" defer></script>

3. Keep binding expressions CSP-parser-safe: the AST parser handles arithmetic, comparisons, logical operators, ternaries, and property access, but not method calls (str.toUpperCase()), template literals, or globals (Math, Date). Put that logic in computed properties instead. See Expressions for the full support table.

Core Attributes Reference

Attribute Purpose Example
data-component="name" Declares a component instance <div data-component="counter">
data-bind="property" One-way text binding (textContent) <span data-bind="count"></span>
data-bind="computedName" Bind to computed property (no prefix needed) <span data-bind="fullName"></span>
data-bind-html="property" Bind as innerHTML (XSS risk with untrusted input, use setHtmlSanitizer()) <div data-bind-html="htmlContent"></div>
data-bind-class="expression" Dynamic CSS classes (object or expression) <div data-bind-class="{ active: isActive }"> or <div data-bind-class="isActive ? 'active' : ''">
data-bind-style="expression" Dynamic inline styles via object <div data-bind-style="{ color: textColor }">
data-bind-attr="expression" Bind HTML attributes dynamically <img data-bind-attr="{ src: imagePath, alt: description }" />
data-model="property" Two-way binding for inputs <input data-model="username">
data-model-number Convert value to number <input data-model="price" data-model-number>
data-model-trim Trim whitespace from value <input data-model="name" data-model-trim>
data-model-lazy Update only on blur/change <input data-model="email" data-model-lazy>
data-action="method" Click handler (default) <button data-action="save">Save</button>
data-action="event:method" Specific event handler <input data-action="input:search">
data-list="arrayProperty" Render array items <ul data-list="items">
data-pool="poolName" Lightweight collection renderer (plain objects, no proxy overhead) <div data-pool="enemies" data-key="id">
data-show="property" Show/hide via CSS (display: none) <div data-show="isVisible">
data-render="property" Insert/remove from DOM entirely <div data-render="showPanel">
data-portal="selector" Teleport content to another element <div data-portal="body">Modal content</div>
data-transition="name" CSS transitions on show/hide <div data-show="visible" data-transition="fade">
data-external Preserve element during HTML updates <div data-external data-component="live-chart">
data-prop-*="value" Pass props to child component <div data-component="child" data-prop-user="currentUser">
data-props="{ ... }" Pass multiple props as object <div data-component="child" data-props="{ title: heading, color: theme }">
data-use-template="name" Reference parent's named template <div data-use-template="cardTemplate" data-with="user">
data-item-template="name" Define a named template for children <template data-item-template="cardTemplate">
data-template-key="property" Select template variant by data value <div data-component="viewer" data-template-key="viewType">
data-type="value" Template variant identifier (for data-template-key) <template data-type="card">...</template>
data-query="name" Bind element to a declared external data source (Full build). Template child = list; bare subtree = record <ul data-query="products"><template>...</template></ul>
data-seed='{"json":1}' SSR: JSON attribute carrying fields the page does not display (row ids, machine values) into hydrated state <tr data-seed='{"id":811}'>

Note: All data-* attributes also support a data-wf-* prefix (e.g., data-wf-bind, data-wf-action). When both forms are present on the same element the data-wf-* form wins. In practice the major peer libraries don't collide, since Bootstrap uses data-bs-*, HTMX uses hx-*, and Alpine uses x-*, so the unprefixed form is fine for almost every project. Reach for data-wf-* only when an external library or a bespoke integration legitimately uses the bare names (data-bind, data-show, data-action, etc.) for its own purposes. To enforce data-wf-* exclusively, set data-wf-prefix="true" on the framework script tag (toggles the useWfPrefixOnly config in www/js/src/core/Bootstrap.js).

data-show vs data-render

Both conditionally display content, but they work differently:

Attribute Behavior When to Use
data-show Toggles display: none. Element remains in DOM. Frequent toggling, preserve form state, simple show/hide
data-render Inserts/removes from DOM entirely. Destroys nested components. Complex conditional content, memory efficiency, expensive widgets
<!-- data-show: Toggle visibility, keep element in DOM -->
<div data-show="isLoggedIn">Welcome back!</div>

<!-- data-render: Insert/remove from DOM completely -->
<div data-render="showExpensiveWidget">
    <div data-component="heavy-chart">...</div>
</div>

Component Definition Pattern

wildflower.component('component-name', {
    // Reactive state object
    state: {
        count: 0,
        items: [],
        user: { name: '', email: '' }
    },

    // Computed properties (cached, auto-update on dependency change)
    computed: {
        doubleCount() {
            return this.count * 2;
        },
        itemCount() {
            return this.items.length;
        }
    },

    // Lifecycle: called after component mounts
    init() {
        console.log('Component initialized');
    },

    // Lifecycle: called before component destroys
    destroy() {
        // Cleanup code here
    },

    // Action methods (called from data-action)
    increment() {
        this.count++;
    },

    // Actions receive (event, element, context)
    handleClick(event, element) {
        event.preventDefault();
        // element is the clicked DOM element
    },

    // List item actions receive item data in details parameter
    removeItem(event, element, details) {
        const index = details.index;
        this.items.splice(index, 1);
    }
});

Lifecycle Constraints

Reserved method names. Do NOT use these names for action handlers or helpers; the framework drives them on its own schedule:

  • init, beforeInit: called once during mount
  • destroy, beforeDestroy: called once during teardown
  • onUpdate, beforeUpdate: called on every reactive update
  • onError: called when a method throws
  • tick: called every animation frame for any component that defines it, pools or not. It is also the only loop to write: a simulation that runs until it settles, a clock, a queue drainer all belong in tick(dt) gated by a state flag, never in a hand-rolled requestAnimationFrame or setInterval loop

Most common AI-generation trap: naming an action tick on a non-animation component. The framework will call it every frame instead of on click. Pick a specific verb (increment, handleClick, refresh).

Actions before init are queued, not dropped. If a user click fires before init() completes, the call is held and replayed in order after init returns. Generated handlers can assume init()-set state is present when they execute, even if the click happened during the brief mount/init window or while a subscribed store was loading.

Stale event arg in replayed actions. Replayed actions see the original DOM event, but event.preventDefault() is a no-op by replay time. For forms that must reliably block submission, use data-event-prevent on the form element (framework intercepts before user code) rather than calling preventDefault() in the handler.

List Rendering Pattern

Use HTML5 <template> element inside data-list container:

<ul data-list="items">
    <template>
        <li>
            <span data-bind="name"></span>
            <span data-bind="price"></span>
            <button data-action="removeItem">Remove</button>
        </li>
    </template>
</ul>

With corresponding state:

state: {
    items: [
        { name: 'Apple', price: 1.50 },
        { name: 'Banana', price: 0.75 }
    ]
}

Accessing list index: The framework provides index and item data via the details parameter:

removeItem(event, element, details) {
    const index = details.index;  // Current item's index
    const item = details.item;    // Current item's data
    this.items.splice(index, 1);
}

Primitive arrays (strings, numbers): use data-bind="$item" to bind the item itself, since there's no property name to bind to:

<ul data-list="tags">
    <template><li data-bind="$item"></li></template>
</ul>

State: tags: ['html', 'css', 'javascript']. The $item reference is item-context only; for object arrays use property names directly. See the dedicated Primitive Lists section below for more.

Nested Lists Pattern

<div data-list="categories">
    <template>
        <div class="category">
            <h3 data-bind="name"></h3>
            <ul data-list="items">
                <template>
                    <li data-bind="title"></li>
                </template>
            </ul>
        </div>
    </template>
</div>

Data Pool Rendering (data-pool)

data-pool is an explicit-control rendering primitive for collections. Unlike data-list, pool items are plain JS objects with zero reactive proxy overhead. You mutate objects directly and call markDirty() to trigger updates. Pools handle full CRUD, selection, and bulk operations faster than the push-reactive path. Use for performance-sensitive workloads, real-time data (dashboards, tickers), large datasets (1K+ items), and per-frame animation (games, particles, visualizations). Use data-list when items need two-way binding (data-model), parent computed properties, or nested lists.

Pick one pool mode and stay in it. A pool that never calls markDirty() is an animation pool: every frame repaints every entity, which is what tick(dt) workloads want. The first markDirty() call converts the pool to a data pool for the rest of its life, and from then on only marked entities repaint. Do not mix them. Adding a markDirty() call to a tick()-driven pool silently freezes every entity you did not mark.
<div data-pool="enemies" data-key="id">
    <template>
        <div data-bind-style="{ left: x + 'px', top: y + 'px' }">
            <img data-bind-attr="{ src: imgSrc }">
            <span data-bind="label"></span>
        </div>
    </template>
</div>
// Declare pools alongside state (preferred):
wildflower.component('game', {
    pools: {
        enemies: {
            onAdd: 'onSpawn',         // lifecycle hooks (string ref or inline fn)
            onRemove: 'onDeath',      // fires before individual removal
            onClear: 'onWaveEnd'      // bulk clear (skips onRemove)
        },
        projectiles: {}               // no hooks needed
    }
})

// Pool API (via this.pools.name):
const pool = this.pools.enemies;

// Array-like API (preferred: reads like native JS arrays):
pool.push({ id: 1, x: 100, y: 200, imgSrc: 'orc.png', label: 'Orc' });
pool.push(arrayOfItems);             // bulk add via DocumentFragment (single DOM op)
pool.length;                          // current count
for (const e of pool) { /* ... */ }   // iterate via Symbol.iterator
pool.filter(e => e.alive);            // returns plain array
pool.map(e => e.id);
pool.find(e => e.id === 42);
pool.forEach(fn); pool.some(fn); pool.every(fn); pool.reduce(fn, init);

// Key-based ops (pool uses swap-with-last, so no positional pop/splice):
pool.remove(1);                       // remove by key
pool.get(1);                          // get entity by key (or undefined)
pool.update(1, { x: 200 });           // patch properties (sync for static pools)
pool.clear();                         // remove all
pool.getElement(1);                   // get DOM element by key
pool.swap(key1, key2);                // swap two items' DOM positions
pool.markDirty(key);                  // re-evaluate bindings for one entity (see mode note)
pool.props;                           // shared props object (parent-injected data)

// Long-form aliases (identical semantics): pool.add(obj), pool.size, pool.items
// DOM updates happen automatically on next rAF
// Imperative form also works: this.getPool('enemies')

Entity Shape: state defaults, computed properties, methods

Declare entity: { state, computed, ... } on a pool to give every entity a shared shape. Entity computed properties derive transform/background/class strings from raw inputs (x, y, z), and data-bind-* reads them on flush. In tick(dt) you only mutate the inputs. There is no markDirty call and no manual string assembly.

Check the pool size before reaching for entity.computed. Entity computeds are uncached by contract: each one re-evaluates on every read of every flush, measured at roughly 60µs per entity per flush. At 200 entities that is about 12ms of a 16.7ms frame, before the simulation does any work of its own, so development builds warn at that size (WF-411). Use entity computeds for pools in the tens. For a per-frame pool in the hundreds or larger, write the derived value as a plain data field in tick(dt) instead, or mark non-animating entities with data-pool-static. A boids demo lost about 3ms per frame at 800 entities by moving to entity.computed and the change was reverted.
pools: {
    particles: {
        entity: {
            state: { hp: 100, vx: 0, vy: 0 },      // defaults merged into new entities (spawn values win)
            computed: {
                tf() { return `translate(${this.x}px,${this.y}px)`; },
                bg() { return COLOR_LUT[Math.round(this.z * 4) & 255]; }
            },
            kill() { this.hp = 0; }                 // entity methods, callable as entity.kill()
        }
    }
}

// Template: <div data-bind-style="{ transform: tf, background: bg }"></div>

tick(dt) {
    for (const p of this.pools.particles) {
        p.x += p.vx * dt;
        p.y += p.vy * dt;
    }
    // tf and bg auto-recompute from the mutated inputs, no markDirty needed.
}

Entity state is a shallow merge. Spawn values override template keys. Entity computed accessors are installed per-entity and skipped when the spawn already has an own property of the same name. Any non-reserved function at the top of the entity block becomes a method callable as entity.methodName().

Static Pools, Bulk Add, and Pool Props

// Static pool: add data-pool-static (boolean) to skip rAF flush loop.
// Items render synchronously on add()/update(). Zero idle CPU cost.
// HTML: <div data-pool="users" data-key="id" data-pool-static>

// Bulk add: pass an array, single DOM operation via DocumentFragment
this.pools.users.add(data); // data is an array of objects

// Pool props: shared data available to all items via `props.` prefix
pools: {
    boids: {
        props: { shape: 'arrow', theme: 'dark' }
    }
}
// Template: data-bind-class="className + ' ' + props.shape"
// Update: this.pools.boids.props.shape = 'circle'; (all items update on next flush)

tick(dt) Lifecycle Hook

Components with a tick method get called once per animation frame. The framework manages the rAF loop automatically. No manual setup or teardown. tick runs BEFORE pool flush, so entity mutations are visible in the DOM on the same frame.

wildflower.component('game', {
    init() { this._pool = this.pools.enemies; },

    // Called automatically each frame. dt = ms since last frame (clamped to 250ms).
    tick(dt) {
        for (const e of this._pool.items) {
            e.x += e.vx * dt;
            e.y += e.vy * dt;
        }
    }
    // No destroy() needed: framework cleans up the rAF loop
});

tick(dt) also receives now (performance.now()) as a second argument. Works with or without pools, useful for canvas, Three.js, or any per-frame logic.

Run / stop loops use tick too. A model that runs until it settles, a clock, a queue drainer: define tick(dt) on the component and gate it with a state flag. The framework's loop is always running; the flag is the switch. Buttons toggle the flag and a stop condition clears it. For a slower cadence than one step per frame, accumulate dt in a plain _-prefixed field and step when it crosses your interval.

wildflower.component('schelling', {
    state: { cells: [], round: 0, running: false },
    step() {
        // Work on a plain copy, write the reactive array ONCE at the end.
        // In-place writes (this.cells[i] = ...) inside the loop would trigger a
        // re-render per write and read every neighbour through the reactive facade.
        const cells = this.cells.slice();
        /* ...compute one round on `cells`... */
        this.cells = cells;
        this.round++;
    },
    toggleRun() { this.running = !this.running; },
    tick(dt) {
        if (!this.running) return;
        this._acc = (this._acc || 0) + dt;   // optional pacing; non-reactive field
        if (this._acc < 100) return;         // one round every 100 ms
        this._acc = 0;
        this.step();
        if (this.allHappy) this.running = false;   // stop condition
    }
});

Hot loops over a large state array (dev warning WF-216). A step that reads a big reactive array thousands of times, every cell checking its neighbours, should read from a plain local copy and write the result back once: const cells = this.cells.slice(); ...compute next...; this.cells = next;. Reads through this.cells[i] inside the hot loop go through the reactive facade on every access; one write at the end is all the DOM needs.

tick() is guaranteed. Once the component initializes, tick(dt) fires every animation frame, so do not add a requestAnimationFrame loop as a fallback or watchdog beside it. A hand-rolled rAF loop next to tick() double-steps the simulation, and no fallback condition exists in which tick() silently fails to run.

Pool Item Events

Pool items use data-action, the same attribute as everywhere else. There is no data-pool-action. Inside the template, the handler receives the entity first and the DOM event second.

<div data-pool="tasks" data-key="id">
    <template>
        <div class="task">
            <span data-bind="title"></span>
            <button class="edit-btn" data-action="onEdit">Edit</button>
            <button class="delete-btn" data-action="onDelete">Delete</button>
        </div>
    </template>
</div>
// Event name in front of the method selects the event; the default is click
onEdit(item, event) { /* the Edit button of this entity */ },
onDelete(item, event) { this.pools.tasks.remove(item.id); }

For one listener over the whole collection, put data-action on the pool container and call this.getItemFromEvent(event) to resolve the entity. It returns { item, element }, or null when the event landed on the container itself. event.target.closest('.edit-btn') tells the handler which sub-element was hit.

pool.onChange Callback

Fires synchronously on add(), remove(), and clear(). Pool size is already updated when the callback fires.

const pool = this.pools.enemies;
pool.onChange = (p) => {
    document.getElementById('count').textContent = p.size;
};

data-pool vs data-list

data-list data-pool
Items Reactive proxy objects Plain JS objects
Updates Automatic on state mutation Batched via rAF loop
Template scope Full component context (state, computed, stores) Entity properties only
Use case Interactive collections (forms, inline editing, two-way binding) Performance-sensitive CRUD, real-time data, large datasets, per-frame animation
Overhead Proxy per item Zero proxy overhead
Supported bindings in pool templates: data-bind, data-bind-style, data-bind-attr, data-bind-class, data-show. Resolve against entity properties and props.* (shared pool props).
Key constraint: Pool templates can reference entity properties and props.* (parent-injected shared data), but NOT component state, computed properties, or store values. data-key defaults to id. Pools are automatically cleaned up on component destroy.

data-list vs data-pool Template Scope

The two list systems expose different scopes inside their templates. Pools narrow to entity + shared props for performance; lists give you the full component scope.

Available inside <template>data-listdata-poolNotes
Item / entity propertiesBoth: data-bind="name" resolves to item.name / entity.name
Component statePools intentionally exclude: avoids per-entity reactive closure
Zero-arg component computedsSame reason
Item-level computeds (fn(item))✅ (as entity.computed)Lists: parametrized component computeds. Pools: per-entity computeds declared in the entity: block
List context vars (_index, _first, _last, _length)N/APools are entity-centric, not position-centric
Parent-injected props (props.X)Both honor the props. prefix for shared parent data
Stores ($store.path)Pools decouple from stores by design
Action methods (data-action)Component methodsEntity methods (preferred), then component fallbackPool entities can declare their own methods that win over component methods of the same name

Source: src/rendering/PoolRenderer.js (entity-only ctx, no component-state merge); src/rendering/ListRenderer.js + ListItemBinding.js + src/core/BindingResolver.js (full scope). Tests: tests/pool-entity-computed.test.js, tests/pool-props.test.js, tests/list-item-context.test.js.

data-bind-class Syntax

The data-bind-class attribute supports object syntax (preferred for toggling) and expression syntax (for building class strings):

<!-- Object syntax: toggle classes by boolean (preferred) -->
<div data-bind-class="{ active: isActive }">
<div data-bind-class="{ error: hasError, warning: hasWarning }">
<div data-bind-class="{ selected: id === selectedId }">

<!-- Expression syntax: returns a class name string -->
<div data-bind-class="isActive ? 'active' : 'inactive'">
<div data-bind-class="isError ? 'alert alert-danger' : 'alert alert-success'">

<!-- Both work in list items -->
<ul data-list="items">
    <template>
        <li data-bind-class="{ done: completed }">
            <span data-bind="text"></span>
        </li>
    </template>
</ul>

Cross-Component Communication

Use the $ accessor in templates to read another component's state or computed properties:

<!-- Read another component's state directly in HTML -->
<span data-bind="$theme-manager.mode"></span>
<div data-show="$nav-manager.menuOpen">Navigation</div>
<span data-bind="$user-profile.fullName"></span>
// In computed properties: automatic dependency tracking
wildflower.component('observer', {
    computed: {
        themeMode() {
            const theme = wildflower.getComponent('theme-manager');
            return theme ? theme.mode : 'light';
        }
    }
});
Automatic Dependency Tracking: $component.path in templates and getComponent() in computed properties both have automatic reactivity. The component re-renders when the source entity's state changes.

Cross-Component Method Calls

wildflower.getComponent('name') returns the component context (or null if the component hasn't mounted yet). Calling methods on the returned object is supported and idiomatic, useful for sibling components that need to drive each other:

wildflower.component('demo-host', {
    runDemo() {
        // Returns null if 'main-panel' isn't mounted yet, so guard accordingly
        const main = wildflower.getComponent('main-panel');
        if (main) main.startDemo();
    }
});

When to use which pattern:

  • Shared mutable state across multiple components → use a store. $store.path in templates, this.stores.X in methods. Reactive everywhere; multiple readers get automatic dependency tracking.
  • Reading another component's state in a template → use the $component.path shorthand (e.g., data-bind="$user-profile.fullName"). Reactive; tracked automatically.
  • Reading another component's state in a computed → use wildflower.getComponent(name) inside the computed. Tracked automatically (the framework wraps the call in a ContextProxy so reads register as deps).
  • Telling another component to do something (one-shot method call, no shared state) → wildflower.getComponent(name).method(). Don't use it to push state. That's what stores are for.

Source: src/features/ErrorBoundaries.js. getComponent(name) returns a ContextProxy when called inside a computed (for dependency tracking), or the raw context when called from anywhere else; returns null if no instance with that name exists.

Component Mount / Destroy Events

External scripts (analytics, devtools, page-level orchestration, tests) can listen for framework lifecycle events on document. Use wildflower:ready as the canonical "framework is up" signal. It fires once per page after all initial scanning and mounting completes.

EventWhen it firesDetail
wildflower:ready Framework initialization complete (all initial components scanned and mounted). Fires once per page. { instance }
wildflower:componentInit Each component instance after its init() runs. { instance, context }
wildflower:componentDestroy Component instance about to be torn down. { instance, context }
wildflower:store-ready Each store after creation; lets components delay setup until a store exists. { storeName }
// External script: wait for framework, then probe state
document.addEventListener('wildflower:ready', () => {
    console.log('WildflowerJS ready');
});

// Per-component readiness
document.addEventListener('wildflower:componentInit', (e) => {
    if (e.detail.instance.name === 'main-panel') {
        // run analytics, attach external observers, etc.
    }
});

For deterministic sequencing in tests, tooling, or page-load orchestration, use wildflower.whenSettled(): a promise that resolves once the framework's scheduled work has drained (the microtask effect flush, the deferred init window, the rAF render, and the final effect microtask), so all pending updates are applied, including components whose init injected further components. It is a boundary tool for code that observes the page from outside (tests, tooling, interop with non-reactive libraries); idiomatic components never need it, because bindings keep the DOM current and onUpdate covers the changed-now-act case. The pool rAF loop is excluded by contract: pools animate plain objects outside the reactive graph, so a running pool animation neither blocks settling nor counts as pending work.

Source: src/core/FrameworkInit.js (wildflower:ready), src/components/ComponentScanning.js + ComponentLifecycle.js (componentInit), src/features/ErrorBoundaries.js (componentDestroy), src/state/StoreManager.js (store-ready), src/core/WildflowerCore.js (whenSettled).

Store Pattern (Global State)

// Create a store
wildflower.store('user', {
    state: { name: 'Guest', isLoggedIn: false },
    computed: {
        greeting() { return 'Hello, ' + this.name; }
    },
    login(name) {
        this.name = name;
        this.isLoggedIn = true;
    }
});

// ✅ BEST: Use $ directly in HTML, no computed wrappers needed
// <span data-bind="$user.name"></span>
// <span data-bind="$user.greeting"></span>

// ✅ ALSO GOOD: Declarative subscription with this.stores
// (use when you need store access in JS methods)
wildflower.component('header', {
    subscribe: {
        user: ['name', 'isLoggedIn']
    },

    // Use this.stores in methods
    refreshUser() {
        this.stores.user.refreshProfile();
    }
});

// With onStoreUpdate() for side effects
wildflower.component('login-form', {
    state: { username: '' },

    subscribe: {
        user: ['isLoggedIn']
    },

    onStoreUpdate(storeName, path, newValue, oldValue) {
        if (storeName === 'user' && path === 'isLoggedIn' && newValue) {
            this.showWelcomeMessage();
        }
    },

    handleLogin() {
        this.stores.user.login(this.username);
    }
});
Store Auto-Injection: When you add a subscribe: {} block, this.stores is automatically available with references to all subscribed stores. Use it in computed properties, methods, and lifecycle hooks.
Without subscribe: If a component doesn't have a subscribe block, this.stores is NOT available. Use wildflower.getStore('name') instead. It works in computed properties with automatic dependency tracking (no subscribe needed for reactivity).

Store Readiness API

Components that depend on stores can use the subscribe declaration to automatically wait for stores before init() runs. The subscribe block also enables this.stores auto-injection:

// ✅ RECOMMENDED: Declarative subscribe with paths
wildflower.component('app-init', {
    subscribe: {
        config: ['settings', 'features']  // Wait for config store + subscribe to paths
    },

    init() {
        // GUARANTEED: config store is ready, this.stores.config available
        // Note: for HTML binding, prefer $config.settings directly
        this.settings = this.stores.config.settings;
    }
});

// Multiple stores with this.stores
wildflower.component('app-shell', {
    subscribe: {
        theme: ['mode'],
        user: ['profile', 'preferences']
    },

    init() {
        // Both stores guaranteed ready, both available via this.stores
        this.theme = this.stores.theme.mode;
        this.userName = this.stores.user.profile?.name;
    },

    // React to store changes
    onStoreUpdate(storeName, path, newValue) {
        if (storeName === 'theme' && path === 'mode') {
            this.applyTheme(newValue);
        }
    }
});

// With timeout configuration
wildflower.component('critical-component', {
    subscribe: {
        auth: ['user', 'token']
    },
    subscribeTimeout: 3000,  // Max 3 seconds (default: 5000)

    onError(error) {
        if (error.type === 'subscribe_timeout') {
            this.showOfflineMode();
        }
    }
});

Alternative APIs (for programmatic control):

// Check if store is ready
const store = wildflower.getStore('config');
if (store.isReady()) {
    // Store is initialized
}

// Manual async waiting (for special cases)
async init() {
    const configStore = wildflower.getStore('config');
    await configStore.waitForReady();
}

// Listen for store ready events
document.addEventListener('wildflower:store-ready', (e) => {
    console.log(`Store ${e.detail.storeName} is ready`);
});
When to use: Prefer the subscribe declaration for automatic waiting. Use waitForReady() only for special programmatic cases. Stores with async init() are ready after the Promise resolves.

Store-Backed List Patterns

Three patterns for rendering lists from store data (all work with full reactivity):

<!-- Option 1: $store shorthand (simplest for direct binding) -->
<div data-list="$cart.items" data-key="id">
    <template><div data-bind="name"></div></template>
</div>

<!-- Option 2: computed property with this.stores (for filtering/transformation) -->
<div data-list="items" data-key="id">
    <template><div data-bind="name"></div></template>
</div>
// Note: For simple pass-through, use $myStore.items in HTML (Option 1)
// Use computed only when you need filtering/transformation:

// With data transformation
wildflower.component('filtered-list', {
    state: { searchTerm: '' },

    subscribe: {
        myStore: ['items']
    },

    computed: {
        items() {
            const allItems = this.stores.myStore.items;
            if (!this.searchTerm) return allItems;
            return allItems.filter(item =>
                item.name.toLowerCase().includes(this.searchTerm.toLowerCase())
            );
        }
    }
});

// With onStoreUpdate for side effects
wildflower.component('synced-list', {
    state: { items: [], lastSync: null },

    subscribe: {
        myStore: ['items']
    },

    init() {
        this.items = [...this.stores.myStore.items];
    },

    onStoreUpdate(storeName, path, newValue) {
        if (storeName === 'myStore' && path === 'items') {
            this.items = [...newValue];
            this.lastSync = new Date();
        }
    }
});
Recommendation: Use subscribe + this.stores with computed properties for clean, reactive store-backed lists. Use $entityName.path in HTML for simple cases without transformation. Works for stores, components, and plugins.

Async Computed Properties

A computed may return a promise. The framework awaits it and pushes the resolved value into the bindings. There is no separate API and no await at the call site: the binding reads the value, not the promise. Components, stores, and plugins all behave the same way.

// ✅ CORRECT: return the promise. An async function works identically.
wildflower.component('user-panel', {
    state: { userId: 1 },
    computed: {
        user() {
            const id = this.userId;              // read reactive inputs FIRST
            return fetch('/api/users/' + id).then(r => r.json());
        }
    },
    onError(error, context) { /* rejections arrive here */ }
});

Read every reactive input before the first await or .then(). Dependency tracking is synchronous, so reads inside a callback or after an await are not tracked and the computed will not re-run when they change. Pull them into locals up front, then build the request from those locals.

wildflower.component('cart', {
    state: { items: [] },
    computed: {
        // ✅ CORRECT: read reactive values FIRST, then return the promise
        shippingQuote() {
            const items = this.items;   // synchronous read = tracked
            return fetch('/api/shipping-quote', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ items })
            }).then(r => r.json()).then(q => q.total);
        }
    }
});

// ❌ WRONG: reading state inside .then() or after await. Those reads happen
// after the run and are NOT tracked, so the computed never re-runs.
//     return fetch('/api/quote').then(() => this.items.length);

There is no loading flag, and you must not invent one. The absence of the value IS the signal. On the first evaluation a binding reads undefined, so gate the first-load state with data-show="!user". During a later refresh the binding keeps showing the previous value rather than blanking.

<p data-show="!user">Loading…</p>
<div data-show="user"><h5 data-bind="user.name"></h5></div>

Supersession is built in. If a dependency changes while a request is in flight, the computed re-runs and starts a new one. The last call wins, and an earlier response arriving late is discarded silently, so bindings never go backwards. Do not add cancellation or debounce for correctness; that machinery already exists. A rejected promise leaves the binding at undefined and sends the error to the entity's onError hook.

Item-level computeds stay synchronous. A computed taking the row as a parameter, like label(item), runs through the list renderer rather than the reactive graph, so it does no async tracking. A promise returned there binds as text, and a thousand rows issue a thousand uncoordinated requests. Dev builds warn with WF-235. Fetch the collection once in a component or store computed instead, then read from it per row.
computed: {
    // ❌ WRONG: one request per row, and dev builds warn (WF-235)
    avatar(item) { return fetch('/api/avatars/' + item.id).then(r => r.json()); },

    // ✅ CORRECT: one request for the set, rows read from the result
    avatars() { return fetch('/api/avatars').then(r => r.json()); }
}
Async computed or data-query? Use an async computed for a derived value the component owns. Use data-query (below) for external data the app displays and writes back: it adds refresh policies, first-load and error state, optimistic saves with rollback, and reload persistence, none of which an async computed provides.

Live External Data (data-query)

data-query binds an element to a named external data source and keeps it current. Use it for data that lives OUTSIDE components: a server endpoint, a feed. For server data a component will render, it is THE intended pattern, covering loading and error states, refresh on demand, saves with rollback, and consistent updates come free. Do not fetch in init() and hand-roll loading/saving state for server data; reach for a query first. Plain fetch() remains fine for one-shot calls that render nothing.

The [data-query] container must sit inside a component. Queries bind during component binding, so a container outside every data-component renders nothing (dev builds warn: WF-963). A page with no other component needs only an empty shell: wildflower.component('shell', {}) + <div data-component="shell">…</div> around the query markup.
// ✅ CORRECT: declare globally (like stores), render via markup
wildflower.query('products', {
    from: '/api/products.json',   // URL string, or a function returning data
    key: 'id',                    // row identity: refreshes patch rows in place
    refresh: ['focus', 'etag:60'],// when to re-fetch (seconds, tab focus, etc.)
    retry: 3                      // re-run a failed fetch (1 s, 2 s, 4 s); default 0
});

Retry is one number, and it defaults to zero. retry: N re-runs a failed fetch up to N times on a fixed doubling curve (1 s, 2 s, 4 s, capped at 30 s), with no policy object to configure. Without it a flaky endpoint shows error on the first failure, so declare it for any source that can answer 500 a few times before succeeding. While the ladder runs the query holds its state: rows stay, error stays unset, the loading or stale flag persists, and any success resets the count. Network failures and 5xx retry; most 4xx are final (401, 408 and 429 are the exceptions).

<!-- List shape: template child. The engine renders and patches rows. -->
<ul data-query="products">
    <template><li><span data-bind="name"></span> — <span data-bind="price"></span></li></template>
</ul>

<!-- Record shape: bare subtree, plain paths bind to the single result -->
<article data-query="currentUser">
    <h2 data-bind="name"></h2>
</article>

<!-- Query state binds via $name.* anywhere in the component -->
<p data-show="$products.isLoading">Loading…</p>
<p data-show="$products.error">Failed</p>
<span data-bind="$products.count"></span>

The refresh ladder: 'once' (default) · a number (poll seconds, SECONDS not ms) · 'etag:N' (conditional GET) · 'focus' (tab regains focus) · 'reconnect' · 'sse' (EventSource; JSON message = new data, empty message = re-fetch signal). Combine in an array. When the server pushes, the 'sse' rung IS the wiring, so never construct new EventSource(...) + listeners + state assignment yourself, and never pair a plain fetch() with your own stream: refresh: ['sse'] is that entire block.

'sse' needs the stream's URL. The rung opens an EventSource at stream: when declared, otherwise at from. A JSON endpoint is not an event stream, and the browser rejects it with a MIME-type error, so whenever the data URL and the stream URL differ, which is the usual case, declare both:

wildflower.query('tickets', {
    from: '/api/tickets.json',        // the data
    refresh: ['sse'],                 // stay live from the server's push
    stream: '/api/ticket-stream',     // the EventSource URL (omit only when `from` IS the stream)
    key: 'id'
});

isLoading is first-load-only. It is true only while the query has no usable data. Later refreshes never blank the list; they set isStale instead. Bind $name.isStale for a "syncing" hint; never build your own first-load flag.

// ✅ CORRECT (primary): declare `to` on the query, save with write().
// The change lands on screen immediately; if the server rejects, exactly
// the written fields roll back. Do NOT hand-roll optimistic updates
// (apply + refetch + undo-on-error) — write() IS that machinery.
wildflower.query('tasks', {
    from: '/api/tasks',
    key: 'id',
    deleted: 'removed',                 // tombstone field, used for deletes
    to: '/api/tasks/:id',               // update at PATCH, delete at DELETE
    body: (item) => item,               // REQUIRED, or no body is sent
    confirmation: (d) => d.task,        // omit to refetch instead
    create: { url: '/api/tasks', method: 'POST',
              body: (item) => item, confirmation: (d) => d.task },
    headers: () => ({ Authorization: 'Token ' + auth.token })
});

// A partial object: the key plus ONLY the fields that changed.
wildflower.getQuery('tasks').write({ id: 42, done: true });
// Adds: create() mints the temp key; the server's record supplies the real one.
wildflower.getQuery('tasks').create({ title: 'New task' });
// Deletes: write the declared tombstone field; the row leaves immediately.
// `deleted:` is REQUIRED for the row to go — including for a named delete
// in a `to` map. Without it the request still departs and the row stays on
// screen until the next sync (WF-975), so the button looks broken.
wildflower.getQuery('tasks').write({ id: 42, removed: true });
// Operations that are neither: name them in a `to` MAP and call by name.
//   to: { favorite: { url: '/api/tasks/:id/star', method: 'POST' } }
//   wildflower.getQuery('tasks').write('favorite', { id: 42, starred: true });
// `to` also still takes a FUNCTION, for transports the above cannot express.
// write() returns a promise: await to sequence, .catch on rejection
// (the rollback has already happened).
// $tasks.pendingWrites counts unsettled writes reactively — bind a
// saving indicator: 

Saving…

// Page unload CANCELS in-flight writes (browser behavior): await write() // before navigating, or use keepalive: true in your to() fetch for // writes that must survive unload (64KB body cap). // ✅ CORRECT (fallback, transport you own end to end): act, then invalidate wildflower.component('product-form', { state: { draft: {} }, async addProduct() { await fetch('/api/products', { method: 'POST', body: JSON.stringify(this.draft) }); wildflower.getQuery('products').invalidate(); // conditional re-sync } }); // ❌ WRONG: writing a query's store directly. The next sync overwrites it // (dev builds warn: WF-950). Query stores are engine-owned. wildflower.getQuery('products').rows = [...]; // do not do this // ❌ WRONG: invented API names. None of these exist: wildflower.createQuery(...); wildflower.useQuery(...); this.queries.products; wildflower.queryWrite(...); wildflower.mutate(...);

Several views of the same entity: each query keeps its own copy. Converge siblings once a write settles: await wildflower.getQuery('order-detail').write({ id: 42, status: 'shipped' }); await wildflower.invalidateQueries('orders', 'dashboard-counts');

Reload persistence: persist: true on the declaration keeps the last confirmed rows in the browser, so the next page load paints them instantly, then revalidates. Never hand-roll localStorage caching for query data. Only confirmed server truth is stored (optimistic values never touch disk); wildflower.clearPersisted() removes every snapshot (call at logout).

SSR Handoff (data-ssr + data-query)

Inside data-ssr="true", the server-rendered DOM is the seed: the query parses its starting rows back out of the markup it adopts, using the same data-bind fields (data-type coerces numbers/booleans). There is no state blob and no re-render; the first fetch is a stale refresh of content the user is already reading. Fields the page does not display (usually the row key, or a machine value the display dresses up, like $1,250 for 1250) are carried in a data-seed JSON attribute on the row, which wins the merge at adoption:

<div data-component="warehouse" data-ssr="true">
    <tbody data-query="inventory">
        <template><tr><td data-bind="sku"></td><td data-bind="stock" data-type="number"></td></tr></template>
        <tr data-seed='{"id":811}'><td data-bind="sku">AX-100</td><td data-bind="stock" data-type="number">45</td></tr>
    </tbody>
</div>

The component declares no data for the query, because the query owns its store (declare wildflower.query('inventory', ...) and the rows live in it, never in component state). A data-query element must sit inside a component to activate.

Plugin Access Pattern

Plugins use the same $ accessor as stores and components:

// Register a plugin with reactive state
wildflower.plugin({
    name: 'auth',
    state: {
        isLoggedIn: false,
        user: null
    },
    login(user) {
        this.isLoggedIn = true;
        this.user = user;
    },
    logout() {
        this.isLoggedIn = false;
        this.user = null;
    }
});
<!-- Access plugin state in templates via $ -->
<div data-show="$auth.isLoggedIn">Welcome, <span data-bind="$auth.user.name"></span></div>
<div data-show="!$auth.isLoggedIn">Please log in</div>
// In computed properties: automatic dependency tracking
wildflower.component('auth-guard', {
    computed: {
        isAuthenticated() {
            const auth = wildflower['$auth'];
            return auth ? auth.isLoggedIn : false;
        }
    }
});

// In methods (for mutations)
wildflower.component('login-form', {
    handleLogin() {
        wildflower['$auth'].login({ name: this.username });
    }
});
Automatic Dependency Tracking: $plugin.path in templates and wildflower['$pluginName'] in computed properties both have automatic reactivity.

Form Handling Pattern

<form data-action="submit:handleSubmit">
    <input type="text" data-model="form.name" placeholder="Name">
    <input type="email" data-model="form.email" placeholder="Email">

    <!-- Select binding -->
    <select data-model="form.country">
        <option value="us">United States</option>
        <option value="uk">United Kingdom</option>
    </select>

    <!-- Checkbox binding -->
    <input type="checkbox" data-model="form.subscribe">

    <!-- Radio buttons -->
    <input type="radio" name="priority" value="low" data-model="form.priority">
    <input type="radio" name="priority" value="high" data-model="form.priority">

    <button type="submit">Submit</button>
</form>
state: {
    form: {
        name: '',
        email: '',
        country: 'us',
        subscribe: false,
        priority: 'low'
    }
},

handleSubmit(event) {
    event.preventDefault();
    console.log('Form data:', this.form);
}

DOM Helpers (this.$el)

jQuery-like DOM manipulation scoped to the component. Events are auto-cleaned on destroy.

wildflower.component('interactive-widget', {
    state: { count: 0 },

    init() {
        // Select and chain operations
        this.$el('.message')
            .addClass('highlight')
            .css({ color: 'blue', fontWeight: 'bold' })
            .text('Updated!');

        // Get raw DOM element for third-party libraries
        const input = this.$el('.date-input').el;  // Returns Element or null
        flatpickr(input, { dateFormat: 'Y-m-d' });

        // Event binding (auto-cleanup when component is destroyed)
        this.$el('.btn').on('click', () => {
            this.count++;
        });

        // Iterate over multiple elements
        this.$el('.item').each((el, index) => {
            console.log(`Item ${index}:`, el.textContent);
        });

        // Form value with reactivity bridge (triggers data-model sync)
        this.$el('input').val('new value');
    }
});

Key Methods

CategoryMethods
Selectionthis.$el(selector), this.$el() (root), this.$el(element)
Element Access.el (first raw element), .get(i), .length, .each(fn)
Classes.addClass(), .removeClass(), .toggleClass(), .hasClass()
Styles.css(prop, val), .css({...}), .show(), .hide()
Content.text(), .html(), .val() (triggers data-model sync), .attr()
Events.on(event, fn) (auto-cleanup), .off(event, fn?), .trigger(event)
Traversal.find(), .parent(), .closest(), .children() (boundary-enforced)
Third-Party Library Pattern: Use .el to get the raw DOM element for library initialization:
const el = this.$el('.chart-container').el;
new Chart(el, { type: 'bar', data: chartData });
Component Boundary: Traversal methods (.parent(), .closest()) cannot escape the component element. This prevents accidental manipulation of parent components.
List Item Animations: For animating list items as they are added, removed, or filtered, use AutoAnimate, a zero-config FLIP library loaded via dynamic import(). Initialize it in init() on a data-list container, and disable it in destroy(). When combining with SortableJS, disable AutoAnimate during drag (onStart) and re-enable after one frame in onEnd. See Transitions for full details.

Conditional Rendering

<!-- Show/hide (CSS display: none) -->
<div data-show="isLoggedIn">Welcome back!</div>

<!-- Show when false (negation) -->
<div data-show="!isLoggedIn">Please log in</div>

<!-- Conditional with computed -->
<div data-show="hasItems">Items available</div>

<!-- DOM insertion/removal (data-render) -->
<div data-render="showExpensiveWidget">
    <!-- Only in DOM when true, components destroyed when false -->
</div>

<!-- With transitions -->
<div data-show="isVisible" data-transition="fade">
    Fades in/out smoothly
</div>

Dynamic Templates (data-template-key)

Select which <template> to render based on a data property. Works on standalone components and lists.

Standalone Component with Persistent Content

Non-template children persist across template swaps, no need to duplicate shared UI in every variant:

<div data-component="profile" data-template-key="viewType">
    <!-- Persistent: survives template swaps -->
    <h2 data-bind="title"></h2>

    <template data-type="card">
        <div class="card">
            <h4 data-bind="name"></h4>
            <p data-bind="role"></p>
        </div>
    </template>

    <template data-type="table">
        <table>
            <tr><td>Name</td><td data-bind="name"></td></tr>
            <tr><td>Role</td><td data-bind="role"></td></tr>
        </table>
    </template>

    <!-- Persistent nav: not duplicated per template -->
    <button data-action="showCard">Card</button>
    <button data-action="showTable">Table</button>
</div>

Heterogeneous List Items

Each list item selects its own template via a type property:

<div data-list="notifications" data-key="id" data-template-key="type">
    <template data-type="info">
        <div class="alert-info"><span data-bind="message"></span></div>
    </template>
    <template data-type="error">
        <div class="alert-danger">
            <span data-bind="message"></span>
            <button data-action="retry">Retry</button>
        </div>
    </template>
    <!-- Fallback for unrecognized types -->
    <template>
        <div><span data-bind="message"></span></div>
    </template>
</div>
Anti-pattern: Do NOT duplicate persistent UI (headers, nav buttons, footers) inside each template variant. Place shared elements outside the <template> tags. They are preserved automatically during swaps.
Key behaviors:
  • data-template-key goes on the data-component or data-list element
  • An untyped <template> acts as the default fallback
  • Component state, computed properties, and non-template DOM are preserved during swaps
  • Nested components inside templates are destroyed/created on swap; outside templates they persist
  • Computed properties can be used as the template key value

Event Handling Variations

<!-- Default click -->
<button data-action="handleClick">Click</button>

<!-- Specific events -->
<input data-action="input:onInput">
<input data-action="change:onChange">
<input data-action="keyup:onKeyUp">
<input data-action="keydown:onKeyDown">
<div data-action="mouseover:onHover">

<!-- Multiple events on same element -->
<input data-action="focus:onFocus blur:onBlur">

<!-- With debounce (wait for pause in typing) -->
<input data-action="input:search" data-event-debounce="300">

<!-- With throttle (limit frequency) -->
<button data-action="save" data-event-throttle="1000">

SPA Routing (spa and full builds)

wildflower.createRouter gives client-side routing with history integration, params, and guards:

const nav = wildflower.store('nav', { state: { page: 'home' } });

const router = wildflower.createRouter({
    mode: 'hash',   // '#/path' URLs — works from any server or a local file.
                    // Use 'history' for clean /path URLs (needs server rewrites).
    routes: [
        { path: '/',          name: 'home',     handler: () => wildflower.getStore('nav').page = 'home' },
        { path: '/products',  name: 'products', handler: () => wildflower.getStore('nav').page = 'products' },
        { path: '/users/:id', name: 'user',     handler: ({ params }) => loadUser(params.id) }
    ]
});

router.navigate('/products');                             // programmatic
router.navigate('/search', { query: { q: 'hello' } });    // with query params

The standard view pattern: each route's handler sets a store field, and each view section binds to it, as in <section data-show="$nav.page === 'home'">. In hash mode, plain <a href="#/products"> links navigate natively; the back and forward buttons work with no extra code. Components may define an onRouteChange(to, from) method, called automatically on every navigation (registered and cleaned up with the component; to.params / to.query carry the route data).

Guards: router.beforeEach(({ to, from }) => ...) runs before every navigation. Return true to allow, false to cancel, or a path string to redirect. Per-route: pass beforeEnter (same contract) and meta: {...} in the route definition. router.afterEach(({ to }) => ...) runs after.

Portals (Teleporting Content)

Render content elsewhere in the DOM while maintaining component ownership. This example teleports a modal to <body>; for the full modal recipe (and when a portal is actually required) see Modals & Dialogs.

<div data-component="modal-trigger">
    <button data-action="showModal">Open Modal</button>

    <!-- This content renders at document body, but actions bind to this component -->
    <div data-portal="body" data-show="isModalOpen">
        <div class="modal-backdrop">
            <div class="modal-content">
                <h2>Modal Title</h2>
                <p data-bind="message"></p>
                <button data-action="closeModal">Close</button>
            </div>
        </div>
    </div>
</div>

Lifecycle Hooks

wildflower.component('example', {
    state: { /* ... */ },

    // Called before component mounts
    beforeInit() {
        // Setup that must happen before DOM bindings
    },

    // Called after component mounts and bindings are active
    init() {
        // Fetch data, setup subscriptions, etc.
    },

    // Called before state update propagates (no parameters - use watchers for specifics)
    beforeUpdate() {
        // Validate or transform data before update
    },

    // Called after state update propagates (no parameters - use watchers for specifics)
    onUpdate() {
        // React to state changes
    },

    // Called before component is destroyed
    beforeDestroy() {
        // Prepare for cleanup
    },

    // Called when component is destroyed
    destroy() {
        // Cleanup subscriptions, timers, etc.
    },

    // Error boundary
    onError(error, info) {
        console.error('Component error:', error);
        // Handle gracefully
    }
});

Watchers Pattern

wildflower.component('example', {
    state: {
        searchQuery: ''
    },

    watch: {
        // Watch state property
        'searchQuery': function(newValue, oldValue) {
            console.log('Search changed:', newValue);
            this.performSearch(newValue);
        },

        // Watch nested property
        'user.profile.name': function(newValue) {
            console.log('Name changed:', newValue);
        }
    }
});

Debugging Silent Failures

Most "broken bindings" turn out to be one of a small set of cases. AI authors don't have the human heuristic of "huh, that's weird"; knowing what to look for matters. The list below covers the cases known as of the current build. Most other mistakes have an explicit error or warning path, but treat that as the usual case rather than a guarantee: the entries here exist precisely because the framework cannot warn about them.

SymptomCauseFix
An element with data-component="foo" stays inert. Nothing initializes, event handlers never bind, and the console stays quiet. The component name foo wasn't registered. The framework's component scanner skips unknown names with no console output. Confirm wildflower.component('foo', { ... }) ran before wildflower.scan() (or before the framework's auto-scan). With defer on both the framework and the app script, the auto-scan picks up registrations made during script parse.
A binding renders empty (the surrounding markup appears, the value doesn't). The bound name resolves to undefined: a misspelled state property, a misspelled computed name, or an item property that doesn't exist on this row. Type the name into the browser console: wildflower.getComponent('your-component').yourBinding. undefined confirms the cause. The binding system intentionally skips DOM writes when a value resolves to undefined rather than rendering the literal string "undefined".
Item-level computed returns the wrong value or never updates. The signature declares scope: fn(item, index, info) { ... } is item-level; fn() { ... } is component-level. A zero-arg computed referenced inside a list-template binding (data-bind="X" in a list <template>) is treated as component-level: same value for every row. If your binding renders the same value for every row when you expected per-row variation, your computed is missing its item parameter. Declare it as fn(item) { ... }. Read the row via item.X; component state via this.X; subscribed stores via this.stores.X. Use the bare name in any binding or inside any expression.
A binding evaluates an expression that calls a method, and the method runs but the DOM doesn't update. The method mutates state inside a computed, which violates the "computeds are pure" contract. Or you put a side effect in a binding expression itself. Move side effects into watch handlers or component methods. Bindings should only read.
Component definition has a syntax error and the page looks blank. The component never registered, so its root element stays inert (same as the unregistered-name case). Open the browser console. JavaScript syntax errors surface there. The framework also logs registration errors via its _log('error', ...) path when the definition shape is invalid (non-object, missing name, etc.).
data-show / data-render always evaluates falsy on a list item. You're referencing a property that's not on the item. this.X from outside is component scope; inside a list item template the same name resolves against item properties first, then component scope, then list-context vars. If the value is a per-item lookup (e.g., is this row "shared" given a sibling lookup map keyed by item.id), declare an item-level computed: isShared(item) { return !!this.shares[item.id]; }.
A computed rendered the right value at first, then stopped updating when one particular field changed. Intermittent: some change sequences update, others do not. The computed reads that field behind a short-circuit (&&, ||, a ternary, an early return) and the branch did not run, so the read never happened and the field was never tracked. A computed depends on what it actually read, not on what it could have read. Read every field that could matter at the top of the computed, then branch. See "Eager dependency reading for conditional computeds" on the Computed Properties page.
A value read from another store or component is correct on first render and never updates again. A raw reference to the other entity's state was captured in a closure or a field, which bypasses the tracking proxy. The dependency is lost with no warning; the framework relies on the convention and cannot detect the bypass. Reach across entities on every evaluation, through $entity.path, subscribe + this.stores, getStore() or getComponent(). Never cache the state object itself.
A watcher never fires, and nothing is logged. this.watch = { ... } was assigned at runtime. Watchers are registered before init() runs, and watch is not part of the component context, so the assignment is ignored. Declare the watch block in the component definition, or call this.subscribe(path, fn) inside init() and keep the returned unsubscribe for destroy().

Cases that are not silent failures (have explicit handling):

  • data-bind-class="{ on: x }" returning an object literal: converted to a class string by _classResultToString; no "[object Object]" ever appears.
  • data-cloak on nested elements: the framework recursively removes the attribute via querySelectorAll('[data-cloak]') regardless of nesting depth.
  • Most invalid expression syntax: surfaces as a JavaScript parse error in the console at compile time.

Quick Reference: 5 Most Common Silent Failures

SymptomOne-line fix
Element with data-component stays inertConfirm the name was registered before scan ran
Binding renders empty (markup OK, value missing)Type the name into the console: wildflower.getComponent('x').y. undefined means typo
Item-level computed always returns wrong valueDeclare with a parameter: fn(item) { ... }. That's what makes it item-level
List-item data-show always falsy on a derived valueDefine an item-level computed: isX(item) { return ... }
Page renders blank, no warningOpen console. JS syntax errors in component definitions surface there

CRITICAL: Anti-Patterns to Avoid

DO NOT use these patterns - they are NOT supported:

No Mustache/Handlebars Syntax

<!-- WRONG - This does NOT work -->
<span>{{count}}</span>
<span>{count}</span>
<span>${count}</span>

<!-- CORRECT -->
<span data-bind="count"></span>

No Magic Variables (Use Underscore Prefix)

<!-- WRONG - Dollar sign variables don't exist -->
<span>{{$index}}</span>
<span data-bind="$parent.name"></span>
<span data-bind="$root.data"></span>

<!-- CORRECT - Use underscore prefix for list context -->
<span data-bind="_index"></span>
<span data-bind="_length"></span>
<button data-bind-class="_first ? 'disabled' : ''">Up</button>
<button data-bind-class="_last ? 'disabled' : ''">Down</button>

<!-- In action handlers, use details object -->
<!-- details.index, details.length, details.first, details.last -->

No v-for, v-if, v-bind (Vue syntax)

<!-- WRONG -->
<div v-for="item in items">
<div v-if="isVisible">
<input v-model="name">

<!-- CORRECT -->
<div data-list="items">
<div data-show="isVisible">
<input data-model="name">

No JSX or React Patterns

// WRONG - No JSX
return <div>{this.state.count}</div>;

// WRONG - No functional components
const MyComponent = ({ count }) => <span>{count}</span>;

// CORRECT - Use component definition
wildflower.component('my-component', {
    state: { count: 0 }
});

No Direct DOM Manipulation for Bound Elements

// WRONG - Bypasses reactivity
document.querySelector('[data-bind="count"]').textContent = 5;

// CORRECT - Update state, DOM updates automatically
this.count = 5;

No Watching List Items by Numeric Index

// WRONG - index paths go stale after splice/reorder (dev builds warn: WF-213)
watch: {
    'items.0.name'(newVal) { /* fires for the wrong slot after a reorder */ }
}
this.subscribe('items.2.done', handler); // same problem

// CORRECT - watch the array, find items by id
watch: {
    items() { this.recalculate(); }
},
computed: {
    activeItem() { return this.items.find(i => i.id === this.selectedId); }
}

Reactivity tracks list items by object identity, not position. The index in a change path reflects where the item sat when it was first observed; after a splice, removal, or reorder, an index-path watcher misfires or goes silent.

Exception: Direct DOM Writes in Pool Components

In data-pool components with a tick(dt) method, direct DOM writes are correct for non-pool UI (FPS counters, HUD stats, score displays):

// CORRECT in tick(): direct write for display-only HUD
tick(dt) {
    // Update entities
    for (const e of this._pool.items) { e.x += e.vx * dt; }

    // Direct DOM write for FPS counter
    this._fpsTimer += dt;
    if (this._fpsTimer >= 500) {
        document.getElementById('fps').textContent = Math.round(this._frameCount / (this._fpsTimer / 1000));
        this._frameCount = 0; this._fpsTimer = 0;
    }
}

Don't route per-frame display values through data-bind. The reactive overhead is unnecessary for values that change 60 times per second. Use data-bind for user-interactive or store-driven values.

Pool Aggregates Are Reactive in Computeds

pool.length and pool.size are reactive when read from a computed or effect: the computed re-evaluates when entities are added, removed, or cleared. This is the natural pattern and it works, including when the computed first runs before the pool has finished registering.

// CORRECT - a computed reading pool.length stays live
wildflower.component('hud', {
    computed: {
        count() { return this.pools.enemies.length; }
    }
});

The reactivity is on-demand. Pools that are never read from a computed or effect carry zero reactive machinery, and plain reads (action handlers, tick bodies) remain a bare map lookup. Per-frame entity field mutation stays outside reactivity entirely; the pool performance contract is unchanged. One nuance: pool.items.length is a plain array read and is NOT reactive. Read pool.length instead.

For per-frame display values that don't need reactivity at all (FPS, frame ms), writing to the DOM directly via cached refs as shown in the previous section is still the right tool. Structural counts belong on pool.length and a computed; per-frame telemetry belongs on direct writes.

data-pool-cull Requires left/top Positioning

Spatial culling (data-pool-cull) checks element positions via getBoundingClientRect(). This only works when entities use left/top CSS properties. Entities positioned with transform: translate() report their untransformed position (0,0), causing incorrect culling. Use left/top for pools that need culling, transform for pools that don't.

destroyComponent() Alone Doesn't Prevent Re-Initialization

The framework's mutation observer treats any element with a stale data-component-id (one whose instance is no longer in componentInstances) as a fresh component pending initialization. On the next scan, triggered by any DOM mutation or an explicit wildflower.scan(), the stale id is stripped, a new instance is created, and its init() fires again. This is intentional: it lets third-party HTML caches (DataTables, jQuery plugins) replay cached DOM containing data-component-id attributes. To truly tear down, remove the element AND destroy the instance.

// WRONG - element stays in DOM, scanner re-inits a fresh instance
wildflower.destroyComponent(instance.id);

// CORRECT - both must happen (either order)
instance.element.remove();
wildflower.destroyComponent(instance.id);

For most teardown (removing UI), don't call destroyComponent() at all: remove the element and the framework cleans up automatically via the mutation observer.

No data-bind-class Colon Syntax

<!-- WRONG - Colon syntax does NOT work -->
<div data-bind-class="isActive:active">

<!-- CORRECT - Object syntax (preferred) -->
<div data-bind-class="{ active: isActive }">

<!-- CORRECT - Ternary expression -->
<div data-bind-class="isActive ? 'active' : ''">

Complete Working Examples

These examples use LOCAL state. When the data comes from a server endpoint, whether fetched, saved back, or both, do not copy these patterns with fetch() bolted on. Declare a query instead ("Live External Data (data-query)" above): loading/error states, refresh, optimistic saves with rollback (write()), and reload persistence (persist: true) are built in.

Counter Component

<div data-component="counter">
    <p>Count: <span data-bind="count"></span></p>
    <button data-action="decrement">-</button>
    <button data-action="increment">+</button>
    <button data-action="reset">Reset</button>
</div>

<script>
wildflower.component('counter', {
    state: {
        count: 0
    },

    increment() {
        this.count++;
    },

    decrement() {
        this.count--;
    },

    reset() {
        this.count = 0;
    }
});
</script>

Todo List Component

<div data-component="todo-list">
    <h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>

    <form data-action="submit:addTodo">
        <input type="text" data-model="newTodo" placeholder="What needs to be done?">
        <button type="submit">Add</button>
    </form>

    <ul data-list="todos">
        <template>
            <li data-bind-class="completed ? 'done' : ''">
                <input type="checkbox" data-model="completed">
                <span data-bind="text"></span>
                <button data-action="removeTodo">Delete</button>
            </li>
        </template>
    </ul>

    <div data-show="hasCompleted">
        <button data-action="clearCompleted">Clear completed</button>
    </div>
</div>

<script>
wildflower.component('todo-list', {
    state: {
        newTodo: '',
        todos: []
    },

    computed: {
        remaining() {
            return this.todos.filter(t => !t.completed).length;
        },
        hasCompleted() {
            return this.todos.some(t => t.completed);
        }
    },

    addTodo(event) {
        event.preventDefault();
        if (this.newTodo.trim()) {
            this.todos.push({
                text: this.newTodo.trim(),
                completed: false
            });
            this.newTodo = '';
        }
    },

    removeTodo(event, element, details) {
        const index = details.index;
        this.todos.splice(index, 1);
    },

    clearCompleted() {
        this.todos = this.todos.filter(t => !t.completed);
    }
});
</script>

Modal (click-outside + Esc-to-close)

The complete pattern (focus management, multiple dialog modes, accessibility, when to add a portal) is documented at Modals & Dialogs. The condensed form:

<div data-component="modal-example">
    <button data-action="open">Open Modal</button>

    <!-- data-event-self: close only when the scrim itself is clicked, not
         when a click bubbles up from the content (no stopPropagation needed).
         data-cloak: hide until the framework processes data-show (no flash).
         Add data-portal="body" ONLY if an ancestor has transform/filter/
         contain that traps position:fixed. -->
    <div class="modal-overlay" data-show="isOpen"
         data-action="close" data-event-self data-cloak>
        <div class="modal-content">
            <h2 data-bind="title"></h2>
            <p data-bind="message"></p>
            <button data-action="close">Close</button>
        </div>
    </div>
</div>

<script>
wildflower.component('modal-example', {
    state: {
        isOpen: false,
        title: 'Modal Title',
        message: 'Click outside or press Escape to close.'
    },

    open()  { this.isOpen = true; },
    close() { this.isOpen = false; },

    // Esc-to-close is a global shortcut while the modal is open, so it
    // belongs on document, not as data-event-key-escape on an element
    // (that form is for shortcuts scoped to a focused widget).
    init() {
        this._onKey = (e) => { if (e.key === 'Escape' && this.isOpen) this.close(); };
        document.addEventListener('keydown', this._onKey);
    },
    destroy() { document.removeEventListener('keydown', this._onKey); }
});
</script>

Search with Debounce

<div data-component="search-box">
    <input
        type="text"
        data-model="query"
        data-action="input:search"
        data-event-debounce="300"
        placeholder="Search...">

    <div data-show="isLoading">Searching...</div>

    <ul data-list="results" data-show="!isLoading">
        <template>
            <li data-bind="title"></li>
        </template>
    </ul>

    <p data-show="noResults">No results found</p>
</div>

<script>
wildflower.component('search-box', {
    state: {
        query: '',
        results: [],
        isLoading: false
    },

    computed: {
        noResults() {
            return !this.isLoading &&
                   this.query.length > 0 &&
                   this.results.length === 0;
        }
    },

    async search() {
        if (!this.query.trim()) {
            this.results = [];
            return;
        }

        this.isLoading = true;

        try {
            const response = await fetch(`/api/search?q=${this.query}`);
            this.results = await response.json();
        } finally {
            this.isLoading = false;
        }
    }
});
</script>

Quick Setup Template

Minimal HTML to start a WildflowerJS project:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>My WildflowerJS App</title>

    <!-- Scripts in head with defer for best performance -->
    <script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
    <script defer src="app.js"></script>
</head>
<body>
    <div data-component="my-app">
        <h1>Hello, <span data-bind="name"></span>!</h1>
        <input type="text" data-model="name" placeholder="Enter your name">
    </div>
</body>
</html>

With app.js:

wildflower.component('my-app', {
    state: {
        name: 'World'
    }
});

Testing WildflowerJS Components

Use @wildflowerjs/test-utils for component testing. The package provides utilities for framework loading, state management, and timing.

AI Testing Guide: A comprehensive 900+ line guide is available at packages/test-utils/AI_TESTING_GUIDE.md with detailed recipes for 15+ testing scenarios.

Standard Test Template

import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'
import {
  loadFramework, resetFramework, waitForUpdate,
  waitForCompleteRender, createTestContainer, initContextSystem
} from '@wildflowerjs/test-utils'

describe('MyComponent', () => {
  let container, cleanup

  beforeAll(async () => { await loadFramework() })

  beforeEach(() => {
    resetFramework()
    initContextSystem()
    const result = createTestContainer()
    container = result.container
    cleanup = result.cleanup
  })

  afterEach(() => { cleanup() })

  it('should render correctly', async () => {
    wildflower.component('my-comp', { state: { value: 'Hello' } })
    container.innerHTML = `<div data-component="my-comp">
      <span data-bind="value"></span>
    </div>`

    wildflower._scanForDynamicComponents()
    await waitForCompleteRender()

    expect(container.querySelector('span').textContent).toBe('Hello')
  })
})

Critical: Timing Utilities

UtilityWhen to Use
waitForUpdate()After state changes, button clicks, input events
waitForCompleteRender()After _scanForDynamicComponents(), initial render
waitForState(instance, 'path', value)Waiting for async operations

Primitive Lists ($item)

For lists of strings, numbers, or other primitives, use $item to bind the item value:

<ul data-list="tags">
    <template>
        <li data-bind="$item"></li>
    </template>
</ul>

$item refers to the current item itself. For object arrays, use property names directly (e.g., data-bind="name").

List Context Bindings

Inside list templates, computed properties are resolved by bare name, just like at the component level:

<!-- Component level -->
<div data-bind-style="myStyle">...</div>

<!-- Inside list template: same syntax, no prefix needed -->
<div data-list="items">
  <template>
    <div data-bind-style="itemStyle">...</div>
    <span data-bind="name"></span> <!-- item property -->
    <span data-show="isInCart">IN CART</span> <!-- item-level computed -->
  </template>
</div>

Computed properties take precedence over state when names collide. Inside a list template, item-level computed properties (those with parameters) receive the current item automatically.

Item-Level Computed Properties (Reactive Methods)

Item-level computeds allow per-item calculations in lists that can access component state, store state, and other computeds with automatic reactivity. The framework detects item-level computeds by checking if the function has parameters (fn.length > 0).

Item-level computeds are first-class. Any function declared with a parameter (fn(item) { ... }) can be used as a bare reference (data-bind="X") or inside any expression in any binding type:
  • data-bind, data-bind-class, data-bind-style, data-bind-attr
  • data-show, data-render (including compound expressions like isShared && !isLocked)
  • Object syntax ({ active: isShared }), ternaries (isShared ? 'on' : ''), string concat (isShared ? '✓ ' + name : name)
  • Nested lists: an item-level computed declared on the outer component receives the inner item when used inside an inner list's template
  • As the source array of a nested data-list: <div data-list="reactionChips"> inside a comment row evaluates the reactionChips computed against each comment, so you don't need to pre-decorate parent rows with the array
Inside the function body, the row's data arrives as the first parameter (item); this remains the component context and resolves through the same ContextProxy as component methods: own props → computed → state → store. A zero-arg computed referenced in a row is evaluated at component scope and cannot see item properties.

Nested data-list with an item-level computed source

The framework reads item[path] first (fast path for raw fields) and falls back to evaluating an item-level computed of the same name when the field is undefined. Same authoring pattern as data-bind:

wildflower.component('thread', {
    state: {
        comments: [/* { id, body, reactions: { '👍': ['u-1', 'u-2'] } } */]
    },
    computed: {
        // Per-comment reaction list. The computed receives the comment row
        // as its argument; we project its raw reactions object to a chip
        // array the inner data-list can iterate. No need to pre-build this
        // on every comment.
        reactionChips(comment) {
            return ['👍','❤️','🚀'].map(emoji => {
                const users = (comment.reactions && comment.reactions[emoji]) || []
                return {
                    id: comment.id + ':' + emoji,
                    emoji,
                    chipClass: users.length > 0 ? 'chip is-active' : 'chip'
                }
            })
        }
    }
})
<div data-list="comments" data-key="id">
    <template>
        <div class="comment">
            <p data-bind="body"></p>
            <div class="reactions" data-list="reactionChips" data-key="id">
                <template>
                    <button data-bind-class="chipClass" data-bind="emoji"></button>
                </template>
            </div>
        </div>
    </template>
</div>

Caveat: the fallback only fires when item[path] is undefined and path is a flat name (no dots). A raw field of the same name shadows the computed; for traversal lookups, use a flat-named computed.

Key Insight: Functions with parameters are item-level (receive item, index), functions without parameters are component-level (evaluated once per change).
wildflower.component('product-list', {
    state: {
        products: [
            { id: 1, name: 'Widget', price: 10 },
            { id: 2, name: 'Gadget', price: 20 }
        ],
        taxRate: 0.1
    },
    computed: {
        // Component-level (no parameters) - evaluated once at component level
        cartTotal() {
            return wildflower.getStore('cart').total;
        },

        // Item-level (has parameters) - evaluated per list item
        // Signature: (item, index) - matches JS array method conventions
        inCartQty(item) {
            const cart = wildflower.getStore('cart');
            return cart.items.find(i => i.id === item.id)?.qty || 0;
        },

        // Item-level computeds can call other item-level computeds
        isInCart(item) {
            return this.inCartQty(item) > 0;
        },

        // Can access component state directly
        priceWithTax(item) {
            return '$' + (item.price * (1 + this.taxRate)).toFixed(2);
        },

        // Second parameter is the index
        rowClass(item, index) {
            return index % 2 === 0 ? 'row-even' : 'row-odd';
        }
    }
});

Template usage. Computed properties resolve by name, no prefix needed:

<div data-list="products" data-key="id">
    <template>
        <div class="product" data-bind-class="rowClass">
            <span data-bind="name"></span>
            <span data-bind="priceWithTax"></span>
            <span class="badge" data-bind="inCartQty"></span>
            <span data-show="isInCart">IN CART</span>
        </div>
    </template>
</div>

Key Features

  • Automatic detection: Functions with parameters are item-level, functions without are component-level
  • Store reactivity: When store state changes, only affected list item bindings update (not the whole list)
  • Computed chaining: this.otherComputed(item) calls another item-level computed; use the flat this.name(item) form and pass the row argument normally; no prefix required
  • Full context access: this.state, this.props available inside item-level computeds
  • Works with every expression-accepting binding: data-bind, data-show, data-render, data-bind-class, data-bind-style, data-bind-attr

Compound Expressions Are Supported

An item-level computed can be used as a bare reference (simplest) or dropped into any expression: object literal, ternary, logical operator, string concatenation. The framework re-evaluates per row and tracks the same fine-grained dependencies in either case.

<!-- All of these forms call isShared(item) once per row -->
<li data-bind-class="isShared">...</li>                           <!-- bare reference (returns string) -->
<li data-bind-class="{ saved: isShared }">...</li>                <!-- object syntax -->
<li data-bind-class="isShared ? 'saved' : ''">...</li>            <!-- ternary -->
<li data-bind-attr="({ 'aria-pressed': isShared })">...</li>      <!-- attr object -->
<li data-show="isShared && !isLocked">...</li>                    <!-- compound show -->
<li data-bind="isShared ? '✓ ' + name : name">...</li>            <!-- ternary in text -->
<li data-bind-style="({ borderColor: badgeColor })">...</li>      <!-- style object -->

Mutating sibling state the computed reads, for example this.shares = { ...this.shares, [item.id]: ... }, re-runs only the affected row's bindings. This is the canonical pattern for per-item lookup maps keyed by item.id. See https://www.wildflowerjs.com/docs/lists#item-level-computed-properties for a live, runnable example.

Use Case: This pattern is ideal for shopping carts, badge counts, per-item status indicators, and any scenario where list items need to react to external (store) state changes.

Summary Checklist for AI Assistants

Before generating WildflowerJS code, verify:
  • Using data-bind, NOT {{mustache}} syntax
  • Using data-list with <template>, NOT v-for
  • Using data-pool for high-frequency entity rendering (games, dashboards), NOT data-list
  • Using data-show or data-render, NOT v-if
  • Using data-model for two-way binding, NOT v-model
  • Using data-action for events, NOT @click or onClick
  • Using data-bind-class="{ active: isActive }" or data-bind-class="expr ? 'class' : ''", NOT data-bind-class="prop:class"
  • Component defined with wildflower.component(name, definition)
  • State is an object, computed is an object of functions
  • Methods are directly on the component definition (not in a methods object)
  • No build step needed - standard HTML and JavaScript only

Framework Migration Guides

Comprehensive translation guides for converting code from other frameworks to WildflowerJS. Share these patterns with your AI coding assistant to help it convert your existing code accurately.

For AI Assistants: These migration guides provide direct mappings from popular frameworks. When asked to convert code from React, Vue, Svelte, or Alpine.js, reference the appropriate section below for accurate translations.

React to WildflowerJS

React uses JSX, hooks, and a virtual DOM. WildflowerJS uses HTML attributes, reactive state, and direct DOM updates. The table below maps one to the other.

Core Concepts Mapping

React WildflowerJS Notes
useState(initialValue) state: { prop: initialValue } State is an object, not individual hooks
useMemo(() => derived, [deps]) computed: { derived() { return ... } } Dependencies are tracked automatically
useEffect(() => {}, [deps]) watch: { 'prop': fn } or init() Watch for reactive effects, init for mount
useRef() document.querySelector() in init() Direct DOM access when needed
useContext() $entity.path or stores Cross-component communication
useCallback() Methods on component Methods are stable by default
useReducer() Methods that update state No need for reducers, update state directly
Props props: {} + data-prop-* Explicit prop definitions with validation
Children / slots data-slot Named slots for content projection

JSX to HTML Attribute Mapping

React JSX WildflowerJS HTML
<span>{count}</span> <span data-bind="count"></span>
<span>{user.name}</span> <span data-bind="user.name"></span>
<span>{doubleCount}</span> (derived) <span data-bind="doubleCount"></span>
<div dangerouslySetInnerHTML={{__html: html}}/> <div data-bind-html="htmlContent"></div>
<input value={val} onChange={e => setVal(e.target.value)}/> <input data-model="val">
<button onClick={handleClick}> <button data-action="handleClick">
<button onClick={() => setCount(c => c+1)}> <button data-action="increment">
<input onChange={handleChange}/> <input data-action="change:handleChange">
<input onInput={handleInput}/> <input data-action="input:handleInput">
{isVisible && <div>...</div>} <div data-show="isVisible">...</div>
{isVisible ? <A/> : <B/>} <div data-show="isVisible">A</div><div data-show="!isVisible">B</div>
{items.map(item => <li key={item.id}>{item.name}</li>)} <ul data-list="items"><template><li data-bind="name"></li></template></ul>
className={isActive ? 'active' : ''} data-bind-class="{ active: isActive }"
className={`btn ${variant}`} class="btn" data-bind-class="variant"
style={{backgroundColor: color}} data-bind-style="{ backgroundColor: color }"

Complete React to WildflowerJS Example

React Version:

import { useState, useMemo } from 'react';

function TodoList() {
    const [todos, setTodos] = useState([]);
    const [newTodo, setNewTodo] = useState('');

    const remaining = useMemo(() =>
        todos.filter(t => !t.completed).length,
        [todos]
    );

    const addTodo = (e) => {
        e.preventDefault();
        if (newTodo.trim()) {
            setTodos([...todos, { text: newTodo.trim(), completed: false }]);
            setNewTodo('');
        }
    };

    const toggleTodo = (index) => {
        const updated = [...todos];
        updated[index].completed = !updated[index].completed;
        setTodos(updated);
    };

    const removeTodo = (index) => {
        setTodos(todos.filter((_, i) => i !== index));
    };

    return (
        <div>
            <h2>Todo List ({remaining} remaining)</h2>
            <form onSubmit={addTodo}>
                <input
                    value={newTodo}
                    onChange={e => setNewTodo(e.target.value)}
                    placeholder="What needs to be done?"
                />
                <button type="submit">Add</button>
            </form>
            <ul>
                {todos.map((todo, index) => (
                    <li key={index} className={todo.completed ? 'done' : ''}>
                        <input
                            type="checkbox"
                            checked={todo.completed}
                            onChange={() => toggleTodo(index)}
                        />
                        <span>{todo.text}</span>
                        <button onClick={() => removeTodo(index)}>Delete</button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

WildflowerJS Version:

<div data-component="todo-list">
    <h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>

    <form data-action="submit:addTodo">
        <input data-model="newTodo" placeholder="What needs to be done?">
        <button type="submit">Add</button>
    </form>

    <ul data-list="todos">
        <template>
            <li data-bind-class="completed ? 'done' : ''">
                <input type="checkbox" data-model="completed">
                <span data-bind="text"></span>
                <button data-action="removeTodo">Delete</button>
            </li>
        </template>
    </ul>
</div>

<script>
wildflower.component('todo-list', {
    state: {
        todos: [],
        newTodo: ''
    },

    computed: {
        remaining() {
            return this.todos.filter(t => !t.completed).length;
        }
    },

    addTodo(event) {
        event.preventDefault();
        if (this.newTodo.trim()) {
            this.todos.push({
                text: this.newTodo.trim(),
                completed: false
            });
            this.newTodo = '';
        }
    },

    removeTodo(event, element, details) {
        const index = details.index;
        this.todos.splice(index, 1);
    }
});
</script>

Key Differences from React

  • No JSX: Use HTML with data attributes instead of JSX syntax
  • No build step: Works directly in browser, no webpack/vite needed
  • Mutable state: Update this.prop directly, no need for setter functions
  • Array mutations: Use push, splice, etc. directly - reactivity tracks mutations
  • No virtual DOM: Direct DOM updates, often faster for simple apps
  • Automatic dependency tracking: No need to specify dependencies in computed properties
  • Checkbox binding: data-model on checkbox binds to boolean automatically
  • Index access: Get index via details.index parameter in action methods

Vue to WildflowerJS

Vue's Options API is closest to WildflowerJS. The Composition API maps similarly to React hooks. The table below gives the translation.

Options API Mapping

Vue Options API WildflowerJS Notes
data() { return {} } state: {} Object directly, not a function
computed: {} computed: {} Identical pattern
methods: {} Methods directly on component No methods wrapper needed
watch: {} watch: {} Identical pattern
mounted() init() Called after component mounts
beforeUnmount() destroy() Cleanup before removal
props: {} props: {} Similar prop definitions
emits: [] this.emit('event', detail) Parent handles via onEvent(detail) method

Template Directive Mapping

Vue Template WildflowerJS HTML
{{ message }} <span data-bind="message"></span>
{{ computed }} <span data-bind="computedName"></span>
v-html="rawHtml" data-bind-html="rawHtml"
v-model="text" data-model="text"
v-model.number="count" <input data-model="count" data-model-number>
v-show="isVisible" data-show="isVisible"
v-if="condition" data-render="condition"
v-else data-show="!condition" (no direct equivalent)
v-for="item in items" :key="item.id" <div data-list="items"><template>...</template></div>
v-for="(item, index) in items" Index via data-bind="_index" in templates or details.index in actions
@click="handleClick" data-action="handleClick"
@click.prevent="handleClick" data-action="handleClick" data-event-prevent
@click.stop="handleClick" data-action="handleClick" data-event-stop
@input="onInput" data-action="input:onInput"
@keyup.enter="submit" data-action="keyup:submit" data-event-key-enter
@input.debounce="search" data-action="input:search" data-event-debounce="300"
:class="{ active: isActive }" data-bind-class="{ active: isActive }"
:class="[baseClass, { active: isActive }]" class="baseClass" data-bind-class="{ active: isActive }"
:style="{ color: textColor }" data-bind-style="{ color: textColor }"
:disabled="isDisabled" data-bind-attr="{ disabled: isDisabled }"
<slot></slot> <div data-slot="default"></div>
<slot name="header"></slot> <div data-slot="header"></div>
<Teleport to="body"> data-portal="body"

Complete Vue to WildflowerJS Example

Vue Version:

<template>
    <div>
        <h2>Counter: {{ count }}</h2>
        <p>Double: {{ doubleCount }}</p>

        <button @click="decrement" :disabled="count <= 0">-</button>
        <button @click="increment">+</button>
        <button @click="reset">Reset</button>

        <div v-show="count > 10" class="warning">
            That's a lot!
        </div>

        <ul>
            <li v-for="(item, index) in history" :key="index">
                {{ item.action }}: {{ item.value }}
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            count: 0,
            history: []
        };
    },

    computed: {
        doubleCount() {
            return this.count * 2;
        }
    },

    methods: {
        increment() {
            this.count++;
            this.history.push({ action: 'increment', value: this.count });
        },

        decrement() {
            if (this.count > 0) {
                this.count--;
                this.history.push({ action: 'decrement', value: this.count });
            }
        },

        reset() {
            this.count = 0;
            this.history = [];
        }
    }
};
</script>

WildflowerJS Version:

<div data-component="counter">
    <h2>Counter: <span data-bind="count"></span></h2>
    <p>Double: <span data-bind="doubleCount"></span></p>

    <button data-action="decrement">-</button>
    <button data-action="increment">+</button>
    <button data-action="reset">Reset</button>

    <div data-show="showWarning" class="warning">
        That's a lot!
    </div>

    <ul data-list="history">
        <template>
            <li>
                <span data-bind="action"></span>: <span data-bind="value"></span>
            </li>
        </template>
    </ul>
</div>

<script>
wildflower.component('counter', {
    state: {
        count: 0,
        history: []
    },

    computed: {
        doubleCount() {
            return this.count * 2;
        },

        showWarning() {
            return this.count > 10;
        }
    },

    increment() {
        this.count++;
        this.history.push({ action: 'increment', value: this.count });
    },

    decrement() {
        if (this.count > 0) {
            this.count--;
            this.history.push({ action: 'decrement', value: this.count });
        }
    },

    reset() {
        this.count = 0;
        this.history = [];
    }
});
</script>

Key Differences from Vue

  • No SFC: No Single File Components - HTML and JS are separate or inline
  • No build step: No vue-loader or vite plugin needed
  • State access: Use this.prop directly (same as Vue's Options API)
  • Methods location: Methods are directly on component, not in methods: {}
  • No v-else: Use negated conditions like data-show="!condition"
  • Expressions in templates: Limited - use computed properties for complex logic
  • Event modifiers: Use separate attributes like data-event-prevent
  • Vuex/Pinia equivalent: Use wildflower.store() to define stores, subscribe + this.stores to access them

Svelte to WildflowerJS

Svelte compiles away the framework. WildflowerJS also has minimal runtime overhead with direct DOM updates. The table below maps one to the other.

Core Concepts Mapping

Svelte WildflowerJS Notes
let count = 0; (reactive) state: { count: 0 } All state in state object
$: doubled = count * 2; computed: { doubled() { return this.count * 2; } } Computed properties
$: { console.log(count); } watch: { count(newVal) { console.log(newVal); } } Reactive statements become watchers
export let prop; props: { prop: { type: String } } Explicit prop definitions
onMount(() => {}) init() {} Lifecycle on mount
onDestroy(() => {}) destroy() {} Lifecycle on destroy
createEventDispatcher() $entity.path or custom events Component communication
Stores (writable, readable) wildflower.store() + subscribe Global state management

Template Syntax Mapping

Svelte Template WildflowerJS HTML
{count} <span data-bind="count"></span>
{@html rawHtml} <div data-bind-html="rawHtml"></div>
bind:value={text} data-model="text"
bind:checked={isChecked} data-model="isChecked"
bind:group={selected} data-model="selected" on each radio
on:click={handleClick} data-action="handleClick"
on:click|preventDefault={fn} data-action="fn" data-event-prevent
on:click|stopPropagation={fn} data-action="fn" data-event-stop
on:keydown|self={fn} data-action="keydown:fn"
{#if condition}...{/if} <div data-show="condition">...</div>
{#if condition}...{:else}...{/if} <div data-show="condition">...</div><div data-show="!condition">...</div>
{#each items as item}...{/each} <div data-list="items"><template>...</template></div>
{#each items as item, index}...{/each} Index via data-bind="_index" in templates or details.index in actions
{#each items as item (item.id)}...{/each} Keyed rendering automatic when items have id
class:active={isActive} data-bind-class="{ active: isActive }"
style:color={textColor} data-bind-style="{ color: textColor }"
<slot></slot> <div data-slot="default"></div>
<slot name="header"></slot> <div data-slot="header"></div>
transition:fade data-transition="fade"

Complete Svelte to WildflowerJS Example

Svelte Version:

<script>
    let items = [];
    let newItem = '';

    $: itemCount = items.length;
    $: hasItems = items.length > 0;

    function addItem() {
        if (newItem.trim()) {
            items = [...items, { text: newItem.trim(), done: false }];
            newItem = '';
        }
    }

    function toggleItem(index) {
        items[index].done = !items[index].done;
        items = items; // Trigger reactivity
    }

    function removeItem(index) {
        items = items.filter((_, i) => i !== index);
    }
</script>

<div>
    <h2>Items: {itemCount}</h2>

    <form on:submit|preventDefault={addItem}>
        <input bind:value={newItem} placeholder="New item">
        <button type="submit">Add</button>
    </form>

    {#if hasItems}
        <ul>
            {#each items as item, index}
                <li class:done={item.done}>
                    <input type="checkbox" bind:checked={item.done}>
                    <span>{item.text}</span>
                    <button on:click={() => removeItem(index)}>X</button>
                </li>
            {/each}
        </ul>
    {:else}
        <p>No items yet</p>
    {/if}
</div>

WildflowerJS Version:

<div data-component="item-list">
    <h2>Items: <span data-bind="itemCount"></span></h2>

    <form data-action="submit:addItem">
        <input data-model="newItem" placeholder="New item">
        <button type="submit">Add</button>
    </form>

    <ul data-list="items" data-show="hasItems">
        <template>
            <li data-bind-class="done ? 'done' : ''">
                <input type="checkbox" data-model="done">
                <span data-bind="text"></span>
                <button data-action="removeItem">X</button>
            </li>
        </template>
    </ul>

    <p data-show="!hasItems">No items yet</p>
</div>

<script>
wildflower.component('item-list', {
    state: {
        items: [],
        newItem: ''
    },

    computed: {
        itemCount() {
            return this.items.length;
        },
        hasItems() {
            return this.items.length > 0;
        }
    },

    addItem(event) {
        event.preventDefault();
        if (this.newItem.trim()) {
            this.items.push({
                text: this.newItem.trim(),
                done: false
            });
            this.newItem = '';
        }
    },

    removeItem(event, element, details) {
        const index = details.index;
        this.items.splice(index, 1);
    }
});
</script>

Key Differences from Svelte

  • No compilation: WildflowerJS runs directly in browser, no build needed
  • Explicit state: All reactive variables go in state: {}
  • Direct mutation: Use push, splice directly - no need to reassign arrays
  • Computed syntax: Functions in computed: {} instead of $: labels
  • Watchers syntax: watch: {} object instead of $: reactive statements
  • No special template syntax: Uses HTML attributes instead of {#if}, {#each}
  • Class binding: Ternary expression instead of class:name directive

Alpine.js to WildflowerJS

Alpine.js is the closest in philosophy to WildflowerJS - both use HTML attributes for reactivity. However, WildflowerJS uses named components and has a more structured approach:

Directive Mapping

Alpine.js WildflowerJS Notes
x-data="{ count: 0 }" data-component="name" + state: {} Named component with separate definition
x-text="message" data-bind="message" Text content binding
x-html="rawHtml" data-bind-html="rawHtml" HTML content binding
x-model="text" data-model="text" Two-way binding
x-show="isVisible" data-show="isVisible" Identical behavior
x-if="condition" data-render="condition" DOM insertion/removal
x-for="item in items" data-list="items" + <template> List rendering
@click="handleClick" data-action="handleClick" Click events
@click.prevent="fn" data-action="fn" data-event-prevent Event modifiers
@click.stop="fn" data-action="fn" data-event-stop Stop propagation
@click.outside="fn" data-action="fn" data-event-outside Detects clicks outside the element
@keyup.enter="fn" data-action="keyup:fn" data-event-key-enter Key modifiers
@input.debounce.300ms="fn" data-action="input:fn" data-event-debounce="300" Debounce
:class="{ active: isActive }" data-bind-class="{ active: isActive }" Class binding
:style="{ color: textColor }" data-bind-style="{ color: textColor }" Style binding
x-init="init()" init() {} in component Initialization hook
x-effect watch: {} Side effects on state change
$watch('prop', fn) watch: { prop: fn } Watch specific property
$refs.element document.querySelector() in init DOM references
$dispatch('event') element.dispatchEvent() or $entity.path Custom events
x-teleport="body" data-portal="body" Teleport content
x-transition data-transition="name" CSS transitions

Complete Alpine.js to WildflowerJS Example

Alpine.js Version:

<div x-data="{
    open: false,
    search: '',
    items: ['Apple', 'Banana', 'Cherry', 'Date'],
    get filtered() {
        return this.items.filter(i =>
            i.toLowerCase().includes(this.search.toLowerCase())
        );
    }
}">
    <button @click="open = !open">
        <span x-text="open ? 'Close' : 'Open'"></span>
    </button>

    <div x-show="open" x-transition>
        <input
            x-model="search"
            @input.debounce.300ms="console.log('searching')"
            placeholder="Search..."
        >

        <ul>
            <template x-for="item in filtered" :key="item">
                <li x-text="item"></li>
            </template>
        </ul>

        <p x-show="filtered.length === 0">No results</p>
    </div>
</div>

WildflowerJS Version:

<div data-component="search-dropdown">
    <button data-action="toggle">
        <span data-bind="buttonText"></span>
    </button>

    <div data-show="open" data-transition="fade">
        <input
            data-model="search"
            data-action="input:onSearch"
            data-event-debounce="300"
            placeholder="Search..."
        >

        <ul data-list="filtered">
            <template>
                <li data-bind="$item"></li>
            </template>
        </ul>

        <p data-show="noResults">No results</p>
    </div>
</div>

<script>
wildflower.component('search-dropdown', {
    state: {
        open: false,
        search: '',
        items: ['Apple', 'Banana', 'Cherry', 'Date']
    },

    computed: {
        buttonText() {
            return this.open ? 'Close' : 'Open';
        },

        filtered() {
            const search = this.search.toLowerCase();
            return this.items.filter(i =>
                i.toLowerCase().includes(search)
            );
        },

        noResults() {
            return this.filtered.length === 0 && this.search.length > 0;
        }
    },

    toggle() {
        this.open = !this.open;
    },

    onSearch() {
        console.log('searching');
    }
});
</script>

Key Differences from Alpine.js

  • Named components: WildflowerJS uses named, reusable component definitions
  • Separate definition: Component logic is in JavaScript, not inline in HTML attributes
  • State object: All state in state: {}, not inline in x-data
  • Computed properties: Use computed: {} instead of getters in x-data
  • Methods: Defined on component, not inline in attributes
  • List template: Uses <template> element instead of template attribute
  • Simple values in lists: Use data-bind="$item" for primitive arrays
  • Event modifiers: Separate attributes instead of dot notation
  • More structure: Better for larger applications with reusable components

Computed Property Access

Computed properties are resolved by bare name across all binding types:

<!-- Just use the name directly -->
<span data-bind="fullName"></span>

<!-- Works on ALL binding types -->
<div data-show="isValid"></div>
<div data-bind-class="statusClass"></div>
<div data-bind-style="boxStyle"></div>
<ul data-list="filteredItems">...</ul>

<!-- In expressions, computed properties also resolve by name -->
<span data-bind="total > 100 ? 'Over budget' : 'OK'"></span>

<!-- Inside list templates, same rules apply -->
<ul data-list="items">
    <template>
        <li data-bind="name"></li>
        <li data-bind-class="id === selectedId ? 'selected' : ''"></li>
    </template>
</ul>

Name Collisions

When a state property and computed property share the same name, computed takes precedence.

JavaScript Access

In JavaScript code (methods, computed functions, lifecycle hooks), ContextProxy auto-resolves state and computed properties. Use bare names:

// ✅ CORRECT - ContextProxy auto-resolves
this.count              // Access state
this.fullName           // Access computed

// ✅ ALSO WORKS - explicit paths are still valid
this.state.count        // Explicit state access
this.computed.fullName  // Explicit computed access (item-level computeds are called flat as this.name(item); no prefix needed)

Quick Reference

Context Syntax Example
HTML template (any binding) Bare name (prefix optional) data-bind="fullName"
HTML expression Bare name in expression data-bind="total > 100 ? 'High' : 'Low'"
Inside list template Bare name (prefix optional) data-show="isInCart"
JavaScript methods/computed Bare name (auto-resolved) this.count, this.fullName
Item-level computed chaining this.X(item) (flat form; no prefix needed) this.inCartQty(item)
$entity.path in templates Not needed (auto-resolved) data-bind="$comp.name"