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 - no events, no callbacks, just reactive state.
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="tooltip-demo">
<!-- 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('tooltip-demo', {
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;
}
init()creates the tooltip DOM element and attaches event listenersupdate()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
usesto make component requirements clear