Skip to content

Event System API

Complete reference for BoardAPI's component event system.

Overview

The Event System enables component-to-component communication via browser's postMessage API. Components emit and listen to events through BoardAPI's central event bus.

Key Concepts

  • Event Bus: BoardAPI acts as central message broker
  • Sandboxing: Components run in isolated iframes
  • Loose Coupling: No direct component-to-component references
  • Broadcast Model: Events are sent to all components on the board

Message Types

1. Component Events (Outgoing)

Events emitted from components to the board.

Format:

javascript
window.parent.postMessage({
  type: 'component-event',
  event: string,      // Event name (e.g., 'action:click')
  data: object        // Event payload (optional)
}, '*');

Example:

javascript
window.parent.postMessage({
  type: 'component-event',
  event: 'action:click',
  data: {
    clickCount: 5,
    timestamp: Date.now()
  }
}, '*');

2. Board Events (Incoming)

Events received by components from the board.

Format:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event') {
    const { event: eventName, data } = event.data;
    // Handle event
  }
});

Example:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'action:click') {
    console.log('Received click:', event.data.data);
  }
});

3. System Events

Control messages sent from BoardAPI to components.

Event TypeDirectionDescription
props-updateBoard → ComponentProps have changed
board-readyBoard → ComponentBoard finished loading
component-mountedBoard → ComponentComponent iframe loaded
component-resizeBoard → ComponentComponent was resized

Event Naming Conventions

Standard Format

Use colon notation for event names:

namespace:action

Examples:

  • action:click - User action events
  • data:update - Data change events
  • state:change - State transition events
  • user:select - User interaction events
NamespaceUsageExamples
action:*User interactionsaction:click, action:submit, action:cancel
data:*Data operationsdata:update, data:delete, data:fetch
state:*State changesstate:change, state:progress, state:complete
user:*User-specificuser:select, user:login, user:logout
component:*Component-specificballoon:pop, timer:expire, quiz:answer

Event Data Structure

Minimal Event

javascript
{
  type: 'component-event',
  event: 'action:click'
}

Full Event with Metadata

javascript
{
  type: 'component-event',
  event: 'action:click',
  data: {
    // Primary payload
    clickCount: 5,

    // Metadata (recommended)
    timestamp: 1700000000,
    componentId: 'clicker-btn-1',
    userId: 'user-uuid',

    // Additional context
    metadata: {
      browser: 'Chrome',
      device: 'desktop'
    }
  }
}

Common Event Patterns

1. Click Events

Emitter:

javascript
document.getElementById('button').addEventListener('click', () => {
  window.parent.postMessage({
    type: 'component-event',
    event: 'action:click',
    data: {
      clickCount: ++clicks,
      timestamp: Date.now()
    }
  }, '*');
});

Listener:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'action:click') {
    handleClick(event.data.data.clickCount);
  }
});

2. State Change Events

Emitter:

javascript
function setState(newState) {
  const oldState = currentState;
  currentState = newState;

  window.parent.postMessage({
    type: 'component-event',
    event: 'state:change',
    data: {
      from: oldState,
      to: newState,
      timestamp: Date.now()
    }
  }, '*');
}

Listener:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'state:change') {
    const { from, to } = event.data.data;
    console.log(`State changed: ${from} → ${to}`);
  }
});

3. Data Update Events

Emitter:

javascript
async function updateData(newData) {
  await saveData(newData);

  window.parent.postMessage({
    type: 'component-event',
    event: 'data:update',
    data: {
      entity: 'user',
      id: '123',
      fields: newData,
      timestamp: Date.now()
    }
  }, '*');
}

Listener:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'data:update') {
    const { entity, id, fields } = event.data.data;
    refreshDisplay(entity, id, fields);
  }
});

4. Progress Events

Emitter:

javascript
function updateProgress(current, total) {
  const percent = Math.round((current / total) * 100);

  window.parent.postMessage({
    type: 'component-event',
    event: 'state:progress',
    data: {
      current,
      total,
      percent,
      completed: current >= total
    }
  }, '*');
}

Listener:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'state:progress') {
    const { percent, completed } = event.data.data;
    updateProgressBar(percent);

    if (completed) {
      showCompletionAnimation();
    }
  }
});

System Events Reference

props-update

Sent when component props are updated by the board.

Direction: Board → Component

Format:

javascript
{
  type: 'props-update',
  props: {
    title: 'New Title',
    color: 'blue'
  }
}

Handling:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'props-update') {
    const newProps = event.data.props;
    updateComponent(newProps);
  }
});

board-ready

Sent when the board has finished initializing.

Direction: Board → Component

Format:

javascript
{
  type: 'board-ready',
  boardId: 'uuid',
  timestamp: 1700000000
}

Handling:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-ready') {
    console.log('Board ready:', event.data.boardId);
    initializeComponent();
  }
});

component-mounted

Sent when the component iframe has mounted.

Direction: Board → Component

Format:

javascript
{
  type: 'component-mounted',
  componentId: 'clicker-btn-1',
  timestamp: 1700000000
}

component-resize

Sent when the component container is resized.

Direction: Board → Component

Format:

javascript
{
  type: 'component-resize',
  width: 300,
  height: 200
}

Handling:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'component-resize') {
    const { width, height } = event.data;
    handleResize(width, height);
  }
});

Event Filtering

Listen to Specific Events Only

javascript
const EVENT_HANDLERS = {
  'action:click': handleClick,
  'data:update': handleDataUpdate,
  'state:change': handleStateChange
};

window.addEventListener('message', (event) => {
  if (event.data.type !== 'board-event') return;

  const handler = EVENT_HANDLERS[event.data.event];
  if (handler) {
    handler(event.data.data);
  }
});

Namespace Filtering

javascript
window.addEventListener('message', (event) => {
  if (event.data.type !== 'board-event') return;

  const eventName = event.data.event;

  // Handle all action:* events
  if (eventName.startsWith('action:')) {
    handleActionEvent(eventName, event.data.data);
  }

  // Handle all data:* events
  if (eventName.startsWith('data:')) {
    handleDataEvent(eventName, event.data.data);
  }
});

Best Practices

1. Always Validate Event Type

Bad:

javascript
window.addEventListener('message', (event) => {
  const { event: eventName } = event.data; // Unsafe!
  handleEvent(eventName);
});

Good:

javascript
window.addEventListener('message', (event) => {
  // Validate message type
  if (event.data.type !== 'board-event') return;

  const { event: eventName, data } = event.data;
  handleEvent(eventName, data);
});

2. Include Timestamps

javascript
window.parent.postMessage({
  type: 'component-event',
  event: 'action:click',
  data: {
    clickCount: 5,
    timestamp: Date.now() // Always include!
  }
}, '*');

3. Validate Event Data

javascript
window.addEventListener('message', (event) => {
  if (event.data.type !== 'board-event') return;

  if (event.data.event === 'action:click') {
    const data = event.data.data;

    // Validate data structure
    if (!data || typeof data.clickCount !== 'number') {
      console.warn('Invalid click event data');
      return;
    }

    handleClick(data.clickCount);
  }
});

4. Use Debouncing for High-Frequency Events

javascript
let debounceTimer;

function emitMouseMove(x, y) {
  clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => {
    window.parent.postMessage({
      type: 'component-event',
      event: 'cursor:move',
      data: { x, y, timestamp: Date.now() }
    }, '*');
  }, 100); // Throttle to max 10 events/sec
}

5. Clean Up Event Listeners

javascript
// Store handler reference
const messageHandler = (event) => {
  if (event.data.type === 'board-event') {
    handleBoardEvent(event.data);
  }
};

// Add listener
window.addEventListener('message', messageHandler);

// Clean up when component unmounts
window.addEventListener('beforeunload', () => {
  window.removeEventListener('message', messageHandler);
});

Security Considerations

Origin Validation (Advanced)

For production components, validate message origin:

javascript
const ALLOWED_ORIGINS = [
  'https://app.boardapi.io',
  'http://localhost:4173'
];

window.addEventListener('message', (event) => {
  // Validate origin
  if (!ALLOWED_ORIGINS.includes(event.origin)) {
    console.warn('Message from untrusted origin:', event.origin);
    return;
  }

  // Process event
  if (event.data.type === 'board-event') {
    handleBoardEvent(event.data);
  }
});

Data Sanitization

Never trust incoming data:

javascript
window.addEventListener('message', (event) => {
  if (event.data.type !== 'board-event') return;

  if (event.data.event === 'data:update') {
    const { text } = event.data.data;

    // Sanitize HTML before inserting
    const sanitized = text.replace(/<script.*?>.*?<\/script>/gi, '');
    document.getElementById('display').textContent = sanitized;
  }
});

Debugging Events

Console Logging

javascript
// Log all incoming messages
window.addEventListener('message', (event) => {
  console.log('[RECEIVED]', event.data);
});

// Log all outgoing events
const originalPostMessage = window.parent.postMessage;
window.parent.postMessage = function(message, targetOrigin) {
  console.log('[SENT]', message);
  return originalPostMessage.call(window.parent, message, targetOrigin);
};

Event Inspector

Create a debug panel to monitor events:

html
<div id="event-log" style="
  position: fixed;
  bottom: 0;
  right: 0;
  width: 300px;
  height: 200px;
  background: rgba(0,0,0,0.9);
  color: #0f0;
  font-family: monospace;
  font-size: 12px;
  overflow-y: auto;
  padding: 10px;
  display: none;
"></div>

<script>
// Toggle debug panel with Ctrl+Shift+D
document.addEventListener('keydown', (e) => {
  if (e.ctrlKey && e.shiftKey && e.key === 'D') {
    const log = document.getElementById('event-log');
    log.style.display = log.style.display === 'none' ? 'block' : 'none';
  }
});

// Log events
window.addEventListener('message', (event) => {
  const log = document.getElementById('event-log');
  const entry = document.createElement('div');
  entry.textContent = JSON.stringify(event.data, null, 2);
  log.prepend(entry);

  // Keep only last 50 events
  while (log.children.length > 50) {
    log.removeChild(log.lastChild);
  }
});
</script>

Examples

Multi-Component Game

See: Balloon Game Tutorial

Flow:

  1. Button emits action:click
  2. Balloon listens, grows, emits action:pop
  3. Score listens to clicks, increments counter

Data Synchronization

Component A (Editor):

javascript
function saveText(text) {
  window.parent.postMessage({
    type: 'component-event',
    event: 'data:update',
    data: { entity: 'document', text }
  }, '*');
}

Component B (Preview):

javascript
window.addEventListener('message', (event) => {
  if (event.data.type === 'board-event' &&
      event.data.event === 'data:update' &&
      event.data.data.entity === 'document') {
    updatePreview(event.data.data.text);
  }
});

Limitations

Current Limitations (v1.0)

  1. No Request-Response Pattern: Events are fire-and-forget
  2. No Event History: Past events are not stored
  3. No Event Filtering: All components receive all events
  4. No Priority: Events are processed in order received

Future Enhancements (Roadmap)

  • Event acknowledgment system
  • Event replay for late-joining components
  • Server-side event filtering
  • Event priority queues
  • Event persistence and logging

Support

For questions and issues:


See Also: