Overview
Hooks allow plugins to expose extension points that other plugins can register handlers for. Unlike events (fire-and-forget), hooks collect return values.
// Toolbar plugin registers a hook
api.registerHook('toolbar:add-button', (config) => {
const btn = document.createElement('button');
btn.textContent = config.label;
toolbar.appendChild(btn);
return btn;
});
// Another plugin uses the hook
const buttons = api.useHook('toolbar:add-button', {
label: 'My Action',
action: () => alert('clicked!')
});
// buttons → [buttonElement, ...]
API
api.registerHook(name, handler)
Register a function that responds to a named hook.
| Parameter | Type | Description |
|---|
name | string | Hook name (use namespace:action format) |
handler | function | Callback receiving payload, can return a value |
api.registerHook('sidebar:add-item', (config) => {
const item = document.createElement('div');
item.textContent = config.label;
sidebarEl.appendChild(item);
return item;
});
api.useHook(name, payload)
Call all registered handlers for a hook. Returns an array of their return values.
| Parameter | Type | Description |
|---|
name | string | Hook name to invoke |
payload | any | Data passed to each handler |
Returns: Array<any> — return values from each handler (empty array if none registered).
const items = api.useHook('sidebar:add-item', {
label: 'Settings',
icon: '⚙️',
onClick: () => openSettings()
});
// items → [divElement, divElement, ...]
api.removeHook(name, handler)
Remove a previously registered handler. Must pass the same function reference.
function myHandler(config) {
// ...
return result;
}
api.registerHook('my-hook', myHandler);
// Later:
api.removeHook('my-hook', myHandler);
Call removeHook in your teardown() function to clean up extension points when your plugin is disabled.
Hooks vs Events
| Events | Hooks |
|---|
| Purpose | Notify | Extend |
| Return values | No | Yes |
| Direction | One-way broadcast | Request → responses |
| Use case | ”Something happened" | "Add your thing to my thing” |
When to Use Each
Use Events When:
- You’re broadcasting that something happened
- You don’t need a response
- Multiple plugins might be interested
- Example:
todo:added, theme:changed
Use Hooks When:
- You’re exposing an extension point
- You need return values from handlers
- You want plugins to contribute to your UI
- Example:
toolbar:add-button, sidebar:add-section
Timing
Hooks are typically registered during setup(), but they’re also commonly invoked after all plugins have loaded. Use the board:allPluginsLoaded event for this:
export function setup(api) {
const toolbar = document.createElement('div');
// Register my extension point
api.registerHook('toolbar:add-button', (config) => {
const btn = document.createElement('button');
btn.textContent = config.label;
btn.onclick = config.action;
toolbar.appendChild(btn);
return btn;
});
// After all plugins are loaded, let them add buttons
api.bus.once('board:allPluginsLoaded', () => {
api.useHook('toolbar:add-button', {
label: 'Default',
action: () => {}
});
});
api.boardEl.appendChild(toolbar);
}
// === sidebar/plugin.js ===
export function setup(api) {
const sidebar = document.createElement('div');
sidebar.style.cssText = `
position: fixed; top: 60px; right: 10px;
display: flex; flex-direction: column; gap: 8px;
`;
// Let plugins add sections
api.registerHook('sidebar:add-section', (config) => {
const section = document.createElement('div');
section.innerHTML = `<h4>${config.title}</h4>`;
sidebar.appendChild(section);
return section;
});
// Let plugins add items
api.registerHook('sidebar:add-item', (config) => {
const item = document.createElement('button');
item.textContent = `${config.icon} ${config.label}`;
item.onclick = config.onClick;
sidebar.appendChild(item);
return item;
});
api.boardEl.appendChild(sidebar);
}
// === my-plugin/plugin.js ===
export function setup(api) {
api.useHook('sidebar:add-section', { title: 'My Tools' });
api.useHook('sidebar:add-item', {
label: 'Settings',
icon: '⚙️',
onClick: () => openSettings()
});
}