WildflowerJS for AI Code Assistants
A condensed reference designed specifically for AI assistants helping developers write WildflowerJS code.
https://www.wildflowerjs.com/llms.txt. This page is a quick-start subset.
WildflowerJS supports the llms.txt convention; the full machine-readable reference lives at the absolute URL above. If you are reading this page in isolation (downloaded copy, no base URL context), the absolute URL is the one to fetch.
Framework Overview
- No build step required - works directly in browser via script tag
- Standard HTML/JS only - the browser parses what you write, with nothing added to the language
- Attribute-based binding - uses HTML data attributes
- Proxy-based reactivity - automatic DOM updates on state change
- Full bundle - includes routing, SSR, and stores
- Multiple build variants - nano, mini-pool, mini, lite, core, spa, and full (see Choosing a Build below)
Choosing a Build
WildflowerJS ships seven builds with one API, so code written against a smaller build runs unchanged on any larger one. Every build includes components, stores (with localStorage persistence), computed properties, form validation, templates, error boundaries, and CSP-safe mode. Pick the smallest build whose features you need; when unsure, use full, which is always correct.
| Build | File | Adds over the previous tier | Size (brotli) |
|---|---|---|---|
| nano | wildflower.nano.min.js | the baseline above, without data-list or pools | 51 KB |
| mini-pool | wildflower.mini-pool.min.js | pools (per-frame entities), still no data-list | 55 KB |
| mini | wildflower.mini.min.js | data-list rendering (in place of pools) | 74 KB |
| lite | wildflower.lite.min.js | lists and pools together | 78 KB |
| core | wildflower.min.js | plugins, portals, transitions | 82 KB |
| spa | wildflower.spa.min.js | routing | 86 KB |
| full | wildflower.full.min.js | data-query, SSR | 99 KB |
CDN: https://cdn.jsdelivr.net/npm/wildflowerjs@1/dist/wildflower.<build>.min.js (for core the file is just wildflower.min.js). During development substitute .dev.js for .min.js to get diagnostic warnings with error codes; switch to .min.js for production.
Developing with the Dev Builds
Load the .dev.js build while building and testing. Dev builds diagnose mistakes in the console as they happen: each warning states the actual cause, often includes the corrected markup, and carries a stable WF-xxx error code linking to https://www.wildflowerjs.com/docs/error-codes?code=WF-xxx. If a page misbehaves (an empty list, a binding that never updates, an action that does not fire), read the console first, since the framework usually names the mistake and the fix.
console.log, console.info, and console.debug entirely. Debug output that must survive in a production build needs console.warn or console.error. Ship .min.js once the console is clean in dev.
Reactivity Rules
WildflowerJS reactivity is proxy-based. The proxy SET trap fires on every assignment to state at any nesting depth, and the corresponding effect re-runs. There is no Vue 2-style Vue.set requirement, immutability requirement, or object-identity rule.
✅ Triggers reactivity
this.count = 5this.user.name = 'Ana'this.shares.m5 = { ... }this.shares['m5'] = { ... }delete this.shares.m5this.items[3].priority = 'high'this.items.push(item)this.items.splice(1, 1)this.items[0] = newItemthis.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.stateobject reference (don't do this) - Mutating after the component has been destroyed
If a binding doesn't update on a direct mutation that should work, treat it as a binding bug, not a mutation issue. File it.
Source proof: src/state/reactive-graph/core.js, the set and deleteProperty traps of the reactive proxy. Tests: tests/direct-mutation.test.js exercises nested object property mutation, array index assignment, nested-object-in-array property mutation, and array methods. The proxy traps fire and the dependency graph marks the affected bindings dirty in every case.
The this.X Shortcut (Idiomatic)
Inside component methods and computeds, this.shares resolves to this.state.shares automatically. And this.savedCount resolves to a computed of the same name. The this.state / this.computed longhand also works, but the shortcut is the idiomatic form. Resolution order: own property → computed → state.
wildflower.component('cart', {
state: { items: [], discount: 0 },
computed: {
// Use this.items directly, not this.state.items
total() { return this.items.reduce((s, i) => s + i.price, 0); },
// Use this.total directly, not this.computed.total
finalPrice() { return this.total * (1 - this.discount); }
},
addItem(item) {
// Direct nested mutation: triggers reactivity
this.items.push(item);
}
});
Source: src/state/ContextProxy.js: "Makes this.count in methods resolve identically to data-bind="count" in templates." This applies inside the component definition's own methods/computeds, item-level computeds in lists, watcher callbacks, store methods, and lifecycle hooks. The longer this.state.X form remains valid and is sometimes useful for disambiguation when a state property name collides with a method name.
this.X shortcut described above apply to component / store / item-level-computed scope. Pool entities are plain objects with property descriptors, not proxies. There is no this.state, this.stores, or automatic dep tracking on entity reads. Plain descriptors keep per-frame updates cheap for high-frequency rendering (games, dashboards, simulations). For reactive per-item state in a regular list, use a data-list with item-level computeds (the section below). For pool-entity binding rules, see the data-pool section.
&&, ||,
a ternary, an early return) is never read, so it is never tracked, and the binding will not
re-evaluate when that value later changes. Nothing errors and nothing warns: the only symptom is an
update that does not happen, usually intermittently, depending on which branch ran first.
Read every field that could matter at the top of the computed, then branch.
// ❌ When `enabled` is false, `rules` is never read, so adding a rule
// changes nothing on screen. Looks like broken reactivity; is not.
computed: {
hidden(item) {
const f = this.stores.filters;
return f.enabled ? f.rules.some(r => r.match === item.kind) : false;
}
}
// ✅ Read both, then decide.
computed: {
hidden(item) {
const f = this.stores.filters;
const on = f.enabled; // always read, always tracked
const rules = f.rules; // always read, always tracked
return on ? rules.some(r => r.match === item.kind) : false;
}
}
Applies to component-level and item-level computeds alike, and to reads of state, props, stores and queries. Same rule in every runtime-proxy reactive system (Vue, Solid, MobX, Preact Signals). The full treatment, with the underlying mechanism, is on the Computed Properties page under "Eager dependency reading for conditional computeds".
$entity.path in markup, and subscribe + this.stores,
getStore() or getComponent() in JavaScript. Capturing another entity's state
object in a closure or a field bypasses the tracking proxy and the dependency is silently
lost: the value reads correctly once, and the update never fires afterwards. The framework
cannot detect this and cannot warn about it.
// ❌ Captured once. Later changes to the cart never reach the binding.
init() { this._cart = wildflower.getStore('cart').state; },
computed: { total() { return this._cart.total; } }
// ✅ Read through the tracking surface on every evaluation.
computed: { total() { return wildflower.getStore('cart').total; } }
Quick Reference: Reactivity
| Operation | Reactive? | Idiomatic form |
|---|---|---|
| Scalar reassignment | ✅ | this.count = 5 |
| Nested object property | ✅ | this.user.name = 'Ana' |
| Object map by key | ✅ | this.shares.m5 = obj or this.shares['m5'] = obj |
| Object map key delete | ✅ | delete this.shares.m5 |
| Array push / pop / splice | ✅ | this.items.push(x) |
| Array index assignment | ✅ | this.items[0] = x |
| Property of array item | ✅ | this.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:
indexedDBreads and writespostMessage(worker, iframe, window)BroadcastChannel.postMessageCache.put/Cache.addhistory.pushState/replaceStatestate objects
// IndexedDB
await store.put(wildflower.toRaw(this.items));
// postMessage to a worker
worker.postMessage({ payload: wildflower.toRaw(this.config) });
// History state (router = your wildflower.createRouter(...) instance)
router.navigate('/results', {
state: wildflower.toRaw(this.filters)
});
fetch with a JSON body works directly on a proxy; toRaw is only needed for structured-clone-using APIs. The snapshot is a one-time copy: re-call when a fresh copy is needed.
Optimal Page Structure
When generating HTML pages, always place scripts in <head> with defer for best Lighthouse scores:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="styles.css">
<!-- All scripts in head with defer -->
<script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<div data-component="my-app">
<!-- App content -->
</div>
</body>
</html>
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
DOMContentLoadedwrapper needed for external scripts - Results in ~50% faster First Contentful Paint and ~35% lower Total Blocking Time
<head> with defer, and do not inline your component definitions. If the app code must be inline, load the framework tag without defer, placed before the inline block. Browsers ignore defer on inline scripts, so a deferred framework tag followed by inline registrations runs the registrations first and the page dies with wildflower is not defined. Keeping defer and wrapping every inline registration in DOMContentLoaded also works; it is easier to get wrong.
Single self-contained file (inline app code): no defer on the framework tag.
<!-- No defer: the inline registrations below need `wildflower` to exist already -->
<script src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script>
wildflower.component('my-app', {
state: { count: 0 }
});
</script>
Preventing Flash of Unstyled Content
When using defer scripts, add this CSS to prevent modals from briefly flashing:
/* Hide cloaked elements until framework processes them */
[data-cloak] { display: none !important; }
Add the data-cloak attribute to elements that should be hidden until the framework initializes (e.g., data-show elements starting false, data-portal elements). The framework removes data-cloak after initialization. The !important is load-bearing. [data-cloak] has the same specificity as a single class, so any component rule that sets display would win the tie and the element would flash until the framework hides it.
data-cloak inside <template> elements (i.e., inside data-list templates). Template content is inert and invisible until the framework clones it, so there is no FOUC risk. Using data-cloak inside templates causes bugs with dynamically added list items.
Inline Scripts with Deferred Dependencies
If inline scripts depend on deferred libraries, wrap them in DOMContentLoaded:
document.addEventListener('DOMContentLoaded', function() {
// Deferred scripts are now loaded
externalLibrary.init();
});
Inlined Pages, Blob URLs, Single-File Distributions
When an entire HTML page (framework + app code) is inlined into a single file or fetched as a Blob URL, browsers can re-order parsing such that the defer ordering guarantee doesn't hold. Symptoms: wildflower is not defined, components never mount, "Component not registered" warnings.
Fix: wrap your wildflower.component(...) registrations in DOMContentLoaded:
document.addEventListener('DOMContentLoaded', () => {
wildflower.component('my-component', { /* ... */ });
// wildflower.scan() runs automatically after DOMContentLoaded
});
Normal page loads with separate <script defer> tags don't need this. The wrapper only applies to bundled / inlined / blob-URL distributions.
Strict CSP Environments
WildflowerJS runs under a strict Content Security Policy that forbids both unsafe-eval and unsafe-inline, and it does so with no build step: templates are real <template> elements and expressions fall back to a built-in AST parser. When generating pages for a CSP-constrained target (enterprise, extensions, government), follow three rules:
<!-- 1. Add data-csp-safe so the framework never attempts eval
(zero CSP violations and zero report-uri noise) -->
<script src="wildflower.min.js" data-csp-safe defer></script>
<!-- 2. App code in a same-origin file, never inline
(script-src 'self' blocks inline scripts) -->
<script src="app.js" defer></script>
3. Keep binding expressions CSP-parser-safe: the AST parser handles arithmetic, comparisons, logical operators, ternaries, and property access, but not method calls (str.toUpperCase()), template literals, or globals (Math, Date). Put that logic in computed properties instead. See Expressions for the full support table.
Core Attributes Reference
| Attribute | Purpose | Example |
|---|---|---|
data-component="name" |
Declares a component instance | <div data-component="counter"> |
data-bind="property" |
One-way text binding (textContent) | <span data-bind="count"></span> |
data-bind="computedName" |
Bind to computed property (no prefix needed) | <span data-bind="fullName"></span> |
data-bind-html="property" |
Bind as innerHTML (XSS risk with untrusted input, use setHtmlSanitizer()) |
<div data-bind-html="htmlContent"></div> |
data-bind-class="expression" |
Dynamic CSS classes (object or expression) | <div data-bind-class="{ active: isActive }"> or <div data-bind-class="isActive ? 'active' : ''"> |
data-bind-style="expression" |
Dynamic inline styles via object | <div data-bind-style="{ color: textColor }"> |
data-bind-attr="expression" |
Bind HTML attributes dynamically | <img data-bind-attr="{ src: imagePath, alt: description }" /> |
data-model="property" |
Two-way binding for inputs | <input data-model="username"> |
data-model-number |
Convert value to number | <input data-model="price" data-model-number> |
data-model-trim |
Trim whitespace from value | <input data-model="name" data-model-trim> |
data-model-lazy |
Update only on blur/change | <input data-model="email" data-model-lazy> |
data-action="method" |
Click handler (default) | <button data-action="save">Save</button> |
data-action="event:method" |
Specific event handler | <input data-action="input:search"> |
data-list="arrayProperty" |
Render array items | <ul data-list="items"> |
data-pool="poolName" |
Lightweight collection renderer (plain objects, no proxy overhead) | <div data-pool="enemies" data-key="id"> |
data-show="property" |
Show/hide via CSS (display: none) | <div data-show="isVisible"> |
data-render="property" |
Insert/remove from DOM entirely | <div data-render="showPanel"> |
data-portal="selector" |
Teleport content to another element | <div data-portal="body">Modal content</div> |
data-transition="name" |
CSS transitions on show/hide | <div data-show="visible" data-transition="fade"> |
data-external |
Preserve element during HTML updates | <div data-external data-component="live-chart"> |
data-prop-*="value" |
Pass props to child component | <div data-component="child" data-prop-user="currentUser"> |
data-props="{ ... }" |
Pass multiple props as object | <div data-component="child" data-props="{ title: heading, color: theme }"> |
data-use-template="name" |
Reference parent's named template | <div data-use-template="cardTemplate" data-with="user"> |
data-item-template="name" |
Define a named template for children | <template data-item-template="cardTemplate"> |
data-template-key="property" |
Select template variant by data value | <div data-component="viewer" data-template-key="viewType"> |
data-type="value" |
Template variant identifier (for data-template-key) |
<template data-type="card">...</template> |
data-query="name" |
Bind element to a declared external data source (Full build). Template child = list; bare subtree = record | <ul data-query="products"><template>...</template></ul> |
data-seed='{"json":1}' |
SSR: JSON attribute carrying fields the page does not display (row ids, machine values) into hydrated state | <tr data-seed='{"id":811}'> |
Note: All data-* attributes also support a data-wf-* prefix (e.g., data-wf-bind, data-wf-action). When both forms are present on the same element the data-wf-* form wins. In practice the major peer libraries don't collide, since Bootstrap uses data-bs-*, HTMX uses hx-*, and Alpine uses x-*, so the unprefixed form is fine for almost every project. Reach for data-wf-* only when an external library or a bespoke integration legitimately uses the bare names (data-bind, data-show, data-action, etc.) for its own purposes. To enforce data-wf-* exclusively, set data-wf-prefix="true" on the framework script tag (toggles the useWfPrefixOnly config in www/js/src/core/Bootstrap.js).
data-show vs data-render
Both conditionally display content, but they work differently:
| Attribute | Behavior | When to Use |
|---|---|---|
data-show |
Toggles display: none. Element remains in DOM. |
Frequent toggling, preserve form state, simple show/hide |
data-render |
Inserts/removes from DOM entirely. Destroys nested components. | Complex conditional content, memory efficiency, expensive widgets |
<!-- data-show: Toggle visibility, keep element in DOM -->
<div data-show="isLoggedIn">Welcome back!</div>
<!-- data-render: Insert/remove from DOM completely -->
<div data-render="showExpensiveWidget">
<div data-component="heavy-chart">...</div>
</div>
Component Definition Pattern
wildflower.component('component-name', {
// Reactive state object
state: {
count: 0,
items: [],
user: { name: '', email: '' }
},
// Computed properties (cached, auto-update on dependency change)
computed: {
doubleCount() {
return this.count * 2;
},
itemCount() {
return this.items.length;
}
},
// Lifecycle: called after component mounts
init() {
console.log('Component initialized');
},
// Lifecycle: called before component destroys
destroy() {
// Cleanup code here
},
// Action methods (called from data-action)
increment() {
this.count++;
},
// Actions receive (event, element, context)
handleClick(event, element) {
event.preventDefault();
// element is the clicked DOM element
},
// List item actions receive item data in details parameter
removeItem(event, element, details) {
const index = details.index;
this.items.splice(index, 1);
}
});
Lifecycle Constraints
Reserved method names. Do NOT use these names for action handlers or helpers; the framework drives them on its own schedule:
init,beforeInit: called once during mountdestroy,beforeDestroy: called once during teardownonUpdate,beforeUpdate: called on every reactive updateonError: called when a method throwstick: called every animation frame for any component that defines it, pools or not. It is also the only loop to write: a simulation that runs until it settles, a clock, a queue drainer all belong intick(dt)gated by a state flag, never in a hand-rolledrequestAnimationFrameorsetIntervalloop
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.
markDirty() is an animation pool: every frame repaints every entity, which is what tick(dt) workloads want. The first markDirty() call converts the pool to a data pool for the rest of its life, and from then on only marked entities repaint. Do not mix them. Adding a markDirty() call to a tick()-driven pool silently freezes every entity you did not mark.
<div data-pool="enemies" data-key="id">
<template>
<div data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }">
<span data-bind="label"></span>
</div>
</template>
</div>
// Declare pools alongside state (preferred):
wildflower.component('game', {
pools: {
enemies: {
onAdd: 'onSpawn', // lifecycle hooks (string ref or inline fn)
onRemove: 'onDeath', // fires before individual removal
onClear: 'onWaveEnd' // bulk clear (skips onRemove)
},
projectiles: {} // no hooks needed
}
})
// Pool API (via this.pools.name):
const pool = this.pools.enemies;
// Array-like API (preferred: reads like native JS arrays):
pool.push({ id: 1, x: 100, y: 200, imgSrc: 'orc.png', label: 'Orc' });
pool.push(arrayOfItems); // bulk add via DocumentFragment (single DOM op)
pool.length; // current count
for (const e of pool) { /* ... */ } // iterate via Symbol.iterator
pool.filter(e => e.alive); // returns plain array
pool.map(e => e.id);
pool.find(e => e.id === 42);
pool.forEach(fn); pool.some(fn); pool.every(fn); pool.reduce(fn, init);
// Key-based ops (pool uses swap-with-last, so no positional pop/splice):
pool.remove(1); // remove by key
pool.get(1); // get entity by key (or undefined)
pool.update(1, { x: 200 }); // patch properties (sync for static pools)
pool.clear(); // remove all
pool.getElement(1); // get DOM element by key
pool.swap(key1, key2); // swap two items' DOM positions
pool.markDirty(key); // re-evaluate bindings for one entity (see mode note)
pool.props; // shared props object (parent-injected data)
// Long-form aliases (identical semantics): pool.add(obj), pool.size, pool.items
// DOM updates happen automatically on next rAF
// Imperative form also works: this.getPool('enemies')
Entity Shape: state defaults, computed properties, methods
Declare entity: { state, computed, ... } on a pool to give every entity a shared shape. Entity computed properties derive transform/background/class strings from raw inputs (x, y, z), and data-bind-* reads them on flush. In tick(dt) you only mutate the inputs. There is no markDirty call and no manual string assembly.
entity.computed. Entity computeds are uncached by contract: each one re-evaluates on every read of every flush, measured at roughly 60µs per entity per flush. At 200 entities that is about 12ms of a 16.7ms frame, before the simulation does any work of its own, so development builds warn at that size (WF-411). Use entity computeds for pools in the tens. For a per-frame pool in the hundreds or larger, write the derived value as a plain data field in tick(dt) instead, or mark non-animating entities with data-pool-static. A boids demo lost about 3ms per frame at 800 entities by moving to entity.computed and the change was reverted.pools: {
particles: {
entity: {
state: { hp: 100, vx: 0, vy: 0 }, // defaults merged into new entities (spawn values win)
computed: {
tf() { return `translate(${this.x}px,${this.y}px)`; },
bg() { return COLOR_LUT[Math.round(this.z * 4) & 255]; }
},
kill() { this.hp = 0; } // entity methods, callable as entity.kill()
}
}
}
// Template: <div data-bind-style="{ transform: tf, background: bg }"></div>
tick(dt) {
for (const p of this.pools.particles) {
p.x += p.vx * dt;
p.y += p.vy * dt;
}
// tf and bg auto-recompute from the mutated inputs, no markDirty needed.
}
Entity state is a shallow merge. Spawn values override template keys. Entity computed accessors are installed per-entity and skipped when the spawn already has an own property of the same name. Any non-reserved function at the top of the entity block becomes a method callable as entity.methodName().
Static Pools, Bulk Add, and Pool Props
// Static pool: add data-pool-static (boolean) to skip rAF flush loop.
// Items render synchronously on add()/update(). Zero idle CPU cost.
// HTML: <div data-pool="users" data-key="id" data-pool-static>
// Bulk add: pass an array, single DOM operation via DocumentFragment
this.pools.users.add(data); // data is an array of objects
// Pool props: shared data available to all items via `props.` prefix
pools: {
boids: {
props: { shape: 'arrow', theme: 'dark' }
}
}
// Template: data-bind-class="className + ' ' + props.shape"
// Update: this.pools.boids.props.shape = 'circle'; (all items update on next flush)
tick(dt) Lifecycle Hook
Components with a tick method get called once per animation frame. The framework manages the rAF loop automatically. No manual setup or teardown. tick runs BEFORE pool flush, so entity mutations are visible in the DOM on the same frame.
wildflower.component('game', {
init() { this._pool = this.pools.enemies; },
// Called automatically each frame. dt = ms since last frame (clamped to 250ms).
tick(dt) {
for (const e of this._pool.items) {
e.x += e.vx * dt;
e.y += e.vy * dt;
}
}
// No destroy() needed: framework cleans up the rAF loop
});
tick(dt) also receives now (performance.now()) as a second argument. Works with or without pools, useful for canvas, Three.js, or any per-frame logic.
Run / stop loops use tick too. A model that runs until it settles, a clock, a queue drainer: define tick(dt) on the component and gate it with a state flag. The framework's loop is always running; the flag is the switch. Buttons toggle the flag and a stop condition clears it. For a slower cadence than one step per frame, accumulate dt in a plain _-prefixed field and step when it crosses your interval.
wildflower.component('schelling', {
state: { cells: [], round: 0, running: false },
step() {
// Work on a plain copy, write the reactive array ONCE at the end.
// In-place writes (this.cells[i] = ...) inside the loop would trigger a
// re-render per write and read every neighbour through the reactive facade.
const cells = this.cells.slice();
/* ...compute one round on `cells`... */
this.cells = cells;
this.round++;
},
toggleRun() { this.running = !this.running; },
tick(dt) {
if (!this.running) return;
this._acc = (this._acc || 0) + dt; // optional pacing; non-reactive field
if (this._acc < 100) return; // one round every 100 ms
this._acc = 0;
this.step();
if (this.allHappy) this.running = false; // stop condition
}
});
Hot loops over a large state array (dev warning WF-216). A step that reads a big reactive array thousands of times, every cell checking its neighbours, should read from a plain local copy and write the result back once: const cells = this.cells.slice(); ...compute next...; this.cells = next;. Reads through this.cells[i] inside the hot loop go through the reactive facade on every access; one write at the end is all the DOM needs.
tick() is guaranteed. Once the component initializes, tick(dt) fires every animation frame, so do not add a requestAnimationFrame loop as a fallback or watchdog beside it. A hand-rolled rAF loop next to tick() double-steps the simulation, and no fallback condition exists in which tick() silently fails to run.
Pool Item Events
Pool items use data-action, the same attribute as everywhere else. There is no data-pool-action. Inside the template, the handler receives the entity first and the DOM event second.
<div data-pool="tasks" data-key="id">
<template>
<div class="task">
<span data-bind="title"></span>
<button class="edit-btn" data-action="onEdit">Edit</button>
<button class="delete-btn" data-action="onDelete">Delete</button>
</div>
</template>
</div>
// Event name in front of the method selects the event; the default is click
onEdit(item, event) { /* the Edit button of this entity */ },
onDelete(item, event) { this.pools.tasks.remove(item.id); }
For one listener over the whole collection, put data-action on the pool container and call this.getItemFromEvent(event) to resolve the entity. It returns { item, element }, or null when the event landed on the container itself. event.target.closest('.edit-btn') tells the handler which sub-element was hit.
pool.onChange Callback
Fires synchronously on add(), remove(), and clear(). Pool size is already updated when the callback fires.
const pool = this.pools.enemies;
pool.onChange = (p) => {
document.getElementById('count').textContent = p.size;
};
data-pool vs data-list
data-list |
data-pool |
|
|---|---|---|
| Items | Reactive proxy objects | Plain JS objects |
| Updates | Automatic on state mutation | Batched via rAF loop |
| Template scope | Full component context (state, computed, stores) | Entity properties only |
| Use case | Interactive collections (forms, inline editing, two-way binding) | Performance-sensitive CRUD, real-time data, large datasets, per-frame animation |
| Overhead | Proxy per item | Zero proxy overhead |
data-bind, data-bind-style, data-bind-attr, data-bind-class, data-show. Resolve against entity properties and props.* (shared pool props).
props.* (parent-injected shared data), but NOT component state, computed properties, or store values. data-key defaults to id. Pools are automatically cleaned up on component destroy.
data-list vs data-pool Template Scope
The two list systems expose different scopes inside their templates. Pools narrow to entity + shared props for performance; lists give you the full component scope.
| Available inside <template> | data-list | data-pool | Notes |
|---|---|---|---|
| Item / entity properties | ✅ | ✅ | Both: data-bind="name" resolves to item.name / entity.name |
| Component state | ✅ | ❌ | Pools intentionally exclude: avoids per-entity reactive closure |
| Zero-arg component computeds | ✅ | ❌ | Same 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/A | Pools 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 methods | Entity methods (preferred), then component fallback | Pool entities can declare their own methods that win over component methods of the same name |
Source: src/rendering/PoolRenderer.js (entity-only ctx, no component-state merge); src/rendering/ListRenderer.js + ListItemBinding.js + src/core/BindingResolver.js (full scope). Tests: tests/pool-entity-computed.test.js, tests/pool-props.test.js, tests/list-item-context.test.js.
data-bind-class Syntax
The data-bind-class attribute supports object syntax (preferred for toggling) and expression syntax (for building class strings):
<!-- Object syntax: toggle classes by boolean (preferred) -->
<div data-bind-class="{ active: isActive }">
<div data-bind-class="{ error: hasError, warning: hasWarning }">
<div data-bind-class="{ selected: id === selectedId }">
<!-- Expression syntax: returns a class name string -->
<div data-bind-class="isActive ? 'active' : 'inactive'">
<div data-bind-class="isError ? 'alert alert-danger' : 'alert alert-success'">
<!-- Both work in list items -->
<ul data-list="items">
<template>
<li data-bind-class="{ done: completed }">
<span data-bind="text"></span>
</li>
</template>
</ul>
Cross-Component Communication
Use the $ accessor in templates to read another component's state or computed properties:
<!-- Read another component's state directly in HTML -->
<span data-bind="$theme-manager.mode"></span>
<div data-show="$nav-manager.menuOpen">Navigation</div>
<span data-bind="$user-profile.fullName"></span>
// In computed properties: automatic dependency tracking
wildflower.component('observer', {
computed: {
themeMode() {
const theme = wildflower.getComponent('theme-manager');
return theme ? theme.mode : 'light';
}
}
});
$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.pathin templates,this.stores.Xin methods. Reactive everywhere; multiple readers get automatic dependency tracking. - Reading another component's state in a template → use the
$component.pathshorthand (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 aContextProxyso reads register as deps). - Telling another component to do something (one-shot method call, no shared state) →
wildflower.getComponent(name).method(). Don't use it to push state. That's what stores are for.
Source: src/features/ErrorBoundaries.js. getComponent(name) returns a ContextProxy when called inside a computed (for dependency tracking), or the raw context when called from anywhere else; returns null if no instance with that name exists.
Component Mount / Destroy Events
External scripts (analytics, devtools, page-level orchestration, tests) can listen for framework lifecycle events on document. Use wildflower:ready as the canonical "framework is up" signal. It fires once per page after all initial scanning and mounting completes.
| Event | When it fires | Detail |
|---|---|---|
wildflower:ready |
Framework initialization complete (all initial components scanned and mounted). Fires once per page. | { instance } |
wildflower:componentInit |
Each component instance after its init() runs. |
{ instance, context } |
wildflower:componentDestroy |
Component instance about to be torn down. | { instance, context } |
wildflower:store-ready |
Each store after creation; lets components delay setup until a store exists. | { storeName } |
// External script: wait for framework, then probe state
document.addEventListener('wildflower:ready', () => {
console.log('WildflowerJS ready');
});
// Per-component readiness
document.addEventListener('wildflower:componentInit', (e) => {
if (e.detail.instance.name === 'main-panel') {
// run analytics, attach external observers, etc.
}
});
For deterministic sequencing in tests, tooling, or page-load orchestration, use wildflower.whenSettled(): a promise that resolves once the framework's scheduled work has drained (the microtask effect flush, the deferred init window, the rAF render, and the final effect microtask), so all pending updates are applied, including components whose init injected further components. It is a boundary tool for code that observes the page from outside (tests, tooling, interop with non-reactive libraries); idiomatic components never need it, because bindings keep the DOM current and onUpdate covers the changed-now-act case. The pool rAF loop is excluded by contract: pools animate plain objects outside the reactive graph, so a running pool animation neither blocks settling nor counts as pending work.
Source: src/core/FrameworkInit.js (wildflower:ready), src/components/ComponentScanning.js + ComponentLifecycle.js (componentInit), src/features/ErrorBoundaries.js (componentDestroy), src/state/StoreManager.js (store-ready), src/core/WildflowerCore.js (whenSettled).
Store Pattern (Global State)
// Create a store
wildflower.store('user', {
state: { name: 'Guest', isLoggedIn: false },
computed: {
greeting() { return 'Hello, ' + this.name; }
},
login(name) {
this.name = name;
this.isLoggedIn = true;
}
});
// ✅ BEST: Use $ directly in HTML, no computed wrappers needed
// <span data-bind="$user.name"></span>
// <span data-bind="$user.greeting"></span>
// ✅ ALSO GOOD: Declarative subscription with this.stores
// (use when you need store access in JS methods)
wildflower.component('header', {
subscribe: {
user: ['name', 'isLoggedIn']
},
// Use this.stores in methods
refreshUser() {
this.stores.user.refreshProfile();
}
});
// With onStoreUpdate() for side effects
wildflower.component('login-form', {
state: { username: '' },
subscribe: {
user: ['isLoggedIn']
},
onStoreUpdate(storeName, path, newValue, oldValue) {
if (storeName === 'user' && path === 'isLoggedIn' && newValue) {
this.showWelcomeMessage();
}
},
handleLogin() {
this.stores.user.login(this.username);
}
});
subscribe: {} block, this.stores is automatically available with references to all subscribed stores. Use it in computed properties, methods, and lifecycle hooks.
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`);
});
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();
}
}
});
subscribe + this.stores with computed properties for clean, reactive store-backed lists. Use $entityName.path in HTML for simple cases without transformation. Works for stores, components, and plugins.
Async Computed Properties
A computed may return a promise. The framework awaits it and pushes the resolved value into the bindings. There is no separate API and no await at the call site: the binding reads the value, not the promise. Components, stores, and plugins all behave the same way.
// ✅ CORRECT: return the promise. An async function works identically.
wildflower.component('user-panel', {
state: { userId: 1 },
computed: {
user() {
const id = this.userId; // read reactive inputs FIRST
return fetch('/api/users/' + id).then(r => r.json());
}
},
onError(error, context) { /* rejections arrive here */ }
});
Read every reactive input before the first await or .then(). Dependency tracking is synchronous, so reads inside a callback or after an await are not tracked and the computed will not re-run when they change. Pull them into locals up front, then build the request from those locals.
wildflower.component('cart', {
state: { items: [] },
computed: {
// ✅ CORRECT: read reactive values FIRST, then return the promise
shippingQuote() {
const items = this.items; // synchronous read = tracked
return fetch('/api/shipping-quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items })
}).then(r => r.json()).then(q => q.total);
}
}
});
// ❌ WRONG: reading state inside .then() or after await. Those reads happen
// after the run and are NOT tracked, so the computed never re-runs.
// return fetch('/api/quote').then(() => this.items.length);
There is no loading flag, and you must not invent one. The absence of the value IS the signal. On the first evaluation a binding reads undefined, so gate the first-load state with data-show="!user". During a later refresh the binding keeps showing the previous value rather than blanking.
<p data-show="!user">Loading…</p>
<div data-show="user"><h5 data-bind="user.name"></h5></div>
Supersession is built in. If a dependency changes while a request is in flight, the computed re-runs and starts a new one. The last call wins, and an earlier response arriving late is discarded silently, so bindings never go backwards. Do not add cancellation or debounce for correctness; that machinery already exists. A rejected promise leaves the binding at undefined and sends the error to the entity's onError hook.
label(item), runs through the list renderer rather than the reactive graph, so it does no async tracking. A promise returned there binds as text, and a thousand rows issue a thousand uncoordinated requests. Dev builds warn with WF-235. Fetch the collection once in a component or store computed instead, then read from it per row.
computed: {
// ❌ WRONG: one request per row, and dev builds warn (WF-235)
avatar(item) { return fetch('/api/avatars/' + item.id).then(r => r.json()); },
// ✅ CORRECT: one request for the set, rows read from the result
avatars() { return fetch('/api/avatars').then(r => r.json()); }
}
data-query? Use an async computed for a derived value the component owns. Use data-query (below) for external data the app displays and writes back: it adds refresh policies, first-load and error state, optimistic saves with rollback, and reload persistence, none of which an async computed provides.
Live External Data (data-query)
data-query binds an element to a named external data source and keeps it current. Use it for data that lives OUTSIDE components: a server endpoint, a feed. For server data a component will render, it is THE intended pattern, covering loading and error states, refresh on demand, saves with rollback, and consistent updates come free. Do not fetch in init() and hand-roll loading/saving state for server data; reach for a query first. Plain fetch() remains fine for one-shot calls that render nothing.
[data-query] container must sit inside a component. Queries bind during component binding, so a container outside every data-component renders nothing (dev builds warn: WF-963). A page with no other component needs only an empty shell: wildflower.component('shell', {}) + <div data-component="shell">…</div> around the query markup.
// ✅ CORRECT: declare globally (like stores), render via markup
wildflower.query('products', {
from: '/api/products.json', // URL string, or a function returning data
key: 'id', // row identity: refreshes patch rows in place
refresh: ['focus', 'etag:60'],// when to re-fetch (seconds, tab focus, etc.)
retry: 3 // re-run a failed fetch (1 s, 2 s, 4 s); default 0
});
Retry is one number, and it defaults to zero. retry: N re-runs a failed fetch up to N times on a fixed doubling curve (1 s, 2 s, 4 s, capped at 30 s), with no policy object to configure. Without it a flaky endpoint shows error on the first failure, so declare it for any source that can answer 500 a few times before succeeding. While the ladder runs the query holds its state: rows stay, error stays unset, the loading or stale flag persists, and any success resets the count. Network failures and 5xx retry; most 4xx are final (401, 408 and 429 are the exceptions).
<!-- List shape: template child. The engine renders and patches rows. -->
<ul data-query="products">
<template><li><span data-bind="name"></span> — <span data-bind="price"></span></li></template>
</ul>
<!-- Record shape: bare subtree, plain paths bind to the single result -->
<article data-query="currentUser">
<h2 data-bind="name"></h2>
</article>
<!-- Query state binds via $name.* anywhere in the component -->
<p data-show="$products.isLoading">Loading…</p>
<p data-show="$products.error">Failed</p>
<span data-bind="$products.count"></span>
The refresh ladder: 'once' (default) · a number (poll seconds, SECONDS not ms) · 'etag:N' (conditional GET) · 'focus' (tab regains focus) · 'reconnect' · 'sse' (EventSource; JSON message = new data, empty message = re-fetch signal). Combine in an array. When the server pushes, the 'sse' rung IS the wiring, so never construct new EventSource(...) + listeners + state assignment yourself, and never pair a plain fetch() with your own stream: refresh: ['sse'] is that entire block.
'sse' needs the stream's URL. The rung opens an EventSource at stream: when declared, otherwise at from. A JSON endpoint is not an event stream, and the browser rejects it with a MIME-type error, so whenever the data URL and the stream URL differ, which is the usual case, declare both:
wildflower.query('tickets', {
from: '/api/tickets.json', // the data
refresh: ['sse'], // stay live from the server's push
stream: '/api/ticket-stream', // the EventSource URL (omit only when `from` IS the stream)
key: 'id'
});
isLoading is first-load-only. It is true only while the query has no usable data. Later refreshes never blank the list; they set isStale instead. Bind $name.isStale for a "syncing" hint; never build your own first-load flag.
// ✅ CORRECT (primary): declare `to` on the query, save with write().
// The change lands on screen immediately; if the server rejects, exactly
// the written fields roll back. Do NOT hand-roll optimistic updates
// (apply + refetch + undo-on-error) — write() IS that machinery.
wildflower.query('tasks', {
from: '/api/tasks',
key: 'id',
deleted: 'removed', // tombstone field, used for deletes
to: '/api/tasks/:id', // update at PATCH, delete at DELETE
body: (item) => item, // REQUIRED, or no body is sent
confirmation: (d) => d.task, // omit to refetch instead
create: { url: '/api/tasks', method: 'POST',
body: (item) => item, confirmation: (d) => d.task },
headers: () => ({ Authorization: 'Token ' + auth.token })
});
// A partial object: the key plus ONLY the fields that changed.
wildflower.getQuery('tasks').write({ id: 42, done: true });
// Adds: create() mints the temp key; the server's record supplies the real one.
wildflower.getQuery('tasks').create({ title: 'New task' });
// Deletes: write the declared tombstone field; the row leaves immediately.
// `deleted:` is REQUIRED for the row to go — including for a named delete
// in a `to` map. Without it the request still departs and the row stays on
// screen until the next sync (WF-975), so the button looks broken.
wildflower.getQuery('tasks').write({ id: 42, removed: true });
// Operations that are neither: name them in a `to` MAP and call by name.
// to: { favorite: { url: '/api/tasks/:id/star', method: 'POST' } }
// wildflower.getQuery('tasks').write('favorite', { id: 42, starred: true });
// `to` also still takes a FUNCTION, for transports the above cannot express.
// write() returns a promise: await to sequence, .catch on rejection
// (the rollback has already happened).
// $tasks.pendingWrites counts unsettled writes reactively — bind a
// saving indicator: Saving…
// Page unload CANCELS in-flight writes (browser behavior): await write()
// before navigating, or use keepalive: true in your to() fetch for
// writes that must survive unload (64KB body cap).
// ✅ CORRECT (fallback, transport you own end to end): act, then invalidate
wildflower.component('product-form', {
state: { draft: {} },
async addProduct() {
await fetch('/api/products', { method: 'POST', body: JSON.stringify(this.draft) });
wildflower.getQuery('products').invalidate(); // conditional re-sync
}
});
// ❌ WRONG: writing a query's store directly. The next sync overwrites it
// (dev builds warn: WF-950). Query stores are engine-owned.
wildflower.getQuery('products').rows = [...]; // do not do this
// ❌ WRONG: invented API names. None of these exist:
wildflower.createQuery(...); wildflower.useQuery(...); this.queries.products;
wildflower.queryWrite(...); wildflower.mutate(...);
Several views of the same entity: each query keeps its own copy. Converge siblings once a write settles: await wildflower.getQuery('order-detail').write({ id: 42, status: 'shipped' }); await wildflower.invalidateQueries('orders', 'dashboard-counts');
Reload persistence: persist: true on the declaration keeps the last confirmed rows in the browser, so the next page load paints them instantly, then revalidates. Never hand-roll localStorage caching for query data. Only confirmed server truth is stored (optimistic values never touch disk); wildflower.clearPersisted() removes every snapshot (call at logout).
SSR Handoff (data-ssr + data-query)
Inside data-ssr="true", the server-rendered DOM is the seed: the query parses its starting rows back out of the markup it adopts, using the same data-bind fields (data-type coerces numbers/booleans). There is no state blob and no re-render; the first fetch is a stale refresh of content the user is already reading. Fields the page does not display (usually the row key, or a machine value the display dresses up, like $1,250 for 1250) are carried in a data-seed JSON attribute on the row, which wins the merge at adoption:
<div data-component="warehouse" data-ssr="true">
<tbody data-query="inventory">
<template><tr><td data-bind="sku"></td><td data-bind="stock" data-type="number"></td></tr></template>
<tr data-seed='{"id":811}'><td data-bind="sku">AX-100</td><td data-bind="stock" data-type="number">45</td></tr>
</tbody>
</div>
The component declares no data for the query, because the query owns its store (declare wildflower.query('inventory', ...) and the rows live in it, never in component state). A data-query element must sit inside a component to activate.
Plugin Access Pattern
Plugins use the same $ accessor as stores and components:
// Register a plugin with reactive state
wildflower.plugin({
name: 'auth',
state: {
isLoggedIn: false,
user: null
},
login(user) {
this.isLoggedIn = true;
this.user = user;
},
logout() {
this.isLoggedIn = false;
this.user = null;
}
});
<!-- Access plugin state in templates via $ -->
<div data-show="$auth.isLoggedIn">Welcome, <span data-bind="$auth.user.name"></span></div>
<div data-show="!$auth.isLoggedIn">Please log in</div>
// In computed properties: automatic dependency tracking
wildflower.component('auth-guard', {
computed: {
isAuthenticated() {
const auth = wildflower['$auth'];
return auth ? auth.isLoggedIn : false;
}
}
});
// In methods (for mutations)
wildflower.component('login-form', {
handleLogin() {
wildflower['$auth'].login({ name: this.username });
}
});
$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
| Category | Methods |
|---|---|
| Selection | this.$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) |
.el to get the raw DOM element for library initialization:
const el = this.$el('.chart-container').el;
new Chart(el, { type: 'bar', data: chartData });
.parent(), .closest()) cannot escape the component element. This prevents accidental manipulation of parent components.
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>
<template> tags. They are preserved automatically during swaps.
data-template-keygoes on thedata-componentordata-listelement- An untyped
<template>acts as the default fallback - Component state, computed properties, and non-template DOM are preserved during swaps
- Nested components inside templates are destroyed/created on swap; outside templates they persist
- Computed properties can be used as the template key value
Event Handling Variations
<!-- Default click -->
<button data-action="handleClick">Click</button>
<!-- Specific events -->
<input data-action="input:onInput">
<input data-action="change:onChange">
<input data-action="keyup:onKeyUp">
<input data-action="keydown:onKeyDown">
<div data-action="mouseover:onHover">
<!-- Multiple events on same element -->
<input data-action="focus:onFocus blur:onBlur">
<!-- With debounce (wait for pause in typing) -->
<input data-action="input:search" data-event-debounce="300">
<!-- With throttle (limit frequency) -->
<button data-action="save" data-event-throttle="1000">
SPA Routing (spa and full builds)
wildflower.createRouter gives client-side routing with history integration, params, and guards:
const nav = wildflower.store('nav', { state: { page: 'home' } });
const router = wildflower.createRouter({
mode: 'hash', // '#/path' URLs — works from any server or a local file.
// Use 'history' for clean /path URLs (needs server rewrites).
routes: [
{ path: '/', name: 'home', handler: () => wildflower.getStore('nav').page = 'home' },
{ path: '/products', name: 'products', handler: () => wildflower.getStore('nav').page = 'products' },
{ path: '/users/:id', name: 'user', handler: ({ params }) => loadUser(params.id) }
]
});
router.navigate('/products'); // programmatic
router.navigate('/search', { query: { q: 'hello' } }); // with query params
The standard view pattern: each route's handler sets a store field, and each view section binds to it, as in <section data-show="$nav.page === 'home'">. In hash mode, plain <a href="#/products"> links navigate natively; the back and forward buttons work with no extra code. Components may define an onRouteChange(to, from) method, called automatically on every navigation (registered and cleaned up with the component; to.params / to.query carry the route data).
Guards: router.beforeEach(({ to, from }) => ...) runs before every navigation. Return true to allow, false to cancel, or a path string to redirect. Per-route: pass beforeEnter (same contract) and meta: {...} in the route definition. router.afterEach(({ to }) => ...) runs after.
Portals (Teleporting Content)
Render content elsewhere in the DOM while maintaining component ownership. This example teleports a modal to <body>; for the full modal recipe (and when a portal is actually required) see Modals & Dialogs.
<div data-component="modal-trigger">
<button data-action="showModal">Open Modal</button>
<!-- This content renders at document body, but actions bind to this component -->
<div data-portal="body" data-show="isModalOpen">
<div class="modal-backdrop">
<div class="modal-content">
<h2>Modal Title</h2>
<p data-bind="message"></p>
<button data-action="closeModal">Close</button>
</div>
</div>
</div>
</div>
Lifecycle Hooks
wildflower.component('example', {
state: { /* ... */ },
// Called before component mounts
beforeInit() {
// Setup that must happen before DOM bindings
},
// Called after component mounts and bindings are active
init() {
// Fetch data, setup subscriptions, etc.
},
// Called before state update propagates (no parameters - use watchers for specifics)
beforeUpdate() {
// Validate or transform data before update
},
// Called after state update propagates (no parameters - use watchers for specifics)
onUpdate() {
// React to state changes
},
// Called before component is destroyed
beforeDestroy() {
// Prepare for cleanup
},
// Called when component is destroyed
destroy() {
// Cleanup subscriptions, timers, etc.
},
// Error boundary
onError(error, info) {
console.error('Component error:', error);
// Handle gracefully
}
});
Watchers Pattern
wildflower.component('example', {
state: {
searchQuery: ''
},
watch: {
// Watch state property
'searchQuery': function(newValue, oldValue) {
console.log('Search changed:', newValue);
this.performSearch(newValue);
},
// Watch nested property
'user.profile.name': function(newValue) {
console.log('Name changed:', newValue);
}
}
});
Debugging Silent Failures
Most "broken bindings" turn out to be one of a small set of cases. AI authors don't have the human heuristic of "huh, that's weird"; knowing what to look for matters. The list below covers the cases known as of the current build. Most other mistakes have an explicit error or warning path, but treat that as the usual case rather than a guarantee: the entries here exist precisely because the framework cannot warn about them.
| Symptom | Cause | Fix |
|---|---|---|
An element with data-component="foo" stays inert. Nothing initializes, event handlers never bind, and the console stays quiet. |
The component name foo wasn't registered. The framework's component scanner skips unknown names with no console output. |
Confirm wildflower.component('foo', { ... }) ran before wildflower.scan() (or before the framework's auto-scan). With defer on both the framework and the app script, the auto-scan picks up registrations made during script parse. |
| A binding renders empty (the surrounding markup appears, the value doesn't). | The bound name resolves to undefined: a misspelled state property, a misspelled computed name, or an item property that doesn't exist on this row. |
Type the name into the browser console: wildflower.getComponent('your-component').yourBinding. undefined confirms the cause. The binding system intentionally skips DOM writes when a value resolves to undefined rather than rendering the literal string "undefined". |
| Item-level computed returns the wrong value or never updates. | The signature declares scope: fn(item, index, info) { ... } is item-level; fn() { ... } is component-level. A zero-arg computed referenced inside a list-template binding (data-bind="X" in a list <template>) is treated as component-level: same value for every row. If your binding renders the same value for every row when you expected per-row variation, your computed is missing its item parameter. |
Declare it as fn(item) { ... }. Read the row via item.X; component state via this.X; subscribed stores via this.stores.X. Use the bare name in any binding or inside any expression. |
| A binding evaluates an expression that calls a method, and the method runs but the DOM doesn't update. | The method mutates state inside a computed, which violates the "computeds are pure" contract. Or you put a side effect in a binding expression itself. | Move side effects into watch handlers or component methods. Bindings should only read. |
| Component definition has a syntax error and the page looks blank. | The component never registered, so its root element stays inert (same as the unregistered-name case). | Open the browser console. JavaScript syntax errors surface there. The framework also logs registration errors via its _log('error', ...) path when the definition shape is invalid (non-object, missing name, etc.). |
data-show / data-render always evaluates falsy on a list item. |
You're referencing a property that's not on the item. this.X from outside is component scope; inside a list item template the same name resolves against item properties first, then component scope, then list-context vars. |
If the value is a per-item lookup (e.g., is this row "shared" given a sibling lookup map keyed by item.id), declare an item-level computed: isShared(item) { return !!this.shares[item.id]; }. |
| A computed rendered the right value at first, then stopped updating when one particular field changed. Intermittent: some change sequences update, others do not. | The computed reads that field behind a short-circuit (&&, ||, a ternary, an early return) and the branch did not run, so the read never happened and the field was never tracked. A computed depends on what it actually read, not on what it could have read. |
Read every field that could matter at the top of the computed, then branch. See "Eager dependency reading for conditional computeds" on the Computed Properties page. |
| A value read from another store or component is correct on first render and never updates again. | A raw reference to the other entity's state was captured in a closure or a field, which bypasses the tracking proxy. The dependency is lost with no warning; the framework relies on the convention and cannot detect the bypass. | Reach across entities on every evaluation, through $entity.path, subscribe + this.stores, getStore() or getComponent(). Never cache the state object itself. |
| A watcher never fires, and nothing is logged. | this.watch = { ... } was assigned at runtime. Watchers are registered before init() runs, and watch is not part of the component context, so the assignment is ignored. |
Declare the watch block in the component definition, or call this.subscribe(path, fn) inside init() and keep the returned unsubscribe for destroy(). |
Cases that are not silent failures (have explicit handling):
data-bind-class="{ on: x }"returning an object literal: converted to a class string by_classResultToString; no"[object Object]"ever appears.data-cloakon nested elements: the framework recursively removes the attribute viaquerySelectorAll('[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
| Symptom | One-line fix |
|---|---|
Element with data-component stays inert | Confirm 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 value | Declare with a parameter: fn(item) { ... }. That's what makes it item-level |
List-item data-show always falsy on a derived value | Define an item-level computed: isX(item) { return ... } |
| Page renders blank, no warning | Open console. JS syntax errors in component definitions surface there |
CRITICAL: Anti-Patterns to Avoid
DO NOT use these patterns - they are NOT supported:
No Mustache/Handlebars Syntax
<!-- WRONG - This does NOT work -->
<span>{{count}}</span>
<span>{count}</span>
<span>${count}</span>
<!-- CORRECT -->
<span data-bind="count"></span>
No Magic Variables (Use Underscore Prefix)
<!-- WRONG - Dollar sign variables don't exist -->
<span>{{$index}}</span>
<span data-bind="$parent.name"></span>
<span data-bind="$root.data"></span>
<!-- CORRECT - Use underscore prefix for list context -->
<span data-bind="_index"></span>
<span data-bind="_length"></span>
<button data-bind-class="_first ? 'disabled' : ''">Up</button>
<button data-bind-class="_last ? 'disabled' : ''">Down</button>
<!-- In action handlers, use details object -->
<!-- details.index, details.length, details.first, details.last -->
No v-for, v-if, v-bind (Vue syntax)
<!-- WRONG -->
<div v-for="item in items">
<div v-if="isVisible">
<input v-model="name">
<!-- CORRECT -->
<div data-list="items">
<div data-show="isVisible">
<input data-model="name">
No JSX or React Patterns
// WRONG - No JSX
return <div>{this.state.count}</div>;
// WRONG - No functional components
const MyComponent = ({ count }) => <span>{count}</span>;
// CORRECT - Use component definition
wildflower.component('my-component', {
state: { count: 0 }
});
No Direct DOM Manipulation for Bound Elements
// WRONG - Bypasses reactivity
document.querySelector('[data-bind="count"]').textContent = 5;
// CORRECT - Update state, DOM updates automatically
this.count = 5;
No Watching List Items by Numeric Index
// WRONG - index paths go stale after splice/reorder (dev builds warn: WF-213)
watch: {
'items.0.name'(newVal) { /* fires for the wrong slot after a reorder */ }
}
this.subscribe('items.2.done', handler); // same problem
// CORRECT - watch the array, find items by id
watch: {
items() { this.recalculate(); }
},
computed: {
activeItem() { return this.items.find(i => i.id === this.selectedId); }
}
Reactivity tracks list items by object identity, not position. The index in a
change path reflects where the item sat when it was first observed; after a
splice, removal, or reorder, an index-path watcher misfires or goes silent.
Exception: Direct DOM Writes in Pool Components
In data-pool components with a tick(dt) method, direct DOM writes are correct for non-pool UI (FPS counters, HUD stats, score displays):
// CORRECT in tick(): direct write for display-only HUD
tick(dt) {
// Update entities
for (const e of this._pool.items) { e.x += e.vx * dt; }
// Direct DOM write for FPS counter
this._fpsTimer += dt;
if (this._fpsTimer >= 500) {
document.getElementById('fps').textContent = Math.round(this._frameCount / (this._fpsTimer / 1000));
this._frameCount = 0; this._fpsTimer = 0;
}
}
Don't route per-frame display values through data-bind. The reactive overhead is unnecessary for values that change 60 times per second. Use data-bind for user-interactive or store-driven values.
Pool Aggregates Are Reactive in Computeds
pool.length and pool.size are reactive when read from a computed or effect: the computed re-evaluates when entities are added, removed, or cleared. This is the natural pattern and it works, including when the computed first runs before the pool has finished registering.
// CORRECT - a computed reading pool.length stays live
wildflower.component('hud', {
computed: {
count() { return this.pools.enemies.length; }
}
});
The reactivity is on-demand. Pools that are never read from a computed or effect carry zero reactive machinery, and plain reads (action handlers, tick bodies) remain a bare map lookup. Per-frame entity field mutation stays outside reactivity entirely; the pool performance contract is unchanged. One nuance: pool.items.length is a plain array read and is NOT reactive. Read pool.length instead.
For per-frame display values that don't need reactivity at all (FPS, frame ms), writing to the DOM directly via cached refs as shown in the previous section is still the right tool. Structural counts belong on pool.length and a computed; per-frame telemetry belongs on direct writes.
data-pool-cull Requires left/top Positioning
Spatial culling (data-pool-cull) checks element positions via getBoundingClientRect(). This only works when entities use left/top CSS properties. Entities positioned with transform: translate() report their untransformed position (0,0), causing incorrect culling. Use left/top for pools that need culling, transform for pools that don't.
destroyComponent() Alone Doesn't Prevent Re-Initialization
The framework's mutation observer treats any element with a stale data-component-id (one whose instance is no longer in componentInstances) as a fresh component pending initialization. On the next scan, triggered by any DOM mutation or an explicit wildflower.scan(), the stale id is stripped, a new instance is created, and its init() fires again. This is intentional: it lets third-party HTML caches (DataTables, jQuery plugins) replay cached DOM containing data-component-id attributes. To truly tear down, remove the element AND destroy the instance.
// WRONG - element stays in DOM, scanner re-inits a fresh instance
wildflower.destroyComponent(instance.id);
// CORRECT - both must happen (either order)
instance.element.remove();
wildflower.destroyComponent(instance.id);
For most teardown (removing UI), don't call destroyComponent() at all: remove the element and the framework cleans up automatically via the mutation observer.
No data-bind-class Colon Syntax
<!-- WRONG - Colon syntax does NOT work -->
<div data-bind-class="isActive:active">
<!-- CORRECT - Object syntax (preferred) -->
<div data-bind-class="{ active: isActive }">
<!-- CORRECT - Ternary expression -->
<div data-bind-class="isActive ? 'active' : ''">
Complete Working Examples
fetch() bolted on. Declare a query instead ("Live External Data (data-query)" above): loading/error states, refresh, optimistic saves with rollback (write()), and reload persistence (persist: true) are built in.
Counter Component
<div data-component="counter">
<p>Count: <span data-bind="count"></span></p>
<button data-action="decrement">-</button>
<button data-action="increment">+</button>
<button data-action="reset">Reset</button>
</div>
<script>
wildflower.component('counter', {
state: {
count: 0
},
increment() {
this.count++;
},
decrement() {
this.count--;
},
reset() {
this.count = 0;
}
});
</script>
Todo List Component
<div data-component="todo-list">
<h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>
<form data-action="submit:addTodo">
<input type="text" data-model="newTodo" placeholder="What needs to be done?">
<button type="submit">Add</button>
</form>
<ul data-list="todos">
<template>
<li data-bind-class="completed ? 'done' : ''">
<input type="checkbox" data-model="completed">
<span data-bind="text"></span>
<button data-action="removeTodo">Delete</button>
</li>
</template>
</ul>
<div data-show="hasCompleted">
<button data-action="clearCompleted">Clear completed</button>
</div>
</div>
<script>
wildflower.component('todo-list', {
state: {
newTodo: '',
todos: []
},
computed: {
remaining() {
return this.todos.filter(t => !t.completed).length;
},
hasCompleted() {
return this.todos.some(t => t.completed);
}
},
addTodo(event) {
event.preventDefault();
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = '';
}
},
removeTodo(event, element, details) {
const index = details.index;
this.todos.splice(index, 1);
},
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
}
});
</script>
Modal (click-outside + Esc-to-close)
The complete pattern (focus management, multiple dialog modes, accessibility, when to add a portal) is documented at Modals & Dialogs. The condensed form:
<div data-component="modal-example">
<button data-action="open">Open Modal</button>
<!-- data-event-self: close only when the scrim itself is clicked, not
when a click bubbles up from the content (no stopPropagation needed).
data-cloak: hide until the framework processes data-show (no flash).
Add data-portal="body" ONLY if an ancestor has transform/filter/
contain that traps position:fixed. -->
<div class="modal-overlay" data-show="isOpen"
data-action="close" data-event-self data-cloak>
<div class="modal-content">
<h2 data-bind="title"></h2>
<p data-bind="message"></p>
<button data-action="close">Close</button>
</div>
</div>
</div>
<script>
wildflower.component('modal-example', {
state: {
isOpen: false,
title: 'Modal Title',
message: 'Click outside or press Escape to close.'
},
open() { this.isOpen = true; },
close() { this.isOpen = false; },
// Esc-to-close is a global shortcut while the modal is open, so it
// belongs on document, not as data-event-key-escape on an element
// (that form is for shortcuts scoped to a focused widget).
init() {
this._onKey = (e) => { if (e.key === 'Escape' && this.isOpen) this.close(); };
document.addEventListener('keydown', this._onKey);
},
destroy() { document.removeEventListener('keydown', this._onKey); }
});
</script>
Search with Debounce
<div data-component="search-box">
<input
type="text"
data-model="query"
data-action="input:search"
data-event-debounce="300"
placeholder="Search...">
<div data-show="isLoading">Searching...</div>
<ul data-list="results" data-show="!isLoading">
<template>
<li data-bind="title"></li>
</template>
</ul>
<p data-show="noResults">No results found</p>
</div>
<script>
wildflower.component('search-box', {
state: {
query: '',
results: [],
isLoading: false
},
computed: {
noResults() {
return !this.isLoading &&
this.query.length > 0 &&
this.results.length === 0;
}
},
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
this.isLoading = true;
try {
const response = await fetch(`/api/search?q=${this.query}`);
this.results = await response.json();
} finally {
this.isLoading = false;
}
}
});
</script>
Quick Setup Template
Minimal HTML to start a WildflowerJS project:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My WildflowerJS App</title>
<!-- Scripts in head with defer for best performance -->
<script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<div data-component="my-app">
<h1>Hello, <span data-bind="name"></span>!</h1>
<input type="text" data-model="name" placeholder="Enter your name">
</div>
</body>
</html>
With app.js:
wildflower.component('my-app', {
state: {
name: 'World'
}
});
Testing WildflowerJS Components
Use @wildflowerjs/test-utils for component testing. The package provides utilities for framework loading, state management, and timing.
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
| Utility | When 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).
fn(item) { ... }) can be used as a bare reference (data-bind="X") or inside any expression in any binding type:
data-bind,data-bind-class,data-bind-style,data-bind-attrdata-show,data-render(including compound expressions likeisShared && !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 thereactionChipscomputed against each comment, so you don't need to pre-decorate parent rows with the array
item); this remains the component context and resolves through the same ContextProxy as component methods: own props → computed → state → store. A zero-arg computed referenced in a row is evaluated at component scope and cannot see item properties.
Nested data-list with an item-level computed source
The framework reads item[path] first (fast path for raw fields) and falls back to evaluating an item-level computed of the same name when the field is undefined. Same authoring pattern as data-bind:
wildflower.component('thread', {
state: {
comments: [/* { id, body, reactions: { '👍': ['u-1', 'u-2'] } } */]
},
computed: {
// Per-comment reaction list. The computed receives the comment row
// as its argument; we project its raw reactions object to a chip
// array the inner data-list can iterate. No need to pre-build this
// on every comment.
reactionChips(comment) {
return ['👍','❤️','🚀'].map(emoji => {
const users = (comment.reactions && comment.reactions[emoji]) || []
return {
id: comment.id + ':' + emoji,
emoji,
chipClass: users.length > 0 ? 'chip is-active' : 'chip'
}
})
}
}
})
<div data-list="comments" data-key="id">
<template>
<div class="comment">
<p data-bind="body"></p>
<div class="reactions" data-list="reactionChips" data-key="id">
<template>
<button data-bind-class="chipClass" data-bind="emoji"></button>
</template>
</div>
</div>
</template>
</div>
Caveat: the fallback only fires when item[path] is undefined and path is a flat name (no dots). A raw field of the same name shadows the computed; for traversal lookups, use a flat-named computed.
item, index), functions without parameters are component-level (evaluated once per change).
wildflower.component('product-list', {
state: {
products: [
{ id: 1, name: 'Widget', price: 10 },
{ id: 2, name: 'Gadget', price: 20 }
],
taxRate: 0.1
},
computed: {
// Component-level (no parameters) - evaluated once at component level
cartTotal() {
return wildflower.getStore('cart').total;
},
// Item-level (has parameters) - evaluated per list item
// Signature: (item, index) - matches JS array method conventions
inCartQty(item) {
const cart = wildflower.getStore('cart');
return cart.items.find(i => i.id === item.id)?.qty || 0;
},
// Item-level computeds can call other item-level computeds
isInCart(item) {
return this.inCartQty(item) > 0;
},
// Can access component state directly
priceWithTax(item) {
return '$' + (item.price * (1 + this.taxRate)).toFixed(2);
},
// Second parameter is the index
rowClass(item, index) {
return index % 2 === 0 ? 'row-even' : 'row-odd';
}
}
});
Template usage. Computed properties resolve by name, no prefix needed:
<div data-list="products" data-key="id">
<template>
<div class="product" data-bind-class="rowClass">
<span data-bind="name"></span>
<span data-bind="priceWithTax"></span>
<span class="badge" data-bind="inCartQty"></span>
<span data-show="isInCart">IN CART</span>
</div>
</template>
</div>
Key Features
- Automatic detection: Functions with parameters are item-level, functions without are component-level
- Store reactivity: When store state changes, only affected list item bindings update (not the whole list)
- Computed chaining:
this.otherComputed(item)calls another item-level computed; use the flatthis.name(item)form and pass the row argument normally; no prefix required - Full context access:
this.state,this.propsavailable inside item-level computeds - Works with every expression-accepting binding:
data-bind,data-show,data-render,data-bind-class,data-bind-style,data-bind-attr
Compound Expressions Are Supported
An item-level computed can be used as a bare reference (simplest) or dropped into any expression: object literal, ternary, logical operator, string concatenation. The framework re-evaluates per row and tracks the same fine-grained dependencies in either case.
<!-- All of these forms call isShared(item) once per row -->
<li data-bind-class="isShared">...</li> <!-- bare reference (returns string) -->
<li data-bind-class="{ saved: isShared }">...</li> <!-- object syntax -->
<li data-bind-class="isShared ? 'saved' : ''">...</li> <!-- ternary -->
<li data-bind-attr="({ 'aria-pressed': isShared })">...</li> <!-- attr object -->
<li data-show="isShared && !isLocked">...</li> <!-- compound show -->
<li data-bind="isShared ? '✓ ' + name : name">...</li> <!-- ternary in text -->
<li data-bind-style="({ borderColor: badgeColor })">...</li> <!-- style object -->
Mutating sibling state the computed reads, for example this.shares = { ...this.shares, [item.id]: ... }, re-runs only the affected row's bindings. This is the canonical pattern for per-item lookup maps keyed by item.id. See https://www.wildflowerjs.com/docs/lists#item-level-computed-properties for a live, runnable example.
Summary Checklist for AI Assistants
Before generating WildflowerJS code, verify:
- Using
data-bind, NOT{{mustache}}syntax - Using
data-listwith<template>, NOTv-for - Using
data-poolfor high-frequency entity rendering (games, dashboards), NOTdata-list - Using
data-showordata-render, NOTv-if - Using
data-modelfor two-way binding, NOTv-model - Using
data-actionfor events, NOT@clickoronClick - Using
data-bind-class="{ active: isActive }"ordata-bind-class="expr ? 'class' : ''", NOTdata-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
methodsobject) - 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.
React to WildflowerJS
React uses JSX, hooks, and a virtual DOM. WildflowerJS uses HTML attributes, reactive state, and direct DOM updates. The table below maps one to the other.
Core Concepts Mapping
| React | WildflowerJS | Notes |
|---|---|---|
useState(initialValue) |
state: { prop: initialValue } |
State is an object, not individual hooks |
useMemo(() => derived, [deps]) |
computed: { derived() { return ... } } |
Dependencies are tracked automatically |
useEffect(() => {}, [deps]) |
watch: { 'prop': fn } or init() |
Watch for reactive effects, init for mount |
useRef() |
document.querySelector() in init() |
Direct DOM access when needed |
useContext() |
$entity.path or stores |
Cross-component communication |
useCallback() |
Methods on component | Methods are stable by default |
useReducer() |
Methods that update state | No need for reducers, update state directly |
| Props | props: {} + data-prop-* |
Explicit prop definitions with validation |
| Children / slots | data-slot |
Named slots for content projection |
JSX to HTML Attribute Mapping
| React JSX | WildflowerJS HTML |
|---|---|
<span>{count}</span> |
<span data-bind="count"></span> |
<span>{user.name}</span> |
<span data-bind="user.name"></span> |
<span>{doubleCount}</span> (derived) |
<span data-bind="doubleCount"></span> |
<div dangerouslySetInnerHTML={{__html: html}}/> |
<div data-bind-html="htmlContent"></div> |
<input value={val} onChange={e => setVal(e.target.value)}/> |
<input data-model="val"> |
<button onClick={handleClick}> |
<button data-action="handleClick"> |
<button onClick={() => setCount(c => c+1)}> |
<button data-action="increment"> |
<input onChange={handleChange}/> |
<input data-action="change:handleChange"> |
<input onInput={handleInput}/> |
<input data-action="input:handleInput"> |
{isVisible && <div>...</div>} |
<div data-show="isVisible">...</div> |
{isVisible ? <A/> : <B/>} |
<div data-show="isVisible">A</div><div data-show="!isVisible">B</div> |
{items.map(item => <li key={item.id}>{item.name}</li>)} |
<ul data-list="items"><template><li data-bind="name"></li></template></ul> |
className={isActive ? 'active' : ''} |
data-bind-class="{ active: isActive }" |
className={`btn ${variant}`} |
class="btn" data-bind-class="variant" |
style={{backgroundColor: color}} |
data-bind-style="{ backgroundColor: color }" |
Complete React to WildflowerJS Example
React Version:
import { useState, useMemo } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const remaining = useMemo(() =>
todos.filter(t => !t.completed).length,
[todos]
);
const addTodo = (e) => {
e.preventDefault();
if (newTodo.trim()) {
setTodos([...todos, { text: newTodo.trim(), completed: false }]);
setNewTodo('');
}
};
const toggleTodo = (index) => {
const updated = [...todos];
updated[index].completed = !updated[index].completed;
setTodos(updated);
};
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
return (
<div>
<h2>Todo List ({remaining} remaining)</h2>
<form onSubmit={addTodo}>
<input
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo, index) => (
<li key={index} className={todo.completed ? 'done' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(index)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
WildflowerJS Version:
<div data-component="todo-list">
<h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>
<form data-action="submit:addTodo">
<input data-model="newTodo" placeholder="What needs to be done?">
<button type="submit">Add</button>
</form>
<ul data-list="todos">
<template>
<li data-bind-class="completed ? 'done' : ''">
<input type="checkbox" data-model="completed">
<span data-bind="text"></span>
<button data-action="removeTodo">Delete</button>
</li>
</template>
</ul>
</div>
<script>
wildflower.component('todo-list', {
state: {
todos: [],
newTodo: ''
},
computed: {
remaining() {
return this.todos.filter(t => !t.completed).length;
}
},
addTodo(event) {
event.preventDefault();
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = '';
}
},
removeTodo(event, element, details) {
const index = details.index;
this.todos.splice(index, 1);
}
});
</script>
Key Differences from React
- No JSX: Use HTML with data attributes instead of JSX syntax
- No build step: Works directly in browser, no webpack/vite needed
- Mutable state: Update
this.propdirectly, 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-modelon checkbox binds to boolean automatically - Index access: Get index via
details.indexparameter in action methods
Vue to WildflowerJS
Vue's Options API is closest to WildflowerJS. The Composition API maps similarly to React hooks. The table below gives the translation.
Options API Mapping
| Vue Options API | WildflowerJS | Notes |
|---|---|---|
data() { return {} } |
state: {} |
Object directly, not a function |
computed: {} |
computed: {} |
Identical pattern |
methods: {} |
Methods directly on component | No methods wrapper needed |
watch: {} |
watch: {} |
Identical pattern |
mounted() |
init() |
Called after component mounts |
beforeUnmount() |
destroy() |
Cleanup before removal |
props: {} |
props: {} |
Similar prop definitions |
emits: [] |
this.emit('event', detail) |
Parent handles via onEvent(detail) method |
Template Directive Mapping
| Vue Template | WildflowerJS HTML |
|---|---|
{{ message }} |
<span data-bind="message"></span> |
{{ computed }} |
<span data-bind="computedName"></span> |
v-html="rawHtml" |
data-bind-html="rawHtml" |
v-model="text" |
data-model="text" |
v-model.number="count" |
<input data-model="count" data-model-number> |
v-show="isVisible" |
data-show="isVisible" |
v-if="condition" |
data-render="condition" |
v-else |
data-show="!condition" (no direct equivalent) |
v-for="item in items" :key="item.id" |
<div data-list="items"><template>...</template></div> |
v-for="(item, index) in items" |
Index via data-bind="_index" in templates or details.index in actions |
@click="handleClick" |
data-action="handleClick" |
@click.prevent="handleClick" |
data-action="handleClick" data-event-prevent |
@click.stop="handleClick" |
data-action="handleClick" data-event-stop |
@input="onInput" |
data-action="input:onInput" |
@keyup.enter="submit" |
data-action="keyup:submit" data-event-key-enter |
@input.debounce="search" |
data-action="input:search" data-event-debounce="300" |
:class="{ active: isActive }" |
data-bind-class="{ active: isActive }" |
:class="[baseClass, { active: isActive }]" |
class="baseClass" data-bind-class="{ active: isActive }" |
:style="{ color: textColor }" |
data-bind-style="{ color: textColor }" |
:disabled="isDisabled" |
data-bind-attr="{ disabled: isDisabled }" |
<slot></slot> |
<div data-slot="default"></div> |
<slot name="header"></slot> |
<div data-slot="header"></div> |
<Teleport to="body"> |
data-portal="body" |
Complete Vue to WildflowerJS Example
Vue Version:
<template>
<div>
<h2>Counter: {{ count }}</h2>
<p>Double: {{ doubleCount }}</p>
<button @click="decrement" :disabled="count <= 0">-</button>
<button @click="increment">+</button>
<button @click="reset">Reset</button>
<div v-show="count > 10" class="warning">
That's a lot!
</div>
<ul>
<li v-for="(item, index) in history" :key="index">
{{ item.action }}: {{ item.value }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
history: []
};
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
this.history.push({ action: 'increment', value: this.count });
},
decrement() {
if (this.count > 0) {
this.count--;
this.history.push({ action: 'decrement', value: this.count });
}
},
reset() {
this.count = 0;
this.history = [];
}
}
};
</script>
WildflowerJS Version:
<div data-component="counter">
<h2>Counter: <span data-bind="count"></span></h2>
<p>Double: <span data-bind="doubleCount"></span></p>
<button data-action="decrement">-</button>
<button data-action="increment">+</button>
<button data-action="reset">Reset</button>
<div data-show="showWarning" class="warning">
That's a lot!
</div>
<ul data-list="history">
<template>
<li>
<span data-bind="action"></span>: <span data-bind="value"></span>
</li>
</template>
</ul>
</div>
<script>
wildflower.component('counter', {
state: {
count: 0,
history: []
},
computed: {
doubleCount() {
return this.count * 2;
},
showWarning() {
return this.count > 10;
}
},
increment() {
this.count++;
this.history.push({ action: 'increment', value: this.count });
},
decrement() {
if (this.count > 0) {
this.count--;
this.history.push({ action: 'decrement', value: this.count });
}
},
reset() {
this.count = 0;
this.history = [];
}
});
</script>
Key Differences from Vue
- No SFC: No Single File Components - HTML and JS are separate or inline
- No build step: No vue-loader or vite plugin needed
- State access: Use
this.propdirectly (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.storesto access them
Svelte to WildflowerJS
Svelte compiles away the framework. WildflowerJS also has minimal runtime overhead with direct DOM updates. The table below maps one to the other.
Core Concepts Mapping
| Svelte | WildflowerJS | Notes |
|---|---|---|
let count = 0; (reactive) |
state: { count: 0 } |
All state in state object |
$: doubled = count * 2; |
computed: { doubled() { return this.count * 2; } } |
Computed properties |
$: { console.log(count); } |
watch: { count(newVal) { console.log(newVal); } } |
Reactive statements become watchers |
export let prop; |
props: { prop: { type: String } } |
Explicit prop definitions |
onMount(() => {}) |
init() {} |
Lifecycle on mount |
onDestroy(() => {}) |
destroy() {} |
Lifecycle on destroy |
createEventDispatcher() |
$entity.path or custom events |
Component communication |
Stores (writable, readable) |
wildflower.store() + subscribe |
Global state management |
Template Syntax Mapping
| Svelte Template | WildflowerJS HTML |
|---|---|
{count} |
<span data-bind="count"></span> |
{@html rawHtml} |
<div data-bind-html="rawHtml"></div> |
bind:value={text} |
data-model="text" |
bind:checked={isChecked} |
data-model="isChecked" |
bind:group={selected} |
data-model="selected" on each radio |
on:click={handleClick} |
data-action="handleClick" |
on:click|preventDefault={fn} |
data-action="fn" data-event-prevent |
on:click|stopPropagation={fn} |
data-action="fn" data-event-stop |
on:keydown|self={fn} |
data-action="keydown:fn" |
{#if condition}...{/if} |
<div data-show="condition">...</div> |
{#if condition}...{:else}...{/if} |
<div data-show="condition">...</div><div data-show="!condition">...</div> |
{#each items as item}...{/each} |
<div data-list="items"><template>...</template></div> |
{#each items as item, index}...{/each} |
Index via data-bind="_index" in templates or details.index in actions |
{#each items as item (item.id)}...{/each} |
Keyed rendering automatic when items have id |
class:active={isActive} |
data-bind-class="{ active: isActive }" |
style:color={textColor} |
data-bind-style="{ color: textColor }" |
<slot></slot> |
<div data-slot="default"></div> |
<slot name="header"></slot> |
<div data-slot="header"></div> |
transition:fade |
data-transition="fade" |
Complete Svelte to WildflowerJS Example
Svelte Version:
<script>
let items = [];
let newItem = '';
$: itemCount = items.length;
$: hasItems = items.length > 0;
function addItem() {
if (newItem.trim()) {
items = [...items, { text: newItem.trim(), done: false }];
newItem = '';
}
}
function toggleItem(index) {
items[index].done = !items[index].done;
items = items; // Trigger reactivity
}
function removeItem(index) {
items = items.filter((_, i) => i !== index);
}
</script>
<div>
<h2>Items: {itemCount}</h2>
<form on:submit|preventDefault={addItem}>
<input bind:value={newItem} placeholder="New item">
<button type="submit">Add</button>
</form>
{#if hasItems}
<ul>
{#each items as item, index}
<li class:done={item.done}>
<input type="checkbox" bind:checked={item.done}>
<span>{item.text}</span>
<button on:click={() => removeItem(index)}>X</button>
</li>
{/each}
</ul>
{:else}
<p>No items yet</p>
{/if}
</div>
WildflowerJS Version:
<div data-component="item-list">
<h2>Items: <span data-bind="itemCount"></span></h2>
<form data-action="submit:addItem">
<input data-model="newItem" placeholder="New item">
<button type="submit">Add</button>
</form>
<ul data-list="items" data-show="hasItems">
<template>
<li data-bind-class="done ? 'done' : ''">
<input type="checkbox" data-model="done">
<span data-bind="text"></span>
<button data-action="removeItem">X</button>
</li>
</template>
</ul>
<p data-show="!hasItems">No items yet</p>
</div>
<script>
wildflower.component('item-list', {
state: {
items: [],
newItem: ''
},
computed: {
itemCount() {
return this.items.length;
},
hasItems() {
return this.items.length > 0;
}
},
addItem(event) {
event.preventDefault();
if (this.newItem.trim()) {
this.items.push({
text: this.newItem.trim(),
done: false
});
this.newItem = '';
}
},
removeItem(event, element, details) {
const index = details.index;
this.items.splice(index, 1);
}
});
</script>
Key Differences from Svelte
- No compilation: WildflowerJS runs directly in browser, no build needed
- Explicit state: All reactive variables go in
state: {} - Direct mutation: Use
push,splicedirectly - 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:namedirective
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 oftemplateattribute - Simple values in lists: Use
data-bind="$item"for primitive arrays - Event modifiers: Separate attributes instead of dot notation
- More structure: Better for larger applications with reusable components
Computed Property Access
Computed properties are resolved by bare name across all binding types:
<!-- Just use the name directly -->
<span data-bind="fullName"></span>
<!-- Works on ALL binding types -->
<div data-show="isValid"></div>
<div data-bind-class="statusClass"></div>
<div data-bind-style="boxStyle"></div>
<ul data-list="filteredItems">...</ul>
<!-- In expressions, computed properties also resolve by name -->
<span data-bind="total > 100 ? 'Over budget' : 'OK'"></span>
<!-- Inside list templates, same rules apply -->
<ul data-list="items">
<template>
<li data-bind="name"></li>
<li data-bind-class="id === selectedId ? 'selected' : ''"></li>
</template>
</ul>
Name Collisions
When a state property and computed property share the same name, computed takes precedence.
JavaScript Access
In JavaScript code (methods, computed functions, lifecycle hooks), ContextProxy auto-resolves state and computed properties. Use bare names:
// ✅ CORRECT - ContextProxy auto-resolves
this.count // Access state
this.fullName // Access computed
// ✅ ALSO WORKS - explicit paths are still valid
this.state.count // Explicit state access
this.computed.fullName // Explicit computed access (item-level computeds are called flat as this.name(item); no prefix needed)
Quick Reference
| Context | Syntax | Example |
|---|---|---|
| HTML template (any binding) | Bare name (prefix optional) | data-bind="fullName" |
| HTML expression | Bare name in expression | data-bind="total > 100 ? 'High' : 'Low'" |
| Inside list template | Bare name (prefix optional) | data-show="isInCart" |
| JavaScript methods/computed | Bare name (auto-resolved) | this.count, this.fullName |
| Item-level computed chaining | this.X(item) (flat form; no prefix needed) |
this.inCartQty(item) |
$entity.path in templates |
Not needed (auto-resolved) | data-bind="$comp.name" |