Component Structure
Learn how to structure your BoardAPI components for maximum compatibility and maintainability.
File Organization
Minimal Component
hello-card/
├── manifest.json # Required: Component metadata
└── index.html # Required: Entry pointRecommended Structure
vocabulary-card/
├── manifest.json # Component metadata
├── index.html # Entry point
├── styles.css # Separate stylesheet
├── script.js # Component logic
├── assets/ # Images, fonts, etc.
│ ├── icon.svg
│ └── bg.png
└── README.md # Documentation (optional)Complex Component (with build step)
advanced-chart/
├── manifest.json
├── src/
│ ├── index.js # Entry point (builds to dist/)
│ ├── components/
│ └── utils/
├── dist/
│ └── index.html # Built entry point (referenced in manifest)
├── package.json
└── README.mdmanifest.json Reference
The manifest file describes your component to BoardAPI.
Minimal Example
{
"name": "my-component",
"version": "1.0.0",
"entry": "index.html"
}Full Example
{
"name": "vocabulary-card",
"version": "2.1.0",
"description": "Interactive vocabulary flashcard with audio",
"author": "Your Name <you@example.com>",
"license": "MIT",
"homepage": "https://github.com/yourname/vocab-card",
"repository": {
"type": "git",
"url": "https://github.com/yourname/vocab-card.git"
},
"keywords": ["education", "vocabulary", "flashcard"],
"entry": "dist/index.html",
"dependencies": {},
"component": {
"width": 300,
"height": 200,
"minWidth": 200,
"minHeight": 150,
"maxWidth": 600,
"maxHeight": 400,
"resizable": true,
"aspectRatio": "free",
"schema": {
"type": "object",
"properties": {
"word": {
"type": "string",
"description": "The vocabulary word",
"required": true
},
"translation": {
"type": "string",
"description": "Translation of the word"
},
"example": {
"type": "string",
"description": "Example sentence"
},
"audioUrl": {
"type": "string",
"format": "uri",
"description": "URL to pronunciation audio"
},
"difficulty": {
"type": "string",
"enum": ["easy", "medium", "hard"],
"default": "medium"
}
}
}
}
}Field Reference
Required Fields
| Field | Type | Description |
|---|---|---|
name | string | Component identifier (lowercase, hyphens only) |
version | string | Semantic version (e.g., "1.0.0") |
entry | string | Entry point file path (relative to ZIP root) |
Optional Metadata
| Field | Type | Description |
|---|---|---|
description | string | Short description of component |
author | string | Author name and email |
license | string | License identifier (e.g., "MIT", "Apache-2.0") |
homepage | string | Component website URL |
repository | object | Git repository info |
keywords | string[] | Search keywords |
Component Configuration
| Field | Type | Default | Description |
|---|---|---|---|
component.width | number | 200 | Default width in pixels |
component.height | number | 150 | Default height in pixels |
component.minWidth | number | 100 | Minimum width |
component.minHeight | number | 100 | Minimum height |
component.maxWidth | number | 1000 | Maximum width |
component.maxHeight | number | 1000 | Maximum height |
component.resizable | boolean | true | Can user resize? |
component.aspectRatio | string | "free" | "free", "fixed", or ratio like "16:9" |
Props Schema (component.schema)
Define the data your component accepts using JSON Schema.
{
"component": {
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Card title",
"default": "Untitled"
},
"color": {
"type": "string",
"enum": ["red", "blue", "green"],
"default": "blue"
},
"count": {
"type": "number",
"minimum": 0,
"maximum": 100
},
"enabled": {
"type": "boolean",
"default": true
}
},
"required": ["title"]
}
}
}Entry Point (index.html)
Your entry point file receives props from the board and renders the component.
Basic Template
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Component</title>
<style>
/* Scoped styles */
body { margin: 0; padding: 0; }
.component { padding: 20px; }
</style>
</head>
<body>
<div class="component">
<h2 id="title">Loading...</h2>
</div>
<script>
// Get props from parent
const props = window.componentProps || {};
// Initialize component
function init() {
document.getElementById('title').textContent = props.title || 'Default Title';
}
init();
</script>
</body>
</html>Accessing Props
Props are passed via window.componentProps:
// Get props
const props = window.componentProps || {};
// Access specific props
const title = props.title;
const color = props.color || 'blue';
const items = props.items || [];Receiving Prop Updates
Listen for prop changes:
window.addEventListener('message', (event) => {
if (event.data.type === 'props-update') {
const newProps = event.data.props;
updateComponent(newProps);
}
});Emitting Events
Send events to other components:
function emitEvent(eventName, data) {
window.parent.postMessage({
type: 'component-event',
event: eventName,
data: data
}, '*');
}
// Usage
emitEvent('item-clicked', { itemId: 123 });Listening to Board Events
window.addEventListener('message', (event) => {
if (event.data.type === 'board-event') {
const { event: eventName, data } = event.data;
if (eventName === 'data-updated') {
refreshData();
}
}
});Assets
Bundling Assets
Include images, fonts, and other assets in your ZIP:
my-component/
├── manifest.json
├── index.html
└── assets/
├── logo.png
├── icon.svg
└── font.woff2Referencing Assets
Use relative paths from your entry point:
<!-- In index.html -->
<img src="./assets/logo.png" alt="Logo">
<link rel="stylesheet" href="./assets/styles.css">
<script src="./assets/script.js"></script>External Assets
You can also reference external URLs:
<!-- CDN libraries -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<!-- Your hosted assets -->
<img src="https://mycdn.com/images/logo.png">WARNING
External assets require internet connection. Bundle critical assets in the ZIP for offline support.
Using Frameworks
Vanilla JavaScript (Recommended)
Smallest bundle size, fastest load time:
<script>
const app = {
data: window.componentProps || {},
render() {
document.getElementById('root').innerHTML = `
<h1>${this.data.title}</h1>
`;
}
};
app.render();
</script>Vue 3 (via CDN)
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<div id="app">
<h1>{{ title }}</h1>
</div>
<script>
const { createApp } = Vue;
createApp({
data() {
return window.componentProps || { title: 'Default' };
}
}).mount('#app');
</script>React (via CDN)
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
function App() {
const props = window.componentProps || {};
return <h1>{props.title || 'Default'}</h1>;
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>With Build Step (Advanced)
Use Vite, Webpack, or Rollup to bundle:
# package.json
{
"scripts": {
"build": "vite build"
}
}Point entry to built file:
{
"entry": "dist/index.html"
}Best Practices
1. Scope Your Styles
Prevent CSS conflicts:
/* Bad - global styles */
h1 { color: red; }
/* Good - scoped */
.my-component h1 { color: red; }2. Handle Missing Props
Always provide defaults:
const title = props.title || 'Untitled';
const items = props.items || [];3. Responsive Design
Support different sizes:
.component {
padding: 20px;
}
/* Adjust for small sizes */
@media (max-width: 250px) {
.component {
padding: 10px;
font-size: 12px;
}
}4. Loading States
Show loading while data fetches:
let loading = true;
async function loadData() {
loading = true;
render();
const data = await fetch('/api/data');
loading = false;
render();
}5. Error Handling
Gracefully handle errors:
try {
const data = await fetchData();
renderData(data);
} catch (error) {
renderError('Failed to load data');
}Component Communication
BoardAPI components communicate via events using the browser's postMessage API. This allows components to work together without direct coupling.
Event Flow Architecture
┌─────────────┐ postMessage ┌─────────────┐
│ Component A │ ─────────────────────────▶│ BoardAPI │
│ │ │ Event Bus │
└─────────────┘ └─────────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Component B │ │ Component C │ │ Component D │
└─────────────┘ └─────────────┘ └─────────────┘Key Principles:
- Components are sandboxed in iframes for security
- Communication is one-way via events (no direct calls)
- BoardAPI acts as central event bus
- Components are loosely coupled
Emitting Events
To send an event from your component to other components on the board:
// Emit event to board
window.parent.postMessage({
type: 'component-event',
event: 'action:click', // Event name
data: { // Event payload
clickCount: 5,
timestamp: Date.now()
}
}, '*');Event Structure:
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be 'component-event' |
event | string | Yes | Event name (use colon notation: namespace:action) |
data | object | No | Event payload (any JSON-serializable data) |
Event Naming Conventions:
- Use colon notation:
namespace:action - Examples:
action:click,data:update,state:change,user:select - Be specific:
balloon:popis better thanpop
Listening to Events
To receive events from other components:
window.addEventListener('message', (event) => {
// Ignore non-board events
if (event.data.type !== 'board-event') return;
const { event: eventName, data } = event.data;
// Handle specific events
switch (eventName) {
case 'action:click':
handleClick(data);
break;
case 'data:update':
handleDataUpdate(data);
break;
default:
console.log('Unknown event:', eventName);
}
});Security Note: Always validate event.data.type to ensure you're processing BoardAPI events and not other browser messages.
Event Types
1. Action Events
User interactions that trigger actions in other components.
// Button click
window.parent.postMessage({
type: 'component-event',
event: 'action:click',
data: { clickCount: 1 }
}, '*');
// Item selected
window.parent.postMessage({
type: 'component-event',
event: 'action:select',
data: { itemId: '123' }
}, '*');2. State Events
Notify other components about state changes.
// State changed
window.parent.postMessage({
type: 'component-event',
event: 'state:change',
data: { from: 'idle', to: 'active' }
}, '*');
// Progress updated
window.parent.postMessage({
type: 'component-event',
event: 'state:progress',
data: { percent: 75 }
}, '*');3. Data Events
Share data between components.
// Data updated
window.parent.postMessage({
type: 'component-event',
event: 'data:update',
data: { score: 100, level: 5 }
}, '*');Complete Example: Multi-Component Game
Scenario: Button → Balloon → Score display
1. Button Component (emits clicks)
// clicker-button/index.html
let clickCount = 0;
document.getElementById('button').addEventListener('click', () => {
clickCount++;
// Emit click event
window.parent.postMessage({
type: 'component-event',
event: 'action:click',
data: { clickCount }
}, '*');
});2. Balloon Component (listens to clicks, emits pop)
// balloon-viewer/index.html
let size = 0;
const maxSize = 10;
// Listen for clicks
window.addEventListener('message', (event) => {
if (event.data.type === 'board-event' &&
event.data.event === 'action:click') {
size++;
growBalloon(size);
// Emit pop event when max size
if (size >= maxSize) {
window.parent.postMessage({
type: 'component-event',
event: 'action:pop',
data: { finalSize: size }
}, '*');
resetBalloon();
}
}
});3. Score Component (listens to clicks)
// score-display/index.html
let score = 0;
// Listen for clicks
window.addEventListener('message', (event) => {
if (event.data.type === 'board-event' &&
event.data.event === 'action:click') {
score++;
updateScoreDisplay(score);
}
});See Full Tutorial: Balloon Game Tutorial
Best Practices
1. Use Namespaced Events
Bad:
event: 'click' // Too generic
event: 'update' // Too vagueGood:
event: 'action:click' // Clear intent
event: 'data:update' // Specific purpose
event: 'balloon:pop' // Component-specific2. Include Metadata
Always include useful context in event data:
window.parent.postMessage({
type: 'component-event',
event: 'action:click',
data: {
clickCount: 5,
timestamp: Date.now(), // When it happened
componentId: 'clicker-btn-1' // Who sent it (optional)
}
}, '*');3. Validate Event Data
Check that received data is valid:
window.addEventListener('message', (event) => {
if (event.data.type !== 'board-event') return;
const { event: eventName, data } = event.data;
if (eventName === 'action:click') {
// Validate data structure
if (!data || typeof data.clickCount !== 'number') {
console.warn('Invalid click event data:', data);
return;
}
handleClick(data.clickCount);
}
});4. Debounce Frequent Events
For high-frequency events (mousemove, resize), use debouncing:
let debounceTimer;
function emitPositionChange(x, y) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
window.parent.postMessage({
type: 'component-event',
event: 'cursor:move',
data: { x, y }
}, '*');
}, 100); // Emit max once per 100ms
}System Events (from BoardAPI)
BoardAPI also sends system events to components:
props-update
Sent when component props change:
window.addEventListener('message', (event) => {
if (event.data.type === 'props-update') {
const newProps = event.data.props;
updateComponent(newProps);
}
});board-ready
Sent when board finishes loading:
window.addEventListener('message', (event) => {
if (event.data.type === 'board-ready') {
console.log('Board is ready!');
initializeComponent();
}
});Debugging Events
Use browser console to monitor events:
// Log all incoming messages
window.addEventListener('message', (event) => {
console.log('[MESSAGE]', event.data);
});
// Log outgoing events
const originalPostMessage = window.parent.postMessage;
window.parent.postMessage = function(message, targetOrigin) {
console.log('[EMIT]', message);
originalPostMessage.call(window.parent, message, targetOrigin);
};Event System API Reference
For complete API documentation, see:
- Event System API - Full reference with all event types
- WebSocket API - Real-time event streaming
External Data Integration
Components can fetch data from external sources through BoardAPI's data provider system.
How It Works
graph LR
A[Component] -->|Request| B[BoardAPI]
B -->|Webhook| C[Your Backend]
C -->|JSON Response| B
B -->|externalData| AFlow:
- Component requests data via
window.externalData - BoardAPI calls your configured webhook/data provider
- Your backend returns JSON data
- BoardAPI passes data to component
Accessing External Data
In your component:
// Access external data passed from BoardAPI
const externalData = window.externalData;
if (externalData) {
console.log('Restaurant data:', externalData.name);
console.log('Menu items:', externalData.menu);
renderData(externalData);
} else {
// Show loading or error state
showError('No data available');
}Data Provider Configuration
When creating a board with external data:
{
"title": "Restaurant Menu Board",
"data_provider": {
"type": "webhook",
"url": "https://api.yourapp.com/restaurant/pizza-palace",
"method": "GET",
"headers": {
"Authorization": "Bearer YOUR_TOKEN"
}
},
"initial_state": {
"objects": [{
"type": "external-component",
"data": {
"componentUrl": "https://components.boardapi.io/restaurant-menu@1.0.0/index.html",
"props": {
"restaurantSlug": "pizza-palace"
}
}
}]
}
}Sandbox Mode with Mock Data
For testing without a backend, use mock_external_data:
{
"title": "Test Board",
"is_sandbox": true,
"mock_external_data": {
"name": "Test Restaurant",
"cuisine": "Italian",
"menu": [
{
"id": 1,
"name": "Pizza Margherita",
"price": 12.99,
"category": "Pizza"
}
]
}
}In component:
// Works with both real and mock data
const data = window.externalData || window.mockData;
renderRestaurant(data);Response Format
Your backend should return JSON that matches your component's expected structure:
{
"name": "Pizza Heaven",
"cuisine": "Italian",
"logo": "https://example.com/logo.png",
"menu": [
{
"id": 1,
"name": "Margherita Pizza",
"description": "Fresh mozzarella, basil, tomato",
"price": 12.99,
"category": "Pizza"
}
]
}Error Handling
Always handle cases where external data might be missing:
function initComponent() {
const data = window.externalData;
if (!data) {
return showError('No data provided');
}
if (!data.menu || data.menu.length === 0) {
return showEmpty('No menu items available');
}
renderMenu(data.menu);
}
initComponent();Best Practices
- Validate data structure - Don't assume fields exist
- Show loading states - Data might arrive asynchronously
- Handle errors gracefully - Network issues can occur
- Use TypeScript - Define interfaces for expected data
- Test with mock data - Use sandbox mode during development
Example: Restaurant Menu Component
See the complete tutorial: Restaurant Menu with External Data
Next Steps
- Publishing Components - Learn how to upload and version components
- Examples - See real-world component implementations
- Balloon Game Tutorial - Build a multi-component game