Advanced Plugin Patterns CORE+

Advanced plugin techniques including service providers, reactive state binding, watchers, subscriptions, and complete real-world examples.

Watching State Changes

Use the watch object to react to state changes declaratively:

wildflower.plugin({
    name: 'cart',
    state: {
        items: [],
        total: 0
    },

    watch: {
        // Watch a specific property
        total(newValue, oldValue) {
            console.log(`Total changed: ${oldValue} -> ${newValue}`)
        },

        // Watch array changes
        items(newItems, oldItems) {
            console.log(`Cart now has ${newItems.length} items`)
        }
    },

    addItem(item) {
        this.items = [...this.items, item]
        this.total += item.price
    },

    install(wf) {}
})

Programmatic Subscriptions

Use subscribe() for dynamic state observation with automatic cleanup:

// Subscribe to a specific path
const unsubscribe = wildflower.$cart.subscribe('total', (newValue, oldValue) => {
    updateCheckoutButton(newValue)
})

// Later, clean up the subscription
unsubscribe()

Binding Plugin State to the DOM

Use $entity.path in data-bind to automatically update the UI when plugin state changes:

<!-- Display plugin state -->
<span data-bind="$cart.total"></span>

<!-- Display computed property -->
<span data-bind="$cart.itemCount"></span>

<!-- Nested properties work too -->
<span data-bind="$user.profile.name"></span>

When plugin state changes, bound elements update automatically - no manual refresh needed.

Service Providers

Providing Services

Register services that components can use:

// Provide an HTTP service
wildflower.provide('http', {
    async get(url) {
        const response = await fetch(url)
        return response.json()
    },
    async post(url, data) {
        const response = await fetch(url, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(data)
        })
        return response.json()
    }
})

// Provide configuration
wildflower.provide('config', {
    apiUrl: 'https://api.example.com',
    debug: true
})

// Provide class instances
class Logger {
    logs = []
    log(msg) { this.logs.push(msg) }
}
wildflower.provide('logger', new Logger())

// Provide utility functions
wildflower.provide('formatDate', (date) => {
    return new Date(date).toLocaleDateString()
})

Using Services in Components

Declare services with the uses property:

wildflower.component('user-list', {
    uses: ['http', 'config'],  // Services this component uses

    state: { users: [] },

    async init() {
        // Services available as $serviceName
        const url = this.$config.apiUrl + '/users'
        this.users = await this.$http.get(url)
    }
})

Using Services in Plugins

Plugins can also declare services they use:

// First, provide services
wildflower.provide('http', httpService)
wildflower.provide('auth', authService)

// Then create a plugin that uses them
wildflower.plugin({
    name: 'dataSync',
    version: '1.0.0',
    uses: ['http', 'auth'],  // Services this plugin uses

    install(wf) {
        // Services available as this.$http, this.$auth
        if (this.$auth.isLoggedIn()) {
            this.$http.get('/sync')
        }
    }
})

Shared Services

The same service instance is shared across all components:

// The same logger instance is shared everywhere
wildflower.provide('logger', new Logger())

wildflower.component('comp-a', {
    uses: ['logger'],
    init() { this.$logger.log('Component A') }
})

wildflower.component('comp-b', {
    uses: ['logger'],
    init() { this.$logger.log('Component B') }
})

// Both components share the same Logger instance

Complete Plugin Example: Analytics

A complete analytics plugin demonstrating multiple features:

wildflower.plugin({
    name: 'analytics',
    version: '1.0.0',

    state: {
        events: [],
        sessionStart: Date.now()
    },

    track(event, data = {}) {
        this.events.push({
            event,
            data,
            timestamp: Date.now()
        })
    },

    getSessionDuration() {
        return Date.now() - this.sessionStart
    },

    computed: {
        eventCount() {
            return this.events.length
        }
    },

    install(wf) {
        // Add tracking directive
        wf.directive('track-click', {
            init(element, value, context) {
                element.addEventListener('click', () => {
                    wildflower.$analytics.track('click', {
                        element: value,
                        component: context.component?.name
                    })
                })
            }
        })

        // Provide a track service for components
        wf.provide('track', (event, data) => {
            wildflower.$analytics.track(event, data)
        })

        // Hook into component lifecycle
        wf.hook('component:afterInit', (instance) => {
            wildflower.$analytics.track('component:init', {
                name: instance.name
            })
        })
    }
})

Using the Analytics Plugin

<!-- Use the tracking directive -->
<button data-track-click="purchase-button">Buy Now</button>

<!-- Use in component -->
<div data-component="checkout">
    <button data-action="complete">Complete Order</button>
</div>
wildflower.component('checkout', {
    uses: ['track'],  // Declare the service
    state: { total: 0 },

    complete() {
        // Use the track service
        this.$track('checkout:complete', { total: this.total })

        // Or access plugin directly
        console.log('Events tracked:', wildflower.$analytics.eventCount)
    }
})

Complete Plugin Example: Shopping Cart

This example demonstrates all reactive plugin features working together:

wildflower.plugin({
    name: 'cart',
    version: '1.0.0',

    state: {
        items: [],
        discount: 0
    },

    computed: {
        itemCount() {
            return this.items.length
        },
        subtotal() {
            return this.items.reduce((sum, item) => sum + item.price, 0)
        },
        total() {
            return this.subtotal - this.discount
        }
    },

    watch: {
        items(newItems) {
            // Persist to localStorage whenever items change
            localStorage.setItem('cart', JSON.stringify(newItems))
        }
    },

    add(product) {
        this.items = [...this.items, product]
    },

    remove(index) {
        this.items = this.items.filter((_, i) => i !== index)
    },

    applyDiscount(amount) {
        this.discount = amount
    },

    clear() {
        this.reset()  // Reset to initial state
    },

    install(wf) {
        // Load saved cart on startup
        const saved = localStorage.getItem('cart')
        if (saved) {
            this.items = JSON.parse(saved)
        }
    }
})

HTML with Automatic Updates

<div data-component="cart-widget">
    <!-- These update automatically when cart changes -->
    <span data-bind="$cart.itemCount"></span> items
    <strong>$<span data-bind="$cart.total"></span></strong>
    <button data-action="clearCart">Clear</button>
</div>

<div data-component="product-card">
    <h3 data-bind="name"></h3>
    <p>$<span data-bind="price"></span></p>
    <button data-action="addToCart">Add to Cart</button>
</div>
wildflower.component('cart-widget', {
    state: {},
    clearCart() {
        wildflower.$cart.clear()
    }
})

wildflower.component('product-card', {
    state: { name: 'Widget', price: 29.99 },
    addToCart() {
        wildflower.$cart.add({
            name: this.name,
            price: this.price
        })
    }
})

Click "Add to Cart" and the cart widget updates instantly from reactive state alone, with no events and no callbacks in between.

Complete Directive Example: Tooltips Plugin

This interactive example demonstrates a production-ready tooltip plugin using all three directive lifecycle hooks. Hover over buttons and list items to see tooltips in action.

<div data-component="plugin-tooltip">
    <!-- Static tooltip -->
    <div class="mb-3">
        <h6>Static Tooltip</h6>
        <button class="btn btn-primary" data-tooltip="This is a static tooltip message">
            Hover Me (Static)
        </button>
    </div>

    <!-- Dynamic tooltip bound to state -->
    <div class="mb-3">
        <h6>Dynamic Tooltip (Bound to State)</h6>
        <button class="btn btn-success" data-tooltip="dynamicMessage">
            Hover Me (Dynamic)
        </button>
        <div class="mt-2">
            <input type="text" class="form-control" data-model="dynamicMessage"
                   placeholder="Type to change tooltip...">
        </div>
    </div>

    <!-- Tooltips in list items -->
    <div class="mb-3">
        <h6>Tooltips in List Items</h6>
        <ul class="list-group" data-list="items">
            <template>
                <li class="list-group-item" data-tooltip="description">
                    <strong data-bind="name"></strong>
                </li>
            </template>
        </ul>
        <button class="btn btn-outline-primary btn-sm mt-2" data-action="addItem">
            Add Item
        </button>
    </div>

    <!-- Conditional tooltip (data-show) -->
    <div class="mb-3">
        <h6>Conditional Visibility</h6>
        <button class="btn btn-outline-secondary btn-sm" data-action="toggleSection">
            Toggle Section
        </button>
        <span class="ms-2 text-muted" data-bind="showSection ? 'Visible' : 'Hidden'"></span>
        <div data-show="showSection" class="mt-2 p-2 bg-light rounded">
            <button class="btn btn-warning" data-tooltip="This tooltip hides when section hides">
                Conditional Tooltip
            </button>
        </div>
    </div>
</div>
// Register the tooltip plugin with directive
wildflower.plugin({
    name: 'tooltips',
    install(wf) {
        wf.directive('tooltip', {
            // Called when element with data-tooltip is discovered
            init(element, value, context) {
                const text = context.resolvedValue || value;

                // Create tooltip element
                const tooltip = document.createElement('div');
                tooltip.className = 'wf-tooltip';
                tooltip.textContent = text;
                document.body.appendChild(tooltip);

                // Store references for cleanup
                element._wfTooltip = tooltip;
                element._wfTooltipText = text;

                element._wfTooltipShow = () => {
                    tooltip.textContent = element._wfTooltipText;
                    const rect = element.getBoundingClientRect();
                    tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
                    tooltip.style.top = rect.top - tooltip.offsetHeight - 10 + 'px';
                    tooltip.classList.add('visible');
                };

                element._wfTooltipHide = () => {
                    tooltip.classList.remove('visible');
                };

                element.addEventListener('mouseenter', element._wfTooltipShow);
                element.addEventListener('mouseleave', element._wfTooltipHide);
            },

            // Called when bound value changes
            update(element, newValue, oldValue, context) {
                if (element._wfTooltip) {
                    element._wfTooltipText = newValue;
                    if (element._wfTooltip.classList.contains('visible')) {
                        element._wfTooltip.textContent = newValue;
                    }
                }
            },

            // Called when element is removed from DOM
            destroy(element, context) {
                if (element._wfTooltip) element._wfTooltip.remove();
                if (element._wfTooltipShow) {
                    element.removeEventListener('mouseenter', element._wfTooltipShow);
                }
                if (element._wfTooltipHide) {
                    element.removeEventListener('mouseleave', element._wfTooltipHide);
                }
                delete element._wfTooltip;
                delete element._wfTooltipShow;
                delete element._wfTooltipHide;
                delete element._wfTooltipText;
            }
        });
    }
});

// Demo component using the tooltip directive
wildflower.component('plugin-tooltip', {
    state: {
        dynamicMessage: 'This tooltip updates as you type!',
        showSection: true,
        items: [
            { name: 'Apple', description: 'A crisp, sweet fruit' },
            { name: 'Banana', description: 'A yellow tropical fruit' },
            { name: 'Cherry', description: 'A small red stone fruit' }
        ],
        itemCounter: 3
    },

    toggleSection() {
        this.showSection = !this.showSection;
    },

    addItem() {
        this.itemCounter++;
        this.items.push({
            name: 'Item ' + this.itemCounter,
            description: 'Description for item ' + this.itemCounter
        });
    }
});
.wf-tooltip {
    position: fixed;
    background: #333;
    color: white;
    padding: 8px 12px;
    border-radius: 4px;
    font-size: 13px;
    z-index: 10000;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.15s;
    max-width: 250px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}

.wf-tooltip.visible {
    opacity: 1;
}

/* Arrow pointing down */
.wf-tooltip::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-top-color: #333;
}
Live Preview
Key Points:
  • init() creates the tooltip DOM element and attaches event listeners
  • update() handles reactive state changes (typing in input updates tooltip)
  • destroy() cleans up DOM elements and event listeners when element is removed
  • The directive works with static values, state bindings, list items, and conditional rendering
  • Always store references on the element for proper cleanup in destroy()

Plugin API Reference

Method Description
wildflower.plugin(plugin, options) Register a plugin (function or object with install method)
wildflower.directive(name, handlers) Register a custom directive
wildflower.hook(event, callback) Register a lifecycle hook callback
wildflower.provide(key, value) Register a service that components can use
wildflower.getService(key) Retrieve a provided service
wildflower.hasProvider(key) Check if a service is registered
wildflower.hasPlugin(name) Check if a plugin is registered
wildflower.getPlugin(name) Get plugin info by name
wildflower.listPlugins() List all registered plugins
wildflower.$pluginName Access plugin state, methods, and computed properties
wildflower.$pluginName.state The plugin's reactive state object
wildflower.$pluginName.reset() Reset plugin state to initial values
wildflower.$pluginName.subscribe(path, callback) Subscribe to state changes, returns unsubscribe function

Best Practices

  • Name your plugins: Always provide a name and version for easier debugging
  • Clean up: Implement destroy handlers for directives to prevent memory leaks
  • Keep plugins focused: Each plugin should do one thing well
  • Document services: Clearly state which services your plugin provides or requires
  • Handle missing providers: Check if services exist before using
  • Explicit is better: Declare services in uses to make component requirements clear