Component Definition API
Complete reference for defining WildflowerJS components.
Quick Reference
Basic Structure
Components are registered using wildflower.component(name, definition):
wildflower.component('component-name', {
// Initial state
state: { /* ... */ },
// Derived values
computed: { /* ... */ },
// Props from parent
props: { /* ... */ },
// Type validation (dev builds)
types: { /* ... */ },
// State change watchers
watch: { /* ... */ },
// Lifecycle: called after component mounts
init() { /* ... */ },
// Lifecycle: called when component is destroyed
destroy() { /* ... */ },
// Lifecycle: called after any state change
onUpdate(changedPaths) { /* ... */ },
// Custom methods - available on this
myMethod() { /* ... */ },
anotherMethod() { /* ... */ }
});
state
Defines the component's reactive state. Changes to state properties automatically update bound DOM elements.
Type
state: object
Description
The state object contains all reactive data for the component. It can include primitives, objects, and arrays. Changes are tracked at the property level using JavaScript Proxies.
Example
wildflower.component('user-card', {
state: {
// Primitive values
name: 'Alice',
age: 28,
isAdmin: false,
// Nested objects
address: {
city: 'Portland',
country: 'USA'
},
// Arrays
tags: ['developer', 'designer'],
// Can be null/undefined initially
selectedItem: null
}
});
Accessing State
// In methods
this.name = 'Bob'; // Update triggers DOM update
this.address.city = 'NYC'; // Nested updates work
this.tags.push('artist'); // Array methods trigger updates
// Reading
const name = this.name;
const city = this.address.city;
Notes
- State is made reactive during component initialization
- Array methods (
push,pop,splice, etc.) trigger updates - Deep nested changes are tracked automatically
- Flat access is idiomatic:
this.countworks because ContextProxy auto-resolves state and computed properties.this.state.countalso works but is more verbose. Never destructure state (const { count } = this) if you need reactivity; the destructured value is a snapshot, not reactive.
computed
Defines derived values that automatically recalculate when their dependencies change.
Type
computed: {
[propertyName: string]: () => any
}
Description
Computed properties are getter functions that derive values from state. They're cached and only recalculate when their dependencies change.
Example
wildflower.component('profile', {
state: {
firstName: 'John',
lastName: 'Doe',
items: [
{ price: 10, quantity: 2 },
{ price: 25, quantity: 1 }
],
taxRate: 0.08
},
computed: {
// Simple derived value
fullName() {
return `${this.firstName} ${this.lastName}`;
},
// Computed from array
subtotal() {
return this.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
},
// Computed from other computed
total() {
const sub = this.subtotal;
return sub + (sub * this.taxRate);
},
// Boolean computed
hasItems() {
return this.items.length > 0;
},
// Filtered list
expensiveItems() {
return this.items.filter(item => item.price > 20);
}
}
});
HTML Usage
<div data-component="profile">
<h2 data-bind="fullName"></h2>
<p>Subtotal: $<span data-bind="subtotal"></span></p>
<p>Total: $<span data-bind="total"></span></p>
<!-- In conditionals -->
<div data-show="hasItems">
You have items in your cart
</div>
<!-- As list source -->
<ul data-list="expensiveItems">
<template><li data-bind="name"></li></template>
</ul>
</div>
Accessing in Methods
// Access computed values directly on this
someMethod() {
const name = this.fullName;
const total = this.total;
}
In List Templates
Computed properties can access list item context:
wildflower.component('order', {
state: {
items: [
{ name: 'Widget', price: 10, qty: 2 },
{ name: 'Gadget', price: 25, qty: 1 }
]
},
computed: {
// When used in a list template, 'this' has item properties
lineTotal() {
return (this.price || 0) * (this.qty || 0);
}
}
});
<ul data-list="items">
<template>
<li>
<span data-bind="name"></span>:
$<span data-bind="lineTotal"></span>
</li>
</template>
</ul>
props
Defines properties that can be passed from parent components or HTML attributes.
Type
props: {
[propName: string]: {
type?: 'string' | 'number' | 'boolean' | 'object' | 'array',
default?: any,
required?: boolean
}
} | string[]
Description
Props allow parent components to pass data to children. Props are read from data-prop-* attributes on the component element.
Simple Array Syntax
wildflower.component('greeting', {
props: ['name', 'greeting'],
computed: {
message() {
return `${this.props.greeting || 'Hello'}, ${this.props.name}!`;
}
}
});
Object Syntax with Validation
wildflower.component('user-badge', {
props: {
userId: {
type: 'number',
required: true
},
size: {
type: 'string',
default: 'medium'
},
showAvatar: {
type: 'boolean',
default: true
}
},
init() {
console.log('User ID:', this.props.userId);
console.log('Size:', this.props.size);
}
});
HTML Usage
<!-- Props passed via data-prop-* attributes -->
<div data-component="user-badge"
data-prop-user-id="42"
data-prop-size="large"
data-prop-show-avatar="true">
</div>
<!-- In a list context, pass item data -->
<ul data-list="users">
<template>
<li>
<div data-component="user-badge"
data-prop-user-id="id">
</div>
</li>
</template>
</ul>
Notes
- Props are converted to camelCase:
data-prop-user-idbecomesthis.props.userId - Type coercion:
"42"becomes42for number type,"true"becomestruefor boolean - Props are read-only - don't modify them directly
- In dev builds, type validation warnings appear in console
types
Defines runtime type validation for state properties (development builds only).
Type
types: {
[propertyPath: string]: 'string' | 'number' | 'boolean' |
'object' | 'array' | 'function' | 'any'
}
Description
When running a development build, the framework validates state changes against declared types and logs warnings for mismatches.
Example
wildflower.component('typed-form', {
state: {
name: '',
age: 0,
isActive: true,
tags: [],
metadata: {}
},
types: {
name: 'string',
age: 'number',
isActive: 'boolean',
tags: 'array',
metadata: 'object'
},
updateAge(newAge) {
// In dev build: warns if newAge isn't a number
this.age = newAge;
}
});
Nested Property Types
wildflower.component('nested-types', {
state: {
user: {
name: '',
profile: {
bio: '',
age: 0
}
}
},
types: {
'user.name': 'string',
'user.profile.bio': 'string',
'user.profile.age': 'number'
}
});
Notes
- Type validation is stripped from production builds for performance
- Warnings appear in the console but don't throw errors
- Use with TypeScript definitions for full type safety
watch
Defines callbacks that run when specific state properties change.
Type
watch: {
[propertyPath: string]: (newValue: any, oldValue: any) => void
}
Description
Watchers let you react to specific state changes with custom logic, useful for side effects like API calls, localStorage updates, or analytics.
Example
wildflower.component('settings', {
state: {
theme: 'light',
language: 'en',
notifications: true
},
watch: {
// Simple watcher
theme(newTheme, oldTheme) {
document.body.className = `theme-${newTheme}`;
console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
},
// Watcher with side effect
language(newLang) {
this.loadTranslations(newLang);
},
// Boolean watcher
notifications(enabled) {
if (enabled) {
this.requestNotificationPermission();
}
}
},
loadTranslations(lang) {
// Load language file...
},
requestNotificationPermission() {
// Request browser permission...
}
});
Watching Nested Properties
wildflower.component('profile', {
state: {
user: {
name: '',
settings: {
darkMode: false
}
}
},
watch: {
// Watch nested property
'user.settings.darkMode'(isDark) {
document.body.classList.toggle('dark', isDark);
}
}
});
Notes
- Watchers run after the DOM has been updated
- Use dot notation for nested property paths
- Watchers receive both new and old values
- Avoid modifying watched properties in the watcher (can cause loops)
beforeInit()
Lifecycle method called after methods are bound but before bindings are processed.
Signature
beforeInit(): void
Description
Called once after the component's methods are bound to the context, but before data-bind, data-action, and other bindings are processed. Useful for setting up initial state that depends on props or list item data before the DOM is rendered.
Available Context
beforeInit() {
this.element; // The root DOM element
this.state; // Reactive state object
this.props; // Read-only props from parent
this.listItem; // List item data (if inside data-list)
this.stores; // Subscribed store references
this.myMethod(); // Custom methods are available
}
Example
wildflower.component('item-card', {
state: {
displayName: '',
priority: 'normal'
},
props: ['itemId'],
beforeInit() {
// Set up state from list item data before bindings render
if (this.listItem) {
this.displayName = this.listItem.name;
this.priority = this.listItem.priority || 'normal';
}
}
});
Notes
- Called before any DOM bindings are processed; the DOM still shows initial template content
this.listItemis already available at this point- Use this for pre-binding state setup; use
init()for post-binding logic
init()
Lifecycle method called after the component mounts and initial bindings are created.
Signature
init(): void
Description
Called once after the component element is bound and all child bindings are set up. Use for initialization logic, fetching data, setting up subscriptions, etc.
Available Context
init() {
// Component element
this.element; // The root DOM element
// State and computed
this.state; // Reactive state object
this.computed; // Computed properties
// Props from parent
this.props; // Read-only props
// Component identity
this.name; // Component name (e.g., 'user-card')
this.id; // Unique component instance ID
// Methods are available
this.myMethod(); // Call your own methods
}
Example
wildflower.component('data-loader', {
state: {
data: null,
loading: true,
error: null
},
props: ['endpoint'],
async init() {
try {
const response = await fetch(this.props.endpoint);
this.data = await response.json();
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
}
});
Common Patterns
init() {
// DOM queries within component
this.canvas = this.element.querySelector('canvas');
// Event listeners for non-declarative events
window.addEventListener('resize', this.handleResize);
// Store subscriptions
wildflower.getStore('auth').subscribe(
'isLoggedIn',
(value) => this.loggedIn = value
);
// Timers
this.interval = setInterval(() => this.tick(), 1000);
}
Notes
- Called after all
data-bind,data-action, etc. are processed - Child components may not be initialized yet
- Can be async - framework doesn't await it
- Clean up any subscriptions/listeners in
destroy()
beforeDestroy()
Lifecycle method called before component cleanup starts.
Signature
beforeDestroy(): void
Description
Called once before the component's cleanup begins. Mirrors beforeInit() for teardown. Use it for any logic that must run while the component's bindings, subscriptions, and child components are still active.
Example
wildflower.component('dashboard-panel', {
state: {
visible: true
},
beforeDestroy() {
// Notify other components while still fully active
const metrics = wildflower.getStore('metrics');
metrics.panelClosed(this.element.id);
},
destroy() {
// Standard cleanup: bindings already being torn down
clearInterval(this.interval);
}
});
Notes
- Called before
destroy(); bindings and children are still active - The plugin system hooks into this as
component:beforeDestroy - Use for pre-teardown communication; use
destroy()for resource cleanup
destroy()
Lifecycle method called before the component is removed from the DOM.
Signature
destroy(): void
Description
Called when the component is about to be destroyed. Use for cleanup: removing event listeners, clearing timers, unsubscribing from stores, etc.
Example
wildflower.component('live-chart', {
state: { data: [] },
init() {
// Set up listeners and intervals
this.handleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.handleResize);
this.interval = setInterval(() => this.fetchData(), 5000);
this.unsubscribe = wildflower.getStore('metrics')
.subscribe('values', (v) => this.data = v);
},
destroy() {
// Clean up everything
window.removeEventListener('resize', this.handleResize);
clearInterval(this.interval);
this.unsubscribe();
},
handleResize() {
// Resize chart...
},
fetchData() {
// Fetch new data...
}
});
When destroy() is Called
- Component element is removed from DOM
data-rendercondition becomes false- Parent component is destroyed
wildflower.destroyComponent(id)is called- Framework's
destroy()method is called
beforeUpdate()
Lifecycle method called before DOM updates are applied.
Signature
beforeUpdate(): void
Description
Called before the framework applies DOM updates after a state change. Useful for capturing DOM measurements or state before the DOM is modified (e.g., scroll positions, element dimensions).
Example
wildflower.component('chat-log', {
state: {
messages: []
},
beforeUpdate() {
// Capture scroll position before DOM updates
const container = this.find('.messages');
this._wasAtBottom = container.scrollTop + container.clientHeight
>= container.scrollHeight - 10;
},
onUpdate() {
// Restore scroll position after DOM updates
if (this._wasAtBottom) {
const container = this.find('.messages');
container.scrollTop = container.scrollHeight;
}
}
});
Notes
- Called before every DOM update cycle; use sparingly for performance
- No parameters are passed
- The DOM still reflects the previous state when this runs
onUpdate()
Lifecycle method called after any state change and DOM update.
Signature
onUpdate(changedPaths?: string[]): void
Description
Called after every state change once the DOM has been updated. Useful for side effects that need to run after any change, or for integrating with third-party libraries that need to know when the DOM changed.
Parameters
| Parameter | Type | Description |
|---|---|---|
changedPaths |
string[] |
Array of state paths that changed (e.g., ['count', 'user.name']) |
Example
wildflower.component('chart', {
state: {
data: [],
options: { type: 'bar' }
},
init() {
this.chart = new ThirdPartyChart(
this.element.querySelector('.chart-container'),
this.data,
this.options
);
},
onUpdate(changedPaths) {
// Re-render chart when data changes
if (changedPaths.includes('data')) {
this.chart.setData(this.data);
}
// Update chart type when options change
if (changedPaths.some(p => p.startsWith('options'))) {
this.chart.setOptions(this.options);
}
},
destroy() {
this.chart.destroy();
}
});
Notes
- Called after every state change - use sparingly for performance
- DOM is already updated when this runs
- Check
changedPathsto avoid unnecessary work - Don't modify state in
onUpdateunless absolutely necessary (can cause loops)
tick(dt, now)
Lifecycle method called every animation frame via requestAnimationFrame.
Signature
tick(dt: number, now: number): void
Description
Called on every animation frame when defined. Components with a tick method are automatically registered in the framework's shared requestAnimationFrame loop. Pool flush happens after all tick callbacks run.
Parameters
| Parameter | Type | Description |
|---|---|---|
dt |
number |
Milliseconds since the last frame, clamped to a maximum of 250ms to prevent large jumps after tab suspension |
now |
number |
Current timestamp from requestAnimationFrame |
Example
wildflower.component('particle-system', {
state: {
x: 0,
y: 0,
velocityX: 100, // pixels per second
velocityY: 50
},
tick(dt, now) {
// dt is in milliseconds; convert to seconds for physics
const seconds = dt / 1000;
this.x += this.velocityX * seconds;
this.y += this.velocityY * seconds;
}
});
Notes
- The rAF loop starts automatically when any component with
tickis initialized - The loop stops when all tickable components are destroyed
dtis clamped to 250ms to prevent physics explosions after tab suspension- Pool flush runs after tick callbacks, so pool mutations in
tickare batched efficiently
onError(error, context)
Error boundary hook called when an error occurs during any lifecycle phase.
Signature
onError(error: Error, context: { lifecycle: string }): void
Description
Provides per-component error handling. When defined, the framework routes errors from lifecycle methods (init, beforeInit, onUpdate, etc.) to this handler instead of throwing.
Parameters
| Parameter | Type | Description |
|---|---|---|
error |
Error |
The error that occurred |
context |
object |
Object with a lifecycle key indicating where the error occurred (e.g., 'init', 'beforeInit', 'subscribe-wait') |
Example
wildflower.component('resilient-widget', {
state: {
data: null,
errorMessage: null
},
async init() {
const response = await fetch('/api/data');
this.data = await response.json();
},
onError(error, context) {
console.warn(`Error in ${context.lifecycle}:`, error.message);
this.errorMessage = 'Something went wrong. Please try again.';
}
});
Notes
- Acts as an error boundary for the component's own lifecycle methods
- The
context.lifecyclekey tells you which phase failed - Use
this.resetError()to recover from an error state
onPropsChange(propsChangeInfo)
Lifecycle method called when parent-passed props change.
Signature
onPropsChange(propsChangeInfo: object): void
Description
Called when one or more props passed from a parent component change. Computed properties are flushed before this hook runs, so derived values are up to date.
Example
wildflower.component('filtered-list', {
props: {
filter: { type: 'string', default: '' }
},
state: {
items: [],
filteredItems: []
},
onPropsChange(info) {
// Re-filter when the parent changes the filter prop
this.filteredItems = this.items.filter(
item => item.name.includes(this.props.filter)
);
}
});
Notes
- Only called when props actually change, not on initial render
- Computed properties are already recalculated when this runs
- The
propsChangeInfoparameter contains information about which props changed
Methods
Custom methods defined at the top level of the component definition.
Description
Any function defined at the top level of the component definition becomes a method, available on this and bindable via data-action.
Example
wildflower.component('counter', {
state: { count: 0 },
// These are all methods
increment() {
this.count++;
},
decrement() {
this.count--;
},
reset() {
this.count = 0;
},
setCount(value) {
this.count = value;
},
// Async methods work too
async fetchCount() {
const response = await fetch('/api/count');
const data = await response.json();
this.count = data.count;
},
// Private convention: prefix with underscore
_validateCount(value) {
return typeof value === 'number' && !isNaN(value);
}
});
Action Handler Signature
Methods bound via data-action receive these parameters:
handleClick(event, element, context) {
// event - The DOM event object
// element - The element that triggered the event
// context - Object with additional info:
// context.index - Index when in a list
// context.item - Current item when in a list
}
List Action Example
wildflower.component('todo-list', {
state: {
todos: [
{ id: 1, text: 'Task 1', done: false },
{ id: 2, text: 'Task 2', done: true }
]
},
toggle(event, element, context) {
// context.index is the position in the list
this.todos[context.index].done =
!this.todos[context.index].done;
},
remove(event, element, context) {
this.todos.splice(context.index, 1);
}
});
Instance Properties
Properties available on this inside component methods.
| Property | Type | Description |
|---|---|---|
this.element |
HTMLElement |
The component's root DOM element |
this.state |
object |
Reactive state object |
this.computed |
object |
Computed properties (read via getters) |
this.props |
object |
Read-only props from parent |
this.name |
string |
Component name (e.g., 'user-card') |
this.id |
string |
Unique component instance ID |
this.listItem |
object | null |
List item data when this component is rendered inside a data-list. null if not in a list. Available from beforeInit() onward. |
this.pools |
object |
Object containing pool handles when pools: {} is declared in the component definition |
this.stores |
object |
Object containing subscribed store references declared via subscribe:. Access as this.stores.storeName. |
this.emit(eventName, data) |
function |
Emit a custom event to the parent component. Parent handles via onEventName method. |
this.$el(selector) |
function |
Scoped jQuery-like DOM query helper (WildQuery). Returns a chainable wrapper scoped to the component element. |
this.find(selector) |
function |
Scoped querySelector: searches only within this component's element |
this.findAll(selector) |
function |
Scoped querySelectorAll: searches only within this component's element |
this.closest(selector) |
function |
Traverses up the DOM from the component's root element using Element.closest() |
Example
wildflower.component('example', {
state: { count: 0 },
computed: {
doubled() { return this.count * 2; }
},
init() {
console.log('Element:', this.element);
console.log('Name:', this.name); // 'example'
console.log('ID:', this.id); // 'example-abc123'
console.log('Count:', this.count);
console.log('Doubled:', this.doubled);
}
});