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 - no custom syntax, no compilation
- Attribute-based binding - uses HTML data attributes
- Proxy-based reactivity - automatic DOM updates on state change
- Full bundle - includes routing, SSR, and stores
- Multiple build variants - lite, core, spa, full, and ai bundles available
Reactivity Rules
WildflowerJS reactivity is proxy-based. The proxy SET trap fires on every assignment to state at any nesting depth, and the corresponding effect re-runs. There is no Vue 2-style Vue.set requirement, no immutability requirement, no object-identity rule.
✅ Triggers reactivity
this.count = 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: www/js/src/state/ProxyHandlers.js SET and DELETE traps. Tests: test-new/direct-mutation.test.js exercises nested object property mutation, array index assignment, nested-object-in-array property mutation, and array methods. The proxy traps fire and _handleStateChange notifies effects in every case.
The this.X Shortcut (Idiomatic)
Inside component methods and computeds, this.shares resolves to this.state.shares automatically. And this.savedCount resolves to a computed of the same name. The this.state / this.computed longhand also works, but the shortcut is the idiomatic form. Resolution order: own property → computed → state.
wildflower.component('cart', {
state: { items: [], discount: 0 },
computed: {
// Use this.items directly, not this.state.items
total() { return this.items.reduce((s, i) => s + i.price, 0); },
// Use this.total directly, not this.computed.total
finalPrice() { return this.total * (1 - this.discount); }
},
addItem(item) {
// Direct nested mutation: triggers reactivity
this.items.push(item);
}
});
Source: www/js/src/state/ContextProxy.js: "Makes this.count in methods resolve identically to data-bind="count" in templates." This applies inside the component definition's own methods/computeds, item-level computeds in lists, watcher callbacks, store methods, and lifecycle hooks. The longer this.state.X form remains valid and is sometimes useful for disambiguation when a state property name collides with a method name.
this.X shortcut described above apply to component / store / item-level-computed scope. Pool entities are plain objects with property descriptors, not proxies. There's no this.state, no this.stores, no automatic dep tracking on entity reads. This is a deliberate performance choice for high-frequency rendering (games, dashboards, simulations). For reactive per-item state in a regular list, use a data-list with item-level computeds (the section below). For pool-entity binding rules, see the data-pool section.
Quick Reference: Reactivity
| 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.state.items));
// postMessage to a worker
worker.postMessage({ payload: wildflower.toRaw(this.state.config) });
// History state
wildflower.router.navigate('/results', {
state: wildflower.toRaw(this.state.filters)
});
fetch with a JSON body works directly on a proxy; toRaw is only needed for structured-clone-using APIs. The snapshot is a one-time copy: re-call when a fresh copy is needed.
Optimal Page Structure
When generating HTML pages, always place scripts in <head> with defer for best Lighthouse scores:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My App</title>
<link rel="stylesheet" href="styles.css">
<!-- All scripts in head with defer -->
<script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<div data-component="my-app">
<!-- App content -->
</div>
</body>
</html>
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
UAM handles this automatically. When using UAM with framework idiom files, the generated HTML follows this optimal structure.
Preventing Flash of Unstyled Content
When using defer scripts, add this CSS to prevent modals from briefly flashing:
/* Hide cloaked elements until framework processes them */
[data-cloak] { display: none; }
Add the data-cloak attribute to elements that should be hidden until the framework initializes (e.g., data-show elements starting false, data-portal elements). The framework removes data-cloak after initialization.
data-cloak inside <template> elements (i.e., inside data-list templates). Template content is inert and invisible until the framework clones it, so there is no FOUC risk. Using data-cloak inside templates causes bugs with dynamically added list items.
Inline Scripts with Deferred Dependencies
If inline scripts depend on deferred libraries, wrap them in DOMContentLoaded:
document.addEventListener('DOMContentLoaded', function() {
// Deferred scripts are now loaded
externalLibrary.init();
});
UAM-generated pages wrap inline scripts automatically.
Inlined Pages, Blob URLs, Single-File Distributions
When an entire HTML page (framework + app code) is inlined into a single file or fetched as a Blob URL, browsers can re-order parsing such that the defer ordering guarantee doesn't hold. Symptoms: wildflower is not defined, components never mount, "Component not registered" warnings.
Fix: wrap your wildflower.component(...) registrations in DOMContentLoaded:
document.addEventListener('DOMContentLoaded', () => {
wildflower.component('my-component', { /* ... */ });
// wildflower.scan() runs automatically after DOMContentLoaded
});
Normal page loads with separate <script defer> tags don't need this. The wrapper only applies to bundled / inlined / blob-URL distributions.
Core Attributes Reference
| Attribute | Purpose | Example |
|---|---|---|
data-component="name" |
Declares a component instance | <div data-component="counter"> |
data-bind="property" |
One-way text binding (textContent) | <span data-bind="count"></span> |
data-bind="computedName" |
Bind to computed property (no prefix needed) | <span data-bind="fullName"></span> |
data-bind-html="property" |
Bind as innerHTML (XSS risk with untrusted input, use setHtmlSanitizer()) |
<div data-bind-html="htmlContent"></div> |
data-bind-class="expression" |
Dynamic CSS classes (object or expression) | <div data-bind-class="{ active: isActive }"> or <div data-bind-class="isActive ? 'active' : ''"> |
data-bind-style="expression" |
Dynamic inline styles via object | <div data-bind-style="{ color: textColor }"> |
data-bind-attr="expression" |
Bind HTML attributes dynamically | <img data-bind-attr="{ src: imagePath, alt: description }" /> |
data-model="property" |
Two-way binding for inputs | <input data-model="username"> |
data-model-number |
Convert value to number | <input data-model="price" data-model-number> |
data-model-trim |
Trim whitespace from value | <input data-model="name" data-model-trim> |
data-model-lazy |
Update only on blur/change | <input data-model="email" data-model-lazy> |
data-action="method" |
Click handler (default) | <button data-action="save">Save</button> |
data-action="event:method" |
Specific event handler | <input data-action="input:search"> |
data-list="arrayProperty" |
Render array items | <ul data-list="items"> |
data-pool="poolName" |
Lightweight collection renderer (plain objects, no proxy overhead) | <div data-pool="enemies" data-key="id"> |
data-show="property" |
Show/hide via CSS (display: none) | <div data-show="isVisible"> |
data-render="property" |
Insert/remove from DOM entirely | <div data-render="showPanel"> |
data-portal="selector" |
Teleport content to another element | <div data-portal="body">Modal content</div> |
data-transition="name" |
CSS transitions on show/hide | <div data-show="visible" data-transition="fade"> |
data-external |
Preserve element during HTML updates | <div data-external data-component="live-chart"> |
data-prop-*="value" |
Pass props to child component | <div data-component="child" data-prop-user="currentUser"> |
data-props="{ ... }" |
Pass multiple props as object | <div data-component="child" data-props="{ title: heading, color: theme }"> |
data-use-template="name" |
Reference parent's named template | <div data-use-template="cardTemplate" data-with="user"> |
data-item-template="name" |
Define a named template for children | <template data-item-template="cardTemplate"> |
data-template-key="property" |
Select template variant by data value | <div data-component="viewer" data-template-key="viewType"> |
data-type="value" |
Template variant identifier (for data-template-key) |
<template data-type="card">...</template> |
Note: All data-* attributes also support a data-wf-* prefix (e.g., data-wf-bind, data-wf-action). When both forms are present on the same element the data-wf-* form wins. In practice the major peer libraries don't collide — Bootstrap uses data-bs-*, HTMX uses hx-*, Alpine uses x-* — so the unprefixed form is fine for almost every project. Reach for data-wf-* only when an external library or a bespoke integration legitimately uses the bare names (data-bind, data-show, data-action, etc.) for its own purposes. To enforce data-wf-* exclusively, set data-wf-prefix="true" on the framework script tag (toggles the useWfPrefixOnly config in www/js/src/core/Bootstrap.js).
data-show vs data-render
Both conditionally display content, but they work differently:
| Attribute | Behavior | When to Use |
|---|---|---|
data-show |
Toggles display: none. Element remains in DOM. |
Frequent toggling, preserve form state, simple show/hide |
data-render |
Inserts/removes from DOM entirely. Destroys nested components. | Complex conditional content, memory efficiency, expensive widgets |
<!-- data-show: Toggle visibility, keep element in DOM -->
<div data-show="isLoggedIn">Welcome back!</div>
<!-- data-render: Insert/remove from DOM completely -->
<div data-render="showExpensiveWidget">
<div data-component="heavy-chart">...</div>
</div>
Component Definition Pattern
wildflower.component('component-name', {
// Reactive state object
state: {
count: 0,
items: [],
user: { name: '', email: '' }
},
// Computed properties (cached, auto-update on dependency change)
computed: {
doubleCount() {
return this.count * 2;
},
itemCount() {
return this.items.length;
}
},
// Lifecycle: called after component mounts
init() {
console.log('Component initialized');
},
// Lifecycle: called before component destroys
destroy() {
// Cleanup code here
},
// Action methods (called from data-action)
increment() {
this.count++;
},
// Actions receive (event, element, context)
handleClick(event, element) {
event.preventDefault();
// element is the clicked DOM element
},
// List item actions receive item data in details parameter
removeItem(event, element, details) {
const index = details.index;
this.items.splice(index, 1);
}
});
Lifecycle Constraints
Reserved method names. Do NOT use these names for action handlers or helpers; the framework drives them on its own schedule:
init,beforeInit: called once during mountdestroy,beforeDestroy: called once during teardownonUpdate,beforeUpdate: called on every reactive updateonError: called when a method throwstick: called every animation frame for components in the pool loop
Most common AI-generation trap: naming an action tick on a non-animation component. The framework will call it every frame instead of on click. Pick a specific verb (increment, handleClick, refresh).
Actions before init are queued, not dropped. If a user click fires before init() completes, the call is held and replayed in order after init returns. Generated handlers can assume init()-set state is present when they execute, even if the click happened during the brief mount/init window or while a subscribed store was loading.
Stale event arg in replayed actions. Replayed actions see the original DOM event, but event.preventDefault() is a no-op by replay time. For forms that must reliably block submission, use data-event-prevent on the form element (framework intercepts before user code) rather than calling preventDefault() in the handler.
List Rendering Pattern
Use HTML5 <template> element inside data-list container:
<ul data-list="items">
<template>
<li>
<span data-bind="name"></span>
<span data-bind="price"></span>
<button data-action="removeItem">Remove</button>
</li>
</template>
</ul>
With corresponding state:
state: {
items: [
{ name: 'Apple', price: 1.50 },
{ name: 'Banana', price: 0.75 }
]
}
Accessing list index: The framework provides index and item data via the details parameter:
removeItem(event, element, details) {
const index = details.index; // Current item's index
const item = details.item; // Current item's data
this.items.splice(index, 1);
}
Primitive arrays (strings, numbers): use data-bind="$item" to bind the item itself, since there's no property name to bind to:
<ul data-list="tags">
<template><li data-bind="$item"></li></template>
</ul>
State: tags: ['html', 'css', 'javascript']. The $item reference is item-context only; for object arrays use property names directly. See the dedicated Primitive Lists section below for more.
Nested Lists Pattern
<div data-list="categories">
<template>
<div class="category">
<h3 data-bind="name"></h3>
<ul data-list="items">
<template>
<li data-bind="title"></li>
</template>
</ul>
</div>
</template>
</div>
Data Pool Rendering (data-pool)
data-pool is an explicit-control rendering primitive for collections. Unlike data-list, pool items are plain JS objects with zero reactive proxy overhead. You mutate objects directly and call markDirty() to trigger updates. Pools handle full CRUD, selection, and bulk operations faster than the push-reactive path. Use for performance-sensitive workloads, real-time data (dashboards, tickers), large datasets (1K+ items), and per-frame animation (games, particles, visualizations). Use data-list when items need two-way binding (data-model), parent computed properties, or nested lists.
<div data-pool="enemies" data-key="id">
<template>
<div data-bind-style="{ left: x + 'px', top: y + 'px' }">
<img data-bind-attr="{ src: imgSrc }">
<span data-bind="label"></span>
</div>
</template>
</div>
// Declare pools alongside state (preferred):
wildflower.component('game', {
pools: {
enemies: {
onAdd: 'onSpawn', // lifecycle hooks (string ref or inline fn)
onRemove: 'onDeath', // fires before individual removal
onClear: 'onWaveEnd' // bulk clear (skips onRemove)
},
projectiles: {} // no hooks needed
}
})
// Pool API (via this.pools.name):
const pool = this.pools.enemies;
// Array-like API (preferred: reads like native JS arrays):
pool.push({ id: 1, x: 100, y: 200, imgSrc: 'orc.png', label: 'Orc' });
pool.push(arrayOfItems); // bulk add via DocumentFragment (single DOM op)
pool.length; // current count
for (const e of pool) { /* ... */ } // iterate via Symbol.iterator
pool.filter(e => e.alive); // returns plain array
pool.map(e => e.id);
pool.find(e => e.id === 42);
pool.forEach(fn); pool.some(fn); pool.every(fn); pool.reduce(fn, init);
// Key-based ops (pool uses swap-with-last, so no positional pop/splice):
pool.remove(1); // remove by key
pool.get(1); // get entity by key (or undefined)
pool.update(1, { x: 200 }); // patch properties (sync for static pools)
pool.clear(); // remove all
pool.getElement(1); // get DOM element by key
pool.swap(key1, key2); // swap two items' DOM positions
pool.markDirty(key); // re-evaluate bindings for one entity
pool.props; // shared props object (parent-injected data)
// Long-form aliases (identical semantics): pool.add(obj), pool.size, pool.items
// DOM updates happen automatically on next rAF
// Imperative form also works: this.pool('enemies')
Entity Shape: state defaults, computed properties, methods
Declare entity: { state, computed, ... } on a pool to give every entity a shared shape. Entity computed properties are the key lever for per-frame animation: derive transform/background/class strings from raw inputs (x, y, z), and data-bind-* reads them on flush. In tick(dt) you only mutate the inputs; no markDirty, no manual string assembly.
pools: {
particles: {
entity: {
state: { hp: 100, vx: 0, vy: 0 }, // defaults merged into new entities (spawn values win)
computed: {
tf() { return `translate(${this.x}px,${this.y}px)`; },
bg() { return COLOR_LUT[Math.round(this.z * 4) & 255]; }
},
kill() { this.hp = 0; } // entity methods, callable as entity.kill()
}
}
}
// Template: <div data-bind-style="{ transform: tf, background: bg }"></div>
tick(dt) {
for (const p of this.pools.particles) {
p.x += p.vx * dt;
p.y += p.vy * dt;
}
// tf and bg auto-recompute from the mutated inputs, no markDirty needed.
}
Entity state is a shallow merge. Spawn values override template keys. Entity computed accessors are installed per-entity and skipped when the spawn already has an own property of the same name. Any non-reserved function at the top of the entity block becomes a method callable as entity.methodName().
Static Pools, Bulk Add, and Pool Props
// Static pool: add data-pool-static (boolean) to skip rAF flush loop.
// Items render synchronously on add()/update(). Zero idle CPU cost.
// HTML: <div data-pool="users" data-key="id" data-pool-static>
// Bulk add: pass an array, single DOM operation via DocumentFragment
this.pools.users.add(data); // data is an array of objects
// Pool props: shared data available to all items via `props.` prefix
pools: {
boids: {
props: { shape: 'arrow', theme: 'dark' }
}
}
// Template: data-bind-class="className + ' ' + props.shape"
// Update: this.pools.boids.props.shape = 'circle'; (all items update on next flush)
tick(dt) Lifecycle Hook
Components with a tick method get called once per animation frame. The framework manages the rAF loop automatically. No manual setup or teardown. tick runs BEFORE pool flush, so entity mutations are visible in the DOM on the same frame.
wildflower.component('game', {
init() { this._pool = this.pool('enemies'); },
// Called automatically each frame. dt = ms since last frame (clamped to 250ms).
tick(dt) {
for (const e of this._pool.items) {
e.x += e.vx * dt;
e.y += e.vy * dt;
}
}
// No destroy() needed: framework cleans up the rAF loop
});
tick(dt) also receives now (performance.now()) as a second argument. Works with or without pools, useful for canvas, Three.js, or any per-frame logic.
data-pool-action Event Delegation
Delegates events from pool items to component methods. Supports multiple declarations with CSS selector targeting. One DOM listener per event type, O(1) entity lookup.
<!-- Single action -->
<div data-pool="enemies" data-key="id" data-pool-action="click:onEnemyClick">
<!-- Multiple actions with selectors -->
<div data-pool="tasks" data-key="id"
data-pool-action="click:.edit-btn:onEdit; click:.delete-btn:onDelete; mouseover:onHover">
<template>
<div class="task">
<span data-bind="title"></span>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</div>
</template>
</div>
// Format: "method" | "event:method" | "event:.selector:method"
// Semicolons separate multiple declarations
// Selector-specific handlers take priority over catch-all on same event
onEdit(item, event) { /* clicked .edit-btn */ },
onDelete(item, event) { this.pools.tasks.remove(item.id); },
onHover(item, event) { /* mouseover anywhere in item */ }
pool.onChange Callback
Fires synchronously on add(), remove(), and clear(). Pool size is already updated when the callback fires.
const pool = this.pool('enemies');
pool.onChange = (p) => {
document.getElementById('count').textContent = p.size;
};
data-pool vs data-list
data-list |
data-pool |
|
|---|---|---|
| Items | Reactive proxy objects | Plain JS objects |
| Updates | Automatic on state mutation | Batched via rAF loop |
| Template scope | Full component context (state, computed, stores) | Entity properties only |
| Use case | Interactive collections (forms, inline editing, two-way binding) | Performance-sensitive CRUD, real-time data, large datasets, per-frame animation |
| Overhead | Proxy per item | Zero proxy overhead |
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 intentionally expose different scopes inside their templates. Pools narrow to entity + shared props for performance; lists give you the full component scope.
| Available inside <template> | data-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: www/js/src/rendering/PoolRenderer.js (entity-only ctx, no component-state merge); www/js/src/rendering/ListRenderer.js + ListItemBinding.js + BindingResolver.js (full scope). Tests: test-new/pool-entity-computed.test.js, test-new/pool-props.test.js, test-new/list-item-context.test.js.
data-bind-class Syntax
The data-bind-class attribute supports object syntax (preferred for toggling) and expression syntax (for building class strings):
<!-- Object syntax: toggle classes by boolean (preferred) -->
<div data-bind-class="{ active: isActive }">
<div data-bind-class="{ error: hasError, warning: hasWarning }">
<div data-bind-class="{ selected: id === selectedId }">
<!-- Expression syntax: returns a class name string -->
<div data-bind-class="isActive ? 'active' : 'inactive'">
<div data-bind-class="isError ? 'alert alert-danger' : 'alert alert-success'">
<!-- Both work in list items -->
<ul data-list="items">
<template>
<li data-bind-class="{ done: completed }">
<span data-bind="text"></span>
</li>
</template>
</ul>
Cross-Component Communication
Use the $ accessor in templates to read another component's state or computed properties:
<!-- Read another component's state directly in HTML -->
<span data-bind="$theme-manager.mode"></span>
<div data-show="$nav-manager.menuOpen">Navigation</div>
<span data-bind="$user-profile.fullName"></span>
// In computed properties: automatic dependency tracking
wildflower.component('observer', {
computed: {
themeMode() {
const theme = wildflower.getComponent('theme-manager');
return theme ? theme.mode : 'light';
}
}
});
$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: www/js/src/features/ErrorBoundaries.js. getComponent(name) returns a ContextProxy when called inside a computed (for dependency tracking), or the raw context when called from anywhere else; returns null if no instance with that name exists.
Component Mount / Destroy Events
External scripts (analytics, devtools, page-level orchestration, tests) can listen for framework lifecycle events on document. Use wildflower:ready as the canonical "framework is up" signal. It fires once per page after all initial scanning and mounting completes.
| 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 or page-load orchestration, use wildflower.whenSettled(): a promise that resolves after the framework's full async chain (microtask flush + setTimeout(0) + rAF + final microtask) so all pending updates are applied.
Source: www/js/src/core/FrameworkInit.js (wildflower:ready), www/js/src/components/ComponentScanning.js + ComponentLifecycle.js (componentInit), www/js/src/features/ErrorBoundaries.js (componentDestroy), www/js/src/state/StoreManager.js (store-ready), www/js/src/core/WildflowerCore.js (whenSettled).
Store Pattern (Global State)
// Create a store
wildflower.store('user', {
state: { name: 'Guest', isLoggedIn: false },
computed: {
greeting() { return 'Hello, ' + this.name; }
},
login(name) {
this.name = name;
this.isLoggedIn = true;
}
});
// ✅ BEST: Use $ directly in HTML, no computed wrappers needed
// <span data-bind="$user.name"></span>
// <span data-bind="$user.greeting"></span>
// ✅ ALSO GOOD: Declarative subscription with this.stores
// (use when you need store access in JS methods)
wildflower.component('header', {
subscribe: {
user: ['name', 'isLoggedIn']
},
// Use this.stores in methods
refreshUser() {
this.stores.user.refreshProfile();
}
});
// With onStoreUpdate() for side effects
wildflower.component('login-form', {
state: { username: '' },
subscribe: {
user: ['isLoggedIn']
},
onStoreUpdate(storeName, path, newValue, oldValue) {
if (storeName === 'user' && path === 'isLoggedIn' && newValue) {
this.showWelcomeMessage();
}
},
handleLogin() {
this.stores.user.login(this.username);
}
});
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.
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">
Portals (Teleporting Content)
Render content elsewhere in the DOM while maintaining component ownership. This example teleports a modal to <body>; for the full modal recipe (and when a portal is actually required) see Modals & Dialogs.
<div data-component="modal-trigger">
<button data-action="showModal">Open Modal</button>
<!-- This content renders at document body, but actions bind to this component -->
<div data-portal="body" data-show="isModalOpen">
<div class="modal-backdrop">
<div class="modal-content">
<h2>Modal Title</h2>
<p data-bind="message"></p>
<button data-action="closeModal">Close</button>
</div>
</div>
</div>
</div>
Lifecycle Hooks
wildflower.component('example', {
state: { /* ... */ },
// Called before component mounts
beforeInit() {
// Setup that must happen before DOM bindings
},
// Called after component mounts and bindings are active
init() {
// Fetch data, setup subscriptions, etc.
},
// Called before state update propagates (no parameters - use watchers for specifics)
beforeUpdate() {
// Validate or transform data before update
},
// Called after state update propagates (no parameters - use watchers for specifics)
onUpdate() {
// React to state changes
},
// Called before component is destroyed
beforeDestroy() {
// Prepare for cleanup
},
// Called when component is destroyed
destroy() {
// Cleanup subscriptions, timers, etc.
},
// Error boundary
onError(error, info) {
console.error('Component error:', error);
// Handle gracefully
}
});
Watchers Pattern
wildflower.component('example', {
state: {
searchQuery: ''
},
watch: {
// Watch state property
'searchQuery': function(newValue, oldValue) {
console.log('Search changed:', newValue);
this.performSearch(newValue);
},
// Watch nested property
'user.profile.name': function(newValue) {
console.log('Name changed:', newValue);
}
}
});
Debugging Silent Failures
Most "broken bindings" turn out to be one of a small set of cases. AI authors don't have the human heuristic of "huh, that's weird"; knowing what to look for matters. The list below is exhaustive as of the current build; cases not on it have explicit error or warning paths.
| Symptom | Cause | Fix |
|---|---|---|
An element with data-component="foo" stays inert: no init, no event handlers, no warning. |
The component name foo wasn't registered. The framework's component scanner skips unknown names with no console output. |
Confirm wildflower.component('foo', { ... }) ran before wildflower.scan() (or before the framework's auto-scan). With defer on both the framework and the app script, the auto-scan picks up registrations made during script parse. |
| A binding renders empty (the surrounding markup appears, the value doesn't). | The bound name resolves to undefined: a misspelled state property, a misspelled computed name, or an item property that doesn't exist on this row. |
Type the name into the browser console: wildflower.getComponent('your-component').yourBinding. undefined confirms the cause. The binding system intentionally skips DOM writes when a value resolves to undefined rather than rendering the literal string "undefined". |
| Item-level computed returns the wrong value or never updates. | The signature declares scope: fn(item, index, info) { ... } is item-level; fn() { ... } is component-level. A zero-arg computed referenced inside a list-template binding (data-bind="X" in a list <template>) is treated as component-level: same value for every row. If your binding renders the same value for every row when you expected per-row variation, your computed is missing its item parameter. |
Declare it as fn(item) { ... }. Read the row via item.X; component state via this.state.X; subscribed stores via this.stores.X. Use the bare name in any binding or inside any expression. |
| A binding evaluates an expression that calls a method, and the method runs but the DOM doesn't update. | The method mutates state inside a computed, which violates the "computeds are pure" contract. Or you put a side effect in a binding expression itself. | Move side effects into watch handlers or component methods. Bindings should only read. |
| Component definition has a syntax error and the page looks blank. | The component never registered, so its root element stays inert (same as the unregistered-name case). | Open the browser console. JavaScript syntax errors surface there. The framework also logs registration errors via its _log('error', ...) path when the definition shape is invalid (non-object, missing name, etc.). |
data-show / data-render always evaluates falsy on a list item. |
You're referencing a property that's not on the item. this.X from outside is component scope; inside a list item template the same name resolves against item properties first, then component scope, then list-context vars. |
If the value is a per-item lookup (e.g., is this row "shared" given a sibling lookup map keyed by item.id), declare an item-level computed: isShared(item) { return !!this.shares[item.id]; }. |
Cases that are not silent failures (have explicit handling):
data-bind-class="{ on: x }"returning an object literal: converted to a class string by_classResultToString; no"[object Object]"ever appears.data-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;
Exception: Direct DOM Writes in Pool Components
In data-pool components with a tick(dt) method, direct DOM writes are correct for non-pool UI (FPS counters, HUD stats, score displays):
// CORRECT in tick(): direct write for display-only HUD
tick(dt) {
// Update entities
for (const e of this._pool.items) { e.x += e.vx * dt; }
// Direct DOM write for FPS counter
this._fpsTimer += dt;
if (this._fpsTimer >= 500) {
document.getElementById('fps').textContent = Math.round(this._frameCount / (this._fpsTimer / 1000));
this._frameCount = 0; this._fpsTimer = 0;
}
}
Don't route per-frame display values through data-bind. The reactive overhead is unnecessary for values that change 60 times per second. Use data-bind for user-interactive or store-driven values.
Pool Aggregates: Mirror in the Tick
pool.length, pool.size, and pool.items.length are plain JavaScript getters by design. They bypass the reactive proxy so that adding and removing entities at 60fps doesn't pay reactivity overhead on every change. The idiomatic pattern: pools live inside components with a tick(), and you mirror the aggregate count to reactive state inside that tick, gated by an inequality check.
// CORRECT - mirror pool aggregates to state inside the tick
wildflower.component('game', {
state: { entityCount: 0 },
pools: { enemies: {}, projectiles: {}, loots: {} },
tick(dt) {
// ... game update logic ...
const total = this.pool('enemies').length
+ this.pool('projectiles').length
+ this.pool('loots').length;
if (this.entityCount !== total) this.entityCount = total;
}
});
The gate matters. Without it, every frame writes to state.entityCount whether the count changed or not, and the reactive cascade fires 60 times per second. With it, the write is a single property assignment with a no-op short-circuit, and bindings only update when the number actually moves.
For per-frame display values that don't need reactivity at all (FPS, frame ms, a live counter on a HUD already inside a tick), write to the DOM directly via cached refs as shown in the previous section. pool.length is fine on that path because no observer is expecting to be notified.
The combination to avoid is a computed body that reads pool.length on a component without a tick. It will evaluate once against the empty pool, cache 0, and never re-run. In practice, pools and ticks travel together; if you don't have a tick, reach for a regular reactive array instead of a pool.
// WRONG - computed reads pool.length, evaluates once, never re-runs
wildflower.component('hud', {
computed: {
count() { return this.pool('enemies').length; }
}
});
data-pool-cull Requires left/top Positioning
Spatial culling (data-pool-cull) checks element positions via getBoundingClientRect(). This only works when entities use left/top CSS properties. Entities positioned with transform: translate() report their untransformed position (0,0), causing incorrect culling. Use left/top for pools that need culling, transform for pools that don't.
destroyComponent() Alone Doesn't Prevent Re-Initialization
The framework's mutation observer treats any element with a stale data-component-id (one whose instance is no longer in componentInstances) as a fresh component pending initialization. On the next scan — triggered by any DOM mutation or explicit wildflower.scan() — the stale id is stripped, a new instance is created, and its init() fires again. This is intentional: it lets third-party HTML caches (DataTables, jQuery plugins) replay cached DOM containing data-component-id attributes. To truly tear down, remove the element AND destroy the instance.
// WRONG - element stays in DOM, scanner re-inits a fresh instance
wildflower.destroyComponent(instance.id);
// CORRECT - both must happen (either order)
instance.element.remove();
wildflower.destroyComponent(instance.id);
For most teardown (removing UI), don't call destroyComponent() at all — just remove the element and the framework cleans up automatically via the mutation observer.
No data-bind-class Colon Syntax
<!-- WRONG - Colon syntax does NOT work -->
<div data-bind-class="isActive:active">
<!-- CORRECT - Object syntax (preferred) -->
<div data-bind-class="{ active: isActive }">
<!-- CORRECT - Ternary expression -->
<div data-bind-class="isActive ? 'active' : ''">
Complete Working Examples
Counter Component
<div data-component="counter">
<p>Count: <span data-bind="count"></span></p>
<button data-action="decrement">-</button>
<button data-action="increment">+</button>
<button data-action="reset">Reset</button>
</div>
<script>
wildflower.component('counter', {
state: {
count: 0
},
increment() {
this.count++;
},
decrement() {
this.count--;
},
reset() {
this.count = 0;
}
});
</script>
Todo List Component
<div data-component="todo-list">
<h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>
<form data-action="submit:addTodo">
<input type="text" data-model="newTodo" placeholder="What needs to be done?">
<button type="submit">Add</button>
</form>
<ul data-list="todos">
<template>
<li data-bind-class="completed ? 'done' : ''">
<input type="checkbox" data-model="completed">
<span data-bind="text"></span>
<button data-action="removeTodo">Delete</button>
</li>
</template>
</ul>
<div data-show="hasCompleted">
<button data-action="clearCompleted">Clear completed</button>
</div>
</div>
<script>
wildflower.component('todo-list', {
state: {
newTodo: '',
todos: []
},
computed: {
remaining() {
return this.todos.filter(t => !t.completed).length;
},
hasCompleted() {
return this.todos.some(t => t.completed);
}
},
addTodo(event) {
event.preventDefault();
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = '';
}
},
removeTodo(event, element, details) {
const index = details.index;
this.todos.splice(index, 1);
},
clearCompleted() {
this.todos = this.todos.filter(t => !t.completed);
}
});
</script>
Modal (click-outside + Esc-to-close)
The complete pattern (focus management, multiple dialog modes, accessibility, when to add a portal) is documented at Modals & Dialogs. The condensed form:
<div data-component="modal-example">
<button data-action="open">Open Modal</button>
<!-- data-event-self: close only when the scrim itself is clicked, not
when a click bubbles up from the content (no stopPropagation needed).
data-cloak: hide until the framework processes data-show (no flash).
Add data-portal="body" ONLY if an ancestor has transform/filter/
contain that traps position:fixed. -->
<div class="modal-overlay" data-show="isOpen"
data-action="close" data-event-self data-cloak>
<div class="modal-content">
<h2 data-bind="title"></h2>
<p data-bind="message"></p>
<button data-action="close">Close</button>
</div>
</div>
</div>
<script>
wildflower.component('modal-example', {
state: {
isOpen: false,
title: 'Modal Title',
message: 'Click outside or press Escape to close.'
},
open() { this.isOpen = true; },
close() { this.isOpen = false; },
// Esc-to-close is a global shortcut while the modal is open, so it
// belongs on document, not as data-event-key-escape on an element
// (that form is for shortcuts scoped to a focused widget).
init() {
this._onKey = (e) => { if (e.key === 'Escape' && this.isOpen) this.close(); };
document.addEventListener('keydown', this._onKey);
},
destroy() { document.removeEventListener('keydown', this._onKey); }
});
</script>
Search with Debounce
<div data-component="search-box">
<input
type="text"
data-model="query"
data-action="input:search"
data-event-debounce="300"
placeholder="Search...">
<div data-show="isLoading">Searching...</div>
<ul data-list="results" data-show="!isLoading">
<template>
<li data-bind="title"></li>
</template>
</ul>
<p data-show="noResults">No results found</p>
</div>
<script>
wildflower.component('search-box', {
state: {
query: '',
results: [],
isLoading: false
},
computed: {
noResults() {
return !this.isLoading &&
this.query.length > 0 &&
this.results.length === 0;
}
},
async search() {
if (!this.query.trim()) {
this.results = [];
return;
}
this.isLoading = true;
try {
const response = await fetch(`/api/search?q=${this.query}`);
this.results = await response.json();
} finally {
this.isLoading = false;
}
}
});
</script>
Quick Setup Template
Minimal HTML to start a WildflowerJS project:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My WildflowerJS App</title>
<!-- Scripts in head with defer for best performance -->
<script defer src="https://unpkg.com/wildflowerjs@1/dist/wildflower.min.js"></script>
<script defer src="app.js"></script>
</head>
<body>
<div data-component="my-app">
<h1>Hello, <span data-bind="name"></span>!</h1>
<input type="text" data-model="name" placeholder="Enter your name">
</div>
</body>
</html>
With app.js:
wildflower.component('my-app', {
state: {
name: 'World'
}
});
Testing WildflowerJS Components
Use @wildflowerjs/test-utils for component testing. The package provides utilities for framework loading, state management, and timing.
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) { ... }) — or a zero-arg form that reads this.X against the row — can be used as a bare reference (data-bind="X") or inside any expression in any binding type:
data-bind,data-bind-class,data-bind-style,data-bind-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
this.X resolves through the same ContextProxy as component methods: own props → computed → state → store. No special exceptions, no version-gated features.
Nested data-list with an item-level computed source
The framework reads item[path] first (fast path for raw fields) and falls back to evaluating an item-level computed of the same name when the field is undefined. Same authoring pattern as data-bind:
wildflower.component('thread', {
state: {
comments: [/* { id, body, reactions: { '👍': ['u-1', 'u-2'] } } */]
},
computed: {
// Per-comment reaction list. `this.reactions` is the raw object on
// the comment row; we project it to a chip array the inner data-list
// can iterate. No need to pre-build this on every comment.
reactionChips() {
if (this.id === undefined) return []
const c = this
return ['👍','❤️','🚀'].map(emoji => {
const users = (c.reactions && c.reactions[emoji]) || []
return {
id: c.id + ':' + emoji,
emoji,
chipClass: users.length > 0 ? 'chip is-active' : 'chip'
}
})
}
}
})
<div data-list="comments" data-key="id">
<template>
<div class="comment">
<p data-bind="body"></p>
<div class="reactions" data-list="reactionChips" data-key="id">
<template>
<button data-bind-class="chipClass" data-bind="emoji"></button>
</template>
</div>
</div>
</template>
</div>
Caveat: the fallback only fires when item[path] is undefined and path is a flat name (no dots). A raw field of the same name shadows the computed; for traversal lookups, use a flat-named computed.
item, index), functions without parameters are component-level (evaluated once per change).
wildflower.component('product-list', {
state: {
products: [
{ id: 1, name: 'Widget', price: 10 },
{ id: 2, name: 'Gadget', price: 20 }
],
taxRate: 0.1
},
computed: {
// Component-level (no parameters) - evaluated once at component level
cartTotal() {
return wildflower.getStore('cart').total;
},
// Item-level (has parameters) - evaluated per list item
// Signature: (item, index) - matches JS array method conventions
inCartQty(item) {
const cart = wildflower.getStore('cart');
return cart.items.find(i => i.id === item.id)?.qty || 0;
},
// Item-level computeds can call other item-level computeds
isInCart(item) {
return this.computed.inCartQty(item) > 0;
},
// Can access component state directly
priceWithTax(item) {
return '$' + (item.price * (1 + this.taxRate)).toFixed(2);
},
// Second parameter is the index
rowClass(item, index) {
return index % 2 === 0 ? 'row-even' : 'row-odd';
}
}
});
Template usage. Computed properties resolve by name, no prefix needed:
<div data-list="products" data-key="id">
<template>
<div class="product" data-bind-class="rowClass">
<span data-bind="name"></span>
<span data-bind="priceWithTax"></span>
<span class="badge" data-bind="inCartQty"></span>
<span data-show="isInCart">IN CART</span>
</div>
</template>
</div>
Key Features
- Automatic detection: Functions with parameters are item-level, functions without are component-level
- Store reactivity: When store state changes, only affected list item bindings update (not the whole list)
- Computed chaining:
this.computed.otherComputed(item)works for calling other item-level computeds - Full context access:
this.state,this.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.state.shares = { ...this.state.shares, [item.id]: ... } — re-runs only the affected row's bindings. This is the canonical pattern for per-item lookup maps keyed by item.id. See https://www.wildflowerjs.com/docs/lists#item-level-computed-properties for a live, runnable example.
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. Here's how to translate React patterns:
Core Concepts Mapping
| React | WildflowerJS | Notes |
|---|---|---|
useState(initialValue) |
state: { prop: initialValue } |
State is an object, not individual hooks |
useMemo(() => derived, [deps]) |
computed: { derived() { return ... } } |
Dependencies are tracked automatically |
useEffect(() => {}, [deps]) |
watch: { 'prop': fn } or init() |
Watch for reactive effects, init for mount |
useRef() |
document.querySelector() in init() |
Direct DOM access when needed |
useContext() |
$entity.path or stores |
Cross-component communication |
useCallback() |
Methods on component | Methods are stable by default |
useReducer() |
Methods that update state | No need for reducers, update state directly |
| Props | props: {} + data-prop-* |
Explicit prop definitions with validation |
| Children / slots | data-slot |
Named slots for content projection |
JSX to HTML Attribute Mapping
| React JSX | WildflowerJS HTML |
|---|---|
<span>{count}</span> |
<span data-bind="count"></span> |
<span>{user.name}</span> |
<span data-bind="user.name"></span> |
<span>{doubleCount}</span> (derived) |
<span data-bind="doubleCount"></span> |
<div dangerouslySetInnerHTML={{__html: html}}/> |
<div data-bind-html="htmlContent"></div> |
<input value={val} onChange={e => setVal(e.target.value)}/> |
<input data-model="val"> |
<button onClick={handleClick}> |
<button data-action="handleClick"> |
<button onClick={() => setCount(c => c+1)}> |
<button data-action="increment"> |
<input onChange={handleChange}/> |
<input data-action="change:handleChange"> |
<input onInput={handleInput}/> |
<input data-action="input:handleInput"> |
{isVisible && <div>...</div>} |
<div data-show="isVisible">...</div> |
{isVisible ? <A/> : <B/>} |
<div data-show="isVisible">A</div><div data-show="!isVisible">B</div> |
{items.map(item => <li key={item.id}>{item.name}</li>)} |
<ul data-list="items"><template><li data-bind="name"></li></template></ul> |
className={isActive ? 'active' : ''} |
data-bind-class="{ active: isActive }" |
className={`btn ${variant}`} |
class="btn" data-bind-class="variant" |
style={{backgroundColor: color}} |
data-bind-style="{ backgroundColor: color }" |
Complete React to WildflowerJS Example
React Version:
import { useState, useMemo } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
const [newTodo, setNewTodo] = useState('');
const remaining = useMemo(() =>
todos.filter(t => !t.completed).length,
[todos]
);
const addTodo = (e) => {
e.preventDefault();
if (newTodo.trim()) {
setTodos([...todos, { text: newTodo.trim(), completed: false }]);
setNewTodo('');
}
};
const toggleTodo = (index) => {
const updated = [...todos];
updated[index].completed = !updated[index].completed;
setTodos(updated);
};
const removeTodo = (index) => {
setTodos(todos.filter((_, i) => i !== index));
};
return (
<div>
<h2>Todo List ({remaining} remaining)</h2>
<form onSubmit={addTodo}>
<input
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo, index) => (
<li key={index} className={todo.completed ? 'done' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(index)}
/>
<span>{todo.text}</span>
<button onClick={() => removeTodo(index)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
WildflowerJS Version:
<div data-component="todo-list">
<h2>Todo List (<span data-bind="remaining"></span> remaining)</h2>
<form data-action="submit:addTodo">
<input data-model="newTodo" placeholder="What needs to be done?">
<button type="submit">Add</button>
</form>
<ul data-list="todos">
<template>
<li data-bind-class="completed ? 'done' : ''">
<input type="checkbox" data-model="completed">
<span data-bind="text"></span>
<button data-action="removeTodo">Delete</button>
</li>
</template>
</ul>
</div>
<script>
wildflower.component('todo-list', {
state: {
todos: [],
newTodo: ''
},
computed: {
remaining() {
return this.todos.filter(t => !t.completed).length;
}
},
addTodo(event) {
event.preventDefault();
if (this.newTodo.trim()) {
this.todos.push({
text: this.newTodo.trim(),
completed: false
});
this.newTodo = '';
}
},
removeTodo(event, element, details) {
const index = details.index;
this.todos.splice(index, 1);
}
});
</script>
Key Differences from React
- No JSX: Use HTML with data attributes instead of JSX syntax
- No build step: Works directly in browser, no webpack/vite needed
- Mutable state: Update
this.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. Here's the translation guide:
Options API Mapping
| Vue Options API | WildflowerJS | Notes |
|---|---|---|
data() { return {} } |
state: {} |
Object directly, not a function |
computed: {} |
computed: {} |
Identical pattern |
methods: {} |
Methods directly on component | No methods wrapper needed |
watch: {} |
watch: {} |
Identical pattern |
mounted() |
init() |
Called after component mounts |
beforeUnmount() |
destroy() |
Cleanup before removal |
props: {} |
props: {} |
Similar prop definitions |
emits: [] |
this.emit('event', detail) |
Parent handles via onEvent(detail) method |
Template Directive Mapping
| Vue Template | WildflowerJS HTML |
|---|---|
{{ message }} |
<span data-bind="message"></span> |
{{ computed }} |
<span data-bind="computedName"></span> |
v-html="rawHtml" |
data-bind-html="rawHtml" |
v-model="text" |
data-model="text" |
v-model.number="count" |
<input data-model="count" data-model-number> |
v-show="isVisible" |
data-show="isVisible" |
v-if="condition" |
data-render="condition" |
v-else |
data-show="!condition" (no direct equivalent) |
v-for="item in items" :key="item.id" |
<div data-list="items"><template>...</template></div> |
v-for="(item, index) in items" |
Index via data-bind="_index" in templates or details.index in actions |
@click="handleClick" |
data-action="handleClick" |
@click.prevent="handleClick" |
data-action="handleClick" data-event-prevent |
@click.stop="handleClick" |
data-action="handleClick" data-event-stop |
@input="onInput" |
data-action="input:onInput" |
@keyup.enter="submit" |
data-action="keyup:submit" data-event-key-enter |
@input.debounce="search" |
data-action="input:search" data-event-debounce="300" |
:class="{ active: isActive }" |
data-bind-class="{ active: isActive }" |
:class="[baseClass, { active: isActive }]" |
class="baseClass" data-bind-class="{ active: isActive }" |
:style="{ color: textColor }" |
data-bind-style="{ color: textColor }" |
:disabled="isDisabled" |
data-bind-attr="{ disabled: isDisabled }" |
<slot></slot> |
<div data-slot="default"></div> |
<slot name="header"></slot> |
<div data-slot="header"></div> |
<Teleport to="body"> |
data-portal="body" |
Complete Vue to WildflowerJS Example
Vue Version:
<template>
<div>
<h2>Counter: {{ count }}</h2>
<p>Double: {{ doubleCount }}</p>
<button @click="decrement" :disabled="count <= 0">-</button>
<button @click="increment">+</button>
<button @click="reset">Reset</button>
<div v-show="count > 10" class="warning">
That's a lot!
</div>
<ul>
<li v-for="(item, index) in history" :key="index">
{{ item.action }}: {{ item.value }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
history: []
};
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
this.history.push({ action: 'increment', value: this.count });
},
decrement() {
if (this.count > 0) {
this.count--;
this.history.push({ action: 'decrement', value: this.count });
}
},
reset() {
this.count = 0;
this.history = [];
}
}
};
</script>
WildflowerJS Version:
<div data-component="counter">
<h2>Counter: <span data-bind="count"></span></h2>
<p>Double: <span data-bind="doubleCount"></span></p>
<button data-action="decrement">-</button>
<button data-action="increment">+</button>
<button data-action="reset">Reset</button>
<div data-show="showWarning" class="warning">
That's a lot!
</div>
<ul data-list="history">
<template>
<li>
<span data-bind="action"></span>: <span data-bind="value"></span>
</li>
</template>
</ul>
</div>
<script>
wildflower.component('counter', {
state: {
count: 0,
history: []
},
computed: {
doubleCount() {
return this.count * 2;
},
showWarning() {
return this.count > 10;
}
},
increment() {
this.count++;
this.history.push({ action: 'increment', value: this.count });
},
decrement() {
if (this.count > 0) {
this.count--;
this.history.push({ action: 'decrement', value: this.count });
}
},
reset() {
this.count = 0;
this.history = [];
}
});
</script>
Key Differences from Vue
- No SFC: No Single File Components - HTML and JS are separate or inline
- No build step: No vue-loader or vite plugin needed
- State access: Use
this.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. Here's how to translate:
Core Concepts Mapping
| Svelte | WildflowerJS | Notes |
|---|---|---|
let count = 0; (reactive) |
state: { count: 0 } |
All state in state object |
$: doubled = count * 2; |
computed: { doubled() { return this.count * 2; } } |
Computed properties |
$: { console.log(count); } |
watch: { count(newVal) { console.log(newVal); } } |
Reactive statements become watchers |
export let prop; |
props: { prop: { type: String } } |
Explicit prop definitions |
onMount(() => {}) |
init() {} |
Lifecycle on mount |
onDestroy(() => {}) |
destroy() {} |
Lifecycle on destroy |
createEventDispatcher() |
$entity.path or custom events |
Component communication |
Stores (writable, readable) |
wildflower.store() + subscribe |
Global state management |
Template Syntax Mapping
| Svelte Template | WildflowerJS HTML |
|---|---|
{count} |
<span data-bind="count"></span> |
{@html rawHtml} |
<div data-bind-html="rawHtml"></div> |
bind:value={text} |
data-model="text" |
bind:checked={isChecked} |
data-model="isChecked" |
bind:group={selected} |
data-model="selected" on each radio |
on:click={handleClick} |
data-action="handleClick" |
on:click|preventDefault={fn} |
data-action="fn" data-event-prevent |
on:click|stopPropagation={fn} |
data-action="fn" data-event-stop |
on:keydown|self={fn} |
data-action="keydown:fn" |
{#if condition}...{/if} |
<div data-show="condition">...</div> |
{#if condition}...{:else}...{/if} |
<div data-show="condition">...</div><div data-show="!condition">...</div> |
{#each items as item}...{/each} |
<div data-list="items"><template>...</template></div> |
{#each items as item, index}...{/each} |
Index via data-bind="_index" in templates or details.index in actions |
{#each items as item (item.id)}...{/each} |
Keyed rendering automatic when items have id |
class:active={isActive} |
data-bind-class="{ active: isActive }" |
style:color={textColor} |
data-bind-style="{ color: textColor }" |
<slot></slot> |
<div data-slot="default"></div> |
<slot name="header"></slot> |
<div data-slot="header"></div> |
transition:fade |
data-transition="fade" |
Complete Svelte to WildflowerJS Example
Svelte Version:
<script>
let items = [];
let newItem = '';
$: itemCount = items.length;
$: hasItems = items.length > 0;
function addItem() {
if (newItem.trim()) {
items = [...items, { text: newItem.trim(), done: false }];
newItem = '';
}
}
function toggleItem(index) {
items[index].done = !items[index].done;
items = items; // Trigger reactivity
}
function removeItem(index) {
items = items.filter((_, i) => i !== index);
}
</script>
<div>
<h2>Items: {itemCount}</h2>
<form on:submit|preventDefault={addItem}>
<input bind:value={newItem} placeholder="New item">
<button type="submit">Add</button>
</form>
{#if hasItems}
<ul>
{#each items as item, index}
<li class:done={item.done}>
<input type="checkbox" bind:checked={item.done}>
<span>{item.text}</span>
<button on:click={() => removeItem(index)}>X</button>
</li>
{/each}
</ul>
{:else}
<p>No items yet</p>
{/if}
</div>
WildflowerJS Version:
<div data-component="item-list">
<h2>Items: <span data-bind="itemCount"></span></h2>
<form data-action="submit:addItem">
<input data-model="newItem" placeholder="New item">
<button type="submit">Add</button>
</form>
<ul data-list="items" data-show="hasItems">
<template>
<li data-bind-class="done ? 'done' : ''">
<input type="checkbox" data-model="done">
<span data-bind="text"></span>
<button data-action="removeItem">X</button>
</li>
</template>
</ul>
<p data-show="!hasItems">No items yet</p>
</div>
<script>
wildflower.component('item-list', {
state: {
items: [],
newItem: ''
},
computed: {
itemCount() {
return this.items.length;
},
hasItems() {
return this.items.length > 0;
}
},
addItem(event) {
event.preventDefault();
if (this.newItem.trim()) {
this.items.push({
text: this.newItem.trim(),
done: false
});
this.newItem = '';
}
},
removeItem(event, element, details) {
const index = details.index;
this.items.splice(index, 1);
}
});
</script>
Key Differences from Svelte
- No compilation: WildflowerJS runs directly in browser, no build needed
- Explicit state: All reactive variables go in
state: {} - Direct mutation: Use
push,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 (required when calling item-level computeds with arguments)
Quick Reference
| Context | Syntax | Example |
|---|---|---|
| HTML template (any binding) | Bare name (prefix optional) | data-bind="fullName" |
| HTML expression | Bare name in expression | data-bind="total > 100 ? 'High' : 'Low'" |
| Inside list template | Bare name (prefix optional) | data-show="isInCart" |
| JavaScript methods/computed | Bare name (auto-resolved) | this.count, this.fullName |
| Item-level computed chaining | this.computed.X(item) (required) |
this.computed.inCartQty(item) |
$entity.path in templates |
Not needed (auto-resolved) | data-bind="$comp.name" |