Skip to main content

Two Mechanisms

EventsHooks
PurposeNotifyExtend
Return valuesNoYes
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

Use board: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

Always use events or hooks — never import('./other-plugin.js').
my-plugin:action not action.
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);
}
api.bus.once('board:ready', () => {
  // Initialization that should only happen once
});