Two Mechanisms
| Events | Hooks | |
|---|---|---|
| Purpose | Notify | Extend |
| Return values | No | Yes |
| Use case | ”Something happened" | "Add your thing to my thing” |
Events (Pub/Sub)
Emitting
api.bus.emit('todo:added', { text: 'Buy milk', count: 3 });
Listening
api.bus.on('todo:added', (data) => {
console.log(`Total todos: ${data.count}`);
});
One-Shot Listening
// Fires once, then auto-removes
api.bus.once('board:ready', (data) => {
console.log('Board ready, only fires once');
});
Unsubscribing
function handler(data) { /* ... */ }
api.bus.on('my-event', handler);
// Later:
api.bus.off('my-event', handler); // Must be same reference
Full Example
// === Plugin A: todo-list ===
export function setup(api) {
const todos = [];
function add(text) {
todos.push({ text, done: false });
api.bus.emit('todo:added', { text, count: todos.length });
}
function toggle(index) {
todos[index].done = !todos[index].done;
api.bus.emit('todo:toggled', { index, done: todos[index].done });
}
}
// === Plugin B: stats ===
export function setup(api) {
let total = 0;
let completed = 0;
api.bus.on('todo:added', (data) => {
total = data.count;
render();
});
api.bus.on('todo:toggled', (data) => {
completed += data.done ? 1 : -1;
render();
});
}
Hooks (Extension Points)
Registering
// Toolbar plugin exposes an extension point
api.registerHook('sidebar:add-section', (config) => {
const section = document.createElement('div');
section.innerHTML = `<h4>${config.title}</h4>`;
sidebar.appendChild(section);
return section;
});
Using
// Another plugin adds a section to the sidebar
const sections = api.useHook('sidebar:add-section', {
title: 'My Section',
content: 'Hello from my plugin!'
});
// sections → [sectionElement, ...]
Removing
function myHandler(config) { /* ... */ }
api.registerHook('my-hook', myHandler);
// Later (e.g., in teardown):
api.removeHook('my-hook', myHandler);
Full Example
// === Plugin A: toolbar ===
export function setup(api) {
const toolbar = document.createElement('div');
toolbar.style.cssText = 'display:flex; gap:8px; padding:12px;';
api.registerHook('toolbar:add-button', (config) => {
const btn = document.createElement('button');
btn.textContent = config.label;
btn.onclick = config.action;
toolbar.appendChild(btn);
return btn;
});
// Invoke hooks after all plugins are loaded
api.bus.once('board:allPluginsLoaded', () => {
api.useHook('toolbar:add-button', {
label: '🏠 Home',
action: () => goHome()
});
});
api.boardEl.appendChild(toolbar);
}
// === Plugin B: my-tools ===
export function setup(api) {
// Wait for toolbar to register its hook
api.bus.once('board:allPluginsLoaded', () => {
api.useHook('toolbar:add-button', {
label: '📊 Stats',
action: () => showStats()
});
api.useHook('toolbar:add-button', {
label: '⚙️ Settings',
action: () => openSettings()
});
});
}
Reacting to Lifecycle Events
After All Plugins Load
Useboard:allPluginsLoaded when your plugin depends on other plugins being ready:
export function setup(api) {
// Register my hooks immediately
api.registerHook('my-plugin:add-item', (config) => { /* ... */ });
// Use other plugins' hooks after they're all loaded
api.bus.once('board:allPluginsLoaded', () => {
api.useHook('toolbar:add-button', {
label: 'My Action',
action: () => doSomething()
});
});
}
Listening to Other Plugins’ Events
// React when a sticky note is created
api.bus.on('sticky-note:created', ({ id }) => {
api.notify(`New note created: ${id}`, 'info');
});
// React when a clock ticks
api.bus.on('clock:tick', ({ hours, minutes }) => {
if (hours === 0 && minutes === 0) {
api.notify('Midnight!', 'warning');
}
});
Best Practices
Don't import other plugins
Don't import other plugins
Always use events or hooks — never
import('./other-plugin.js').Use namespaced event names
Use namespaced event names
my-plugin:action not action.Clean up listeners
Clean up listeners
Remove event listeners and hooks in your
teardown() function:function handler(data) { /* ... */ }
export function setup(api) {
api.bus.on('my-event', handler);
api.registerHook('my-hook', myHandler);
}
export function teardown() {
api.bus.off('my-event', handler);
api.removeHook('my-hook', myHandler);
}
Use `once` for one-time setup
Use `once` for one-time setup
api.bus.once('board:ready', () => {
// Initialization that should only happen once
});