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'
        }
    }
})
Live Preview

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()
        }
    }
})
Live Preview

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="team-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('team-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
        )
    }
})
Live Preview
How emit() works:
  • this.emit('eventName', detail) walks up the component hierarchy looking for an onEventName(detail) handler
  • Event names are auto-capitalized: emit('select')onSelect(), emit('itemAdded')onItemAdded()
  • The detail argument (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 true if the event was dispatched, false if 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'
        }
    }
})
Watcher features:
  • Exact path: 'theme' fires when theme changes
  • Parent path: 'user' fires when user.name changes 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