WildflowerJS Reactive JS, No BS*

A no-build reactive JavaScript framework, rooted in the web platform.
No build step. No dependencies. No lock-in.

<script src="wildflower.min.js"></script> ...and start building.

Back to Basics

The code you write is 100% web standard code. HTML stays HTML. JavaScript stays JavaScript. CSS stays CSS. No JSX, no templating language, no custom syntax to learn. If you know the web platform, you already know how to use this.

WildflowerJS extends the web platform. It doesn't replace it.

Your Development Simplified

Because you develop with 100% web standards, every tool in your existing chain already understands the code: IDE, browser DevTools, linter, formatter, screen reader, SEO crawler. Nothing to install, no custom file types, no sourcemaps. Save the file, refresh, and your change is live.

Just be a web developer.

Batteries Included: One Mental Model

Router, SSR, stores, computed properties, two-way binding, event modifiers, data pools, and TypeScript types, all built in, all speaking the same language. Learn data-bind once and you know binding everywhere: lists, pools, stores, forms. There's no five-library stack to keep in sync.

One script tag. Everything you need.

<div data-component="counter">
  <span data-bind="count"></span>
  <button data-action="increment">
    +1
  </button>
</div>

<script>
wildflower.component('counter', {
  state: { count: 0 },
  increment() { this.count++ }
})
</script>

How It Works

data-bind connects state to the DOM.

data-action connects events to methods.

this.count++ triggers a precise DOM update.

Mutate state. The DOM updates.

Two Reactivity Modes

data-list for automatic reactivity: mutate state, DOM updates. data-pool for explicit control: plain objects, zero proxy overhead, you say what changed.

Same template syntax. Different performance profile. From interactive forms to per-frame particle systems. You choose the right tradeoff for the job.

Try it. Right-click, inspect this demo. Every dot is a real DOM element.

See full demo →

* Build Step

Zero Toolchain

Modern frameworks ask you to install a compiler, a bundler, a package manager, hundreds of fragile transitive dependencies, and a framework-specific file format, before you write a single line of your application.

WildflowerJS was built starting from a single principle: no build step, no tooling. Ever.

WildflowerJS asks you to add a script tag.

There's no CLI scaffolding step, no config files, no .vue/.jsx/.svelte source format. You don't debug through sourcemaps or wait on a build pipeline. Your project has zero dependencies.

Performance isn't a tradeoff. Build steps optimize bundle delivery, not the runtime work that follows it. WildflowerJS writes directly to the DOM, with no virtual DOM or reconciliation pass between state change and update, so it doesn't need a build step to be fast.

The framework is full-featured without the toolchain: router, SSR, stores, computed properties, transitions, pools. You don't need a toolchain to use any of it.

my-app/
  index.html
  app.js
  style.css
  wildflower.min.js

That's the entire project. No package.json.
No node_modules. No config files. Ship it.

Zero Install. Zero Attack Surface.

Every dependency you install is trust extended to a maintainer you've never met, running scripts on your dev machine and in your CI. A typical React + Vite + UI‑lib setup pulls in 300+ transitive packages before you write a feature.

Each one is a potential intrusion vector. NPM worms, OAuth chains compromising deploy platforms, postinstall hijacking: the supply chain is now where production code gets compromised, not the deploy. And signing isn't a backstop: Mini Shai‑Hulud (May 2026) compromised 170+ packages whose malicious versions carried valid SLSA Build Level 3 provenance, because the attestation came from build infrastructure the worm had already taken over.

WildflowerJS users don't have this attack surface, by construction. There is no npm install, no postinstall script, no transitive package graph. The framework is one file you copy or pin by hash.

As of v1.1, the same holds for building the framework itself. WildflowerJS bundles with a vendored rollup and terser pipeline pulled as three SHA‑512‑pinned tarballs: no npm install, no transitive packages, no postinstall scripts in the build path. The entire toolchain is three files you verify by hash.

Zero dependencies is the absence of a problem the rest of the industry has not properly addressed.

A typical React/Vue project:

  npm install
  ├── hundreds of packages
  ├── from hundreds of maintainers
  ├── postinstall scripts run on install
  └── tens to hundreds of MB of transitive code

WildflowerJS:

  <script src="wildflower.min.js"></script>
  └── 1 file.
      No transitive dependencies.

Zero Lock-in

WildflowerJS works with the DOM, not instead of it. There's no virtual DOM intercepting your code and no compiler rewriting your markup. The render cycle is yours.

That means Leaflet, DataTables, Chart.js, D3, Three.js, any library that touches the DOM, just works. No wrapper packages or framework-specific escape hatches required. Drop in a script tag and use it.

Because your code is standard HTML and JavaScript, you're never locked in. Your skills transfer and your code is more portable. If you outgrow the framework, your knowledge doesn't expire.

This also means your "ecosystem" is all of the world of vanilla JS. Without compromises or hacks.

<!-- Use any library directly -->
<div data-component="map-view">
  <div id="map" style="height: 400px"></div>
</div>
wildflower.component('map-view', {
  state: { lat: 51.505, lng: -0.09 },
  init() {
    // Leaflet works as-is. No wrappers.
    this._map = L.map('map')
      .setView([this.lat, this.lng], 13);
    L.tileLayer('https://{s}.tile.osm.org'
      + '/{z}/{x}/{y}.png').addTo(this._map);
  }
})

Precise Reactivity

When you write this.count++, WildflowerJS updates the single DOM node bound to count. Nothing else is touched. There's no tree diffing or reconciliation pass to figure that out.

This isn't a tradeoff. You get fine-grained updates and a simple mental model. Change a property, the bound element updates. That's the entire reactivity model.

Other frameworks ask you to learn signals, accessors, memos, effects, and subscription lifecycles to achieve what WildflowerJS does with a property assignment.

wildflower.component('dashboard', {
  state: {
    users: 1420,
    status: 'healthy'
  },
  computed: {
    summary() {
      return this.users + ' users, ' + this.status;
    }
  },
  refresh() {
    this.users = 1421;
    // Only the elements bound to 'users'
    // and 'summary' update. Everything
    // else on the page is untouched.
  }
})

One Reactivity Model. Everywhere.

Components, Stores, and Plugins all share the same reactive foundation. State, computed properties, and methods work identically no matter where they live. Learn it once, it works the same way in a UI component, a global store, or a framework plugin.

Other frameworks make you learn a different system for each layer. React components use hooks, but stores need Redux or Zustand, which are completely different APIs. Vue components use reactive data, but Pinia stores have their own patterns. Every layer is a new mental model.

In WildflowerJS, there's one model. A store is a component without a template. A plugin is an entity that extends the framework itself, adding directives, lifecycle hooks, and services. The same this.count++ triggers the same reactivity everywhere.

This unlocks patterns other frameworks can't express. A store can run headless physics simulations with tick(), feeding data into a component that renders it through a pool, all using the same reactive primitives, no glue code required.

// Component: reactive UI
wildflower.component('cart', {
  state: { items: [] },
  computed: {
    total() { return this.items.length; }
  }
})

// Store: global shared state
wildflower.store('user', {
  state: { name: '', role: 'guest' },
  computed: {
    isAdmin() { return this.role === 'admin'; }
  }
})

// Plugin: extends the framework
wildflower.plugin({
  name: 'notifications',
  state: { items: [], unreadCount: 0 },
  computed: {
    hasUnread() { return this.unreadCount > 0; }
  },
  add(msg) { this.items.push(msg); this.unreadCount++; }
})
// Access globally: wildflower.$notifications.add(...)

// Same state. Same computed. Same methods.

Data Pools

Every framework wraps collection items in reactive proxies, whether the item needs it or not. WildflowerJS gives you a choice: data-list for push reactivity (automatic), data-pool for pull reactivity (explicit control, zero proxy overhead).

Pools render plain objects with the same template syntax as lists. Mutate the object, call markDirty(), and only that item updates. Full CRUD, selection, bulk operations, all faster than the push-reactive path.

And because pools use pull-based rendering, they scale to simulations, games, particle systems, and data visualizations at native frame rate. Use cases that would choke a virtual DOM. No other framework has anything like this.

<div data-component="user-table">
  <tbody data-pool="users" data-key="id">
    <template>
      <tr>
        <td data-bind="name"></td>
        <td data-bind="status"
            data-bind-class="status === 'active'
              ? 'badge success'
              : 'badge inactive'"></td>
      </tr>
    </template>
  </tbody>
</div>
wildflower.component('user-table', {
  pools: { users: {} },

  init() {
    // Populate: plain objects, no proxies
    data.forEach(u => this.pools.users.add(u));
  },

  // Optional: add tick() and the same pool
  // renders every frame. Same template, same
  // data, different rendering frequency.
  // That's the only difference between a
  // display table and a particle system.
})

Built for AI-Assisted Development

Because WildflowerJS is standard HTML and JavaScript, AI code assistants already know how to write it. There's no custom syntax to hallucinate or compiler quirks to work around. The code an AI generates runs exactly as written, with no build step between generation and execution.

We go further. WildflowerJS ships an AI-optimized reference page with patterns, anti-patterns, and examples designed for code generation context windows. Our llms.txt file follows the llms.txt convention for machine-readable documentation.

And for structured app generation, our Universal App Manifest lets you describe an entire application as a JSON schema (components, state, computed properties, methods, templates) and have an AI generate the working code from the manifest, mediated through framework-specific idiom files.

You: "Build me a todo app with
WildflowerJS"

AI reads llms.txt or ai-assistant.html
     ↓
Generates standard HTML + JS
     ↓
<div data-component="todo-app">
  <input data-model="newItem">
  <button data-action="addItem">
    Add
  </button>
  <ul data-list="items">
    <template>
      <li data-bind="text"></li>
    </template>
  </ul>
</div>
     ↓
Open in your browser. It works, and you can read and understand the code.

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 - no custom syntax, no compilation
  • 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 - lite, core, spa, full, and ai bundles available

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, no immutability requirement, no 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: www/js/src/state/ProxyHandlers.js SET and DELETE traps. Tests: test-new/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 _handleStateChange notifies effects 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: www/js/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 are different by design. 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's no this.state, no this.stores, no automatic dep tracking on entity reads. This is a deliberate performance choice 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.

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.state.items));

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

// History state
wildflower.router.navigate('/results', {
    state: wildflower.toRaw(this.state.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

UAM handles this automatically. When using UAM with framework idiom files, the generated HTML follows this optimal structure.

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; }

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.

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();
});

UAM-generated pages wrap inline scripts automatically.

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.

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>

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 — Bootstrap uses data-bs-*, HTMX uses hx-*, 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 components in the pool 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.

<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
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.pool('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 are the key lever for per-frame animation: 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; no markDirty, no manual string assembly.

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.pool('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.

data-pool-action Event Delegation

Delegates events from pool items to component methods. Supports multiple declarations with CSS selector targeting. One DOM listener per event type, O(1) entity lookup.

<!-- Single action -->
<div data-pool="enemies" data-key="id" data-pool-action="click:onEnemyClick">

<!-- Multiple actions with selectors -->
<div data-pool="tasks" data-key="id"
     data-pool-action="click:.edit-btn:onEdit; click:.delete-btn:onDelete; mouseover:onHover">
    <template>
        <div class="task">
            <span data-bind="title"></span>
            <button class="edit-btn">Edit</button>
            <button class="delete-btn">Delete</button>
        </div>
    </template>
</div>
// Format: "method" | "event:method" | "event:.selector:method"
// Semicolons separate multiple declarations
// Selector-specific handlers take priority over catch-all on same event
onEdit(item, event) { /* clicked .edit-btn */ },
onDelete(item, event) { this.pools.tasks.remove(item.id); },
onHover(item, event) { /* mouseover anywhere in item */ }

pool.onChange Callback

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

const pool = this.pool('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 intentionally 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: www/js/src/rendering/PoolRenderer.js (entity-only ctx, no component-state merge); www/js/src/rendering/ListRenderer.js + ListItemBinding.js + BindingResolver.js (full scope). Tests: test-new/pool-entity-computed.test.js, test-new/pool-props.test.js, test-new/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: www/js/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 or page-load orchestration, use wildflower.whenSettled(): a promise that resolves after the framework's full async chain (microtask flush + setTimeout(0) + rAF + final microtask) so all pending updates are applied.

Source: www/js/src/core/FrameworkInit.js (wildflower:ready), www/js/src/components/ComponentScanning.js + ComponentLifecycle.js (componentInit), www/js/src/features/ErrorBoundaries.js (componentDestroy), www/js/src/state/StoreManager.js (store-ready), www/js/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.

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">

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 is exhaustive as of the current build; cases not on it have explicit error or warning paths.

SymptomCauseFix
An element with data-component="foo" stays inert: no init, no event handlers, no warning. 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.state.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]; }.

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;

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: Mirror in the Tick

pool.length, pool.size, and pool.items.length are plain JavaScript getters by design. They bypass the reactive proxy so that adding and removing entities at 60fps doesn't pay reactivity overhead on every change. The idiomatic pattern: pools live inside components with a tick(), and you mirror the aggregate count to reactive state inside that tick, gated by an inequality check.

// CORRECT - mirror pool aggregates to state inside the tick
wildflower.component('game', {
    state: { entityCount: 0 },
    pools: { enemies: {}, projectiles: {}, loots: {} },

    tick(dt) {
        // ... game update logic ...

        const total = this.pool('enemies').length
                    + this.pool('projectiles').length
                    + this.pool('loots').length;
        if (this.entityCount !== total) this.entityCount = total;
    }
});

The gate matters. Without it, every frame writes to state.entityCount whether the count changed or not, and the reactive cascade fires 60 times per second. With it, the write is a single property assignment with a no-op short-circuit, and bindings only update when the number actually moves.

For per-frame display values that don't need reactivity at all (FPS, frame ms, a live counter on a HUD already inside a tick), write to the DOM directly via cached refs as shown in the previous section. pool.length is fine on that path because no observer is expecting to be notified.

The combination to avoid is a computed body that reads pool.length on a component without a tick. It will evaluate once against the empty pool, cache 0, and never re-run. In practice, pools and ticks travel together; if you don't have a tick, reach for a regular reactive array instead of a pool.

// WRONG - computed reads pool.length, evaluates once, never re-runs
wildflower.component('hud', {
    computed: {
        count() { return this.pool('enemies').length; }
    }
});

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 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 — just 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

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) { ... }) — or a zero-arg form that reads this.X against the row — 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, this.X resolves through the same ContextProxy as component methods: own props → computed → state → store. No special exceptions, no version-gated features.

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. `this.reactions` is the raw object on
        // the comment row; we project it to a chip array the inner data-list
        // can iterate. No need to pre-build this on every comment.
        reactionChips() {
            if (this.id === undefined) return []
            const c = this
            return ['👍','❤️','🚀'].map(emoji => {
                const users = (c.reactions && c.reactions[emoji]) || []
                return {
                    id: c.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.computed.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.computed.otherComputed(item) works for calling other item-level computeds
  • 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.state.shares = { ...this.state.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. Here's how to translate React patterns:

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. Here's the translation guide:

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. Here's how to translate:

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 (required when calling item-level computeds with arguments)

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.computed.X(item) (required) this.computed.inCartQty(item)
$entity.path in templates Not needed (auto-resolved) data-bind="$comp.name"