Communication
WildflowerJS provides several mechanisms for components to share data, call methods on each other, and react to changes across the application.
External State Access
For HTML bindings, use the $ accessor to read state from any entity (store, component, or plugin) directly in templates:
<!-- Bind directly to another entity's state -->
<span data-bind="$user-session.user.name"></span>
<span data-show="$user-session.isLoggedIn">Welcome!</span>
<span data-bind="$cart.itemCount"></span>
For JavaScript access, use subscribe with this.stores for stores, or wildflower.getComponent() for components. The proxy auto-resolves computed vs state properties:
<div class="card">
<div class="card-body">
<!-- Store-backed user session -->
<div data-component="user-controls" class="p-3 border-bottom">
<p>User: <span data-bind="$user-session.user.name"></span></p>
<p>Status: <span data-bind="$user-session.user.isLoggedIn"></span></p>
<p>Points: <span data-bind="$user-session.user.points"></span></p>
<button class="btn btn-primary" data-action="toggleLogin">
Toggle Login
</button>
<button class="btn btn-secondary ms-2" data-action="addPoints">
+10 Points
</button>
</div>
<!-- Component subscribing to the store -->
<div data-component="welcome-banner" class="p-3">
<h5>Welcome Banner</h5>
<p data-bind="welcomeText" class="text-success fs-5"></p>
<small class="text-muted">Uses subscribe + this.stores - auto-updates when store changes</small>
</div>
</div>
</div>
// Store with shared state
wildflower.store('user-session', {
state: {
user: { name: 'John Doe', isLoggedIn: true, points: 75 }
},
toggleLogin() {
this.user = {
...this.user,
isLoggedIn: !this.user.isLoggedIn,
name: !this.user.isLoggedIn ? 'John Doe' : 'Guest'
}
},
addPoints() {
this.user.points += 10
}
})
// Controls component calls store methods
wildflower.component('user-controls', {
subscribe: {
'user-session': ['user']
},
toggleLogin() {
this.stores['user-session'].toggleLogin()
},
addPoints() {
this.stores['user-session'].addPoints()
}
})
// Subscribe to store for reactive JS access
wildflower.component('welcome-banner', {
subscribe: {
'user-session': ['user']
},
computed: {
welcomeText() {
const user = this.stores['user-session'].user
return user.isLoggedIn
? `Welcome back, ${user.name}!`
: 'Please log in to continue'
}
}
})
Cross-Component Method Calls
Use wildflower.getComponent() to get a component instance and call its methods directly:
<div class="card">
<div class="card-body">
<!-- Counter Service Component -->
<div data-component="counter-service" class="p-3 border-bottom">
<p>Current count: <span data-bind="count" class="badge bg-primary fs-5"></span></p>
<p class="text-muted small">This component exposes methods for other components to call</p>
</div>
<!-- Controller Component -->
<div data-component="counter-controller" class="p-3">
<h5>Controller</h5>
<div class="d-flex gap-2">
<button class="btn btn-success" data-action="incrementCounter">+1</button>
<button class="btn btn-warning" data-action="incrementBy" data-amount="5">+5</button>
<button class="btn btn-danger" data-action="resetCounter">Reset</button>
</div>
<p class="text-muted small mt-2">Uses getComponent() to call counter-service methods directly</p>
</div>
</div>
</div>
// Service component with callable methods
wildflower.component('counter-service', {
state: {
count: 0
},
increment() {
this.count++
},
incrementBy(amount) {
this.count += amount
},
reset() {
this.count = 0
}
})
// Controller component that calls methods on counter-service
wildflower.component('counter-controller', {
incrementCounter() {
const counter = wildflower.getComponent('counter-service')
if (counter) {
counter.increment()
}
},
incrementBy(event, element) {
const amount = parseInt(element.dataset.amount) || 1
const counter = wildflower.getComponent('counter-service')
if (counter) {
counter.incrementBy(amount)
}
},
resetCounter() {
const counter = wildflower.getComponent('counter-service')
if (counter) {
counter.reset()
}
}
})
Use wildflower.getComponents(name) when there are multiple instances of the same component type:
// Dismiss all notification toasts
const notifications = wildflower.getComponents('notification-toast')
notifications.forEach(n => n.dismiss())
// Find a specific instance
const urgent = notifications.find(n => n.state.priority === 'high')
if (urgent) {
urgent.highlight()
}
Component Events (this.emit)
Use this.emit() for child-to-parent communication. The child emits a named event, and the parent handles it with an on-prefixed method (e.g., emit('select') calls the parent's onSelect()):
<div class="card">
<div class="card-body">
<!-- Parent Component -->
<div data-component="task-list">
<h5>Tasks</h5>
<p>Last action: <span data-bind="lastAction" class="text-primary"></span></p>
<ul class="list-unstyled" data-list="tasks" data-key="id">
<template>
<li class="border rounded p-2 mb-2">
<div data-component="task-item"
data-prop-task="."
class="d-flex align-items-center justify-content-between w-100">
<span data-bind="props.task.text"></span>
<span class="d-flex gap-2">
<button class="btn btn-sm btn-success"
data-action="complete">Done</button>
<button class="btn btn-sm btn-danger"
data-action="remove">Remove</button>
</span>
</div>
</li>
</template>
</ul>
</div>
</div>
</div>
// Child component emits events to parent
wildflower.component('task-item', {
props: {
task: { type: Object, required: true }
},
complete() {
// Emit 'complete' → parent's onComplete() is called
this.emit('complete', {
id: this.props.task.id,
text: this.props.task.text
})
},
remove() {
// Emit 'remove' → parent's onRemove() is called
this.emit('remove', { id: this.props.task.id })
}
})
// Parent component handles events from children
wildflower.component('task-list', {
state: {
tasks: [
{ id: 1, text: 'Write documentation' },
{ id: 2, text: 'Fix bug #42' },
{ id: 3, text: 'Review PR' }
],
lastAction: 'none'
},
// Handler for child's emit('complete', detail)
onComplete(detail) {
this.lastAction = `Completed: ${detail.text}`
this.tasks = this.tasks.filter(
t => t.id !== detail.id
)
},
// Handler for child's emit('remove', detail)
onRemove(detail) {
this.lastAction = `Removed task #${detail.id}`
this.tasks = this.tasks.filter(
t => t.id !== detail.id
)
}
})
this.emit('eventName', detail)walks up the component hierarchy looking for anonEventName(detail)handler- Event names are auto-capitalized:
emit('select')→onSelect(),emit('itemAdded')→onItemAdded() - The
detailargument (any value or object) is passed directly to the parent handler - Events propagate through the component hierarchy: if the immediate parent doesn't handle it, ancestors are checked
- Returns
trueif the event was dispatched,falseif the component wasn't ready
Watching State Changes
The watch block lets a component react to specific state changes with callback handlers. Watchers receive the new value, old value, and the path that changed:
wildflower.component('settings-panel', {
state: {
theme: 'light',
fontSize: 14,
user: { name: 'Alice', role: 'admin' }
},
watch: {
// Watch a single property
theme(newValue, oldValue) {
document.body.className = `theme-${newValue}`
},
// Watch a nested property
'user.role'(newRole, oldRole) {
console.log(`Role changed: ${oldRole} → ${newRole}`)
},
// Watch any change on an object (fires when user or its children change)
user(newUser, oldUser, path) {
console.log(`User changed at ${path}`)
},
// Watch a store path
'store:theme-preferences.darkMode': function(isDark) {
this.theme = isDark ? 'dark' : 'light'
}
}
})
- Exact path:
'theme'fires whenthemechanges - Parent path:
'user'fires whenuser.namechanges too - Wildcard:
'*'fires on any state change - Store paths:
'store:storeName.path'watches store state (automatically cleaned up)
Immediate Watchers
Append :immediate to any watch key to run the handler immediately during initialization, in addition to on subsequent changes. The handler receives the current value as newValue and undefined as oldValue:
wildflower.component('theme-applier', {
state: {
theme: 'dark',
locale: 'en'
},
watch: {
// Runs immediately with current value, then on every change
'theme:immediate'(newValue, oldValue) {
document.body.className = `theme-${newValue}`
// On init: newValue='dark', oldValue=undefined
// On change: newValue='light', oldValue='dark'
},
// Works with store paths too
'store:user-prefs.language:immediate'(lang) {
this.locale = lang || 'en'
}
}
})
Without :immediate, watchers only fire on subsequent changes. Use immediate watchers when you need to apply side effects based on the initial state (like setting a CSS class or syncing with an external system) without duplicating logic in init().
Store Subscriptions
The subscribe block declares which stores a component depends on. Subscribed stores are injected as this.stores.storeName and the onStoreUpdate hook fires when watched paths change:
wildflower.component('user-dashboard', {
// Declare store dependencies with paths to watch
subscribe: {
user: ['profile', 'preferences'],
cart: ['items']
},
init() {
// Stores are auto-injected after subscription
console.log(this.stores.user.profile)
console.log(this.stores.cart.items)
},
// Called when any subscribed path changes
onStoreUpdate(storeName, path, newValue, oldValue) {
if (storeName === 'user' && path === 'preferences') {
this.applyPreferences(newValue)
}
},
applyPreferences(prefs) {
// React to store changes
}
})
You can also subscribe to stores without watching specific paths, just to ensure the store is available in this.stores:
wildflower.component('nav-bar', {
// Array syntax: wait for stores, no change notifications
subscribe: ['auth', 'navigation'],
init() {
// Both stores available
if (this.stores.auth.isLoggedIn) {
this.stores.navigation.showUserMenu()
}
}
})
Programmatic Subscriptions
For dynamic or conditional subscriptions, use the this.subscribe() method. It returns an unsubscribe function for cleanup:
wildflower.component('live-feed', {
state: { messages: [] },
init() {
// Subscribe to own state changes
this._unsub = this.subscribe('messages', (newMessages) => {
this.scrollToBottom()
})
},
destroy() {
// Clean up programmatic subscriptions
if (this._unsub) this._unsub()
}
})
// Works on stores too
const store = wildflower.getStore('notifications')
const unsub = store.subscribe('unreadCount', (count) => {
document.title = count > 0 ? `(${count}) My App` : 'My App'
})
Choosing the Right Pattern
| Pattern | Use When | Cleanup |
|---|---|---|
$entity.path |
Reading entity state directly in HTML templates (preferred) | Automatic |
getComponent() |
Calling methods on or reading state from another component | N/A |
watch |
Reacting to specific local state or store path changes | Automatic |
subscribe block |
Declaring store dependencies with auto-injection and change notifications | Automatic |
subscribe() method |
Dynamic or conditional observation of any state path | Manual (returns unsubscribe fn) |
emit() |
Child-to-parent communication (events bubble up hierarchy) | N/A |
props |
Parent-to-child data flow declared in HTML | Automatic |