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:
window.parent.postMessage({
type: 'component-event',
event: string, // Event name (e.g., 'action:click')
data: object // Event payload (optional)
}, '*');Example:
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:
window.addEventListener('message', (event) => {
if (event.data.type === 'board-event') {
const { event: eventName, data } = event.data;
// Handle event
}
});Example:
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 Type | Direction | Description |
|---|---|---|
props-update | Board → Component | Props have changed |
board-ready | Board → Component | Board finished loading |
component-mounted | Board → Component | Component iframe loaded |
component-resize | Board → Component | Component was resized |
Event Naming Conventions
Standard Format
Use colon notation for event names:
namespace:actionExamples:
action:click- User action eventsdata:update- Data change eventsstate:change- State transition eventsuser:select- User interaction events
Recommended Namespaces
| Namespace | Usage | Examples |
|---|---|---|
action:* | User interactions | action:click, action:submit, action:cancel |
data:* | Data operations | data:update, data:delete, data:fetch |
state:* | State changes | state:change, state:progress, state:complete |
user:* | User-specific | user:select, user:login, user:logout |
component:* | Component-specific | balloon:pop, timer:expire, quiz:answer |
Event Data Structure
Minimal Event
{
type: 'component-event',
event: 'action:click'
}Full Event with Metadata
{
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:
document.getElementById('button').addEventListener('click', () => {
window.parent.postMessage({
type: 'component-event',
event: 'action:click',
data: {
clickCount: ++clicks,
timestamp: Date.now()
}
}, '*');
});Listener:
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:
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:
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:
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:
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:
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:
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:
{
type: 'props-update',
props: {
title: 'New Title',
color: 'blue'
}
}Handling:
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:
{
type: 'board-ready',
boardId: 'uuid',
timestamp: 1700000000
}Handling:
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:
{
type: 'component-mounted',
componentId: 'clicker-btn-1',
timestamp: 1700000000
}component-resize
Sent when the component container is resized.
Direction: Board → Component
Format:
{
type: 'component-resize',
width: 300,
height: 200
}Handling:
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
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
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:
window.addEventListener('message', (event) => {
const { event: eventName } = event.data; // Unsafe!
handleEvent(eventName);
});Good:
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
window.parent.postMessage({
type: 'component-event',
event: 'action:click',
data: {
clickCount: 5,
timestamp: Date.now() // Always include!
}
}, '*');3. Validate Event Data
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
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
// 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:
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:
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
// 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:
<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
Flow:
- Button emits
action:click - Balloon listens, grows, emits
action:pop - Score listens to clicks, increments counter
Data Synchronization
Component A (Editor):
function saveText(text) {
window.parent.postMessage({
type: 'component-event',
event: 'data:update',
data: { entity: 'document', text }
}, '*');
}Component B (Preview):
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);
}
});Related Documentation
- Component Structure - Communication
- Balloon Game Tutorial
- WebSocket API - Real-time board events
Limitations
Current Limitations (v1.0)
- No Request-Response Pattern: Events are fire-and-forget
- No Event History: Past events are not stored
- No Event Filtering: All components receive all events
- 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: