Skip to content

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 point
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.md

manifest.json Reference

The manifest file describes your component to BoardAPI.

Minimal Example

json
{
  "name": "my-component",
  "version": "1.0.0",
  "entry": "index.html"
}

Full Example

json
{
  "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

FieldTypeDescription
namestringComponent identifier (lowercase, hyphens only)
versionstringSemantic version (e.g., "1.0.0")
entrystringEntry point file path (relative to ZIP root)

Optional Metadata

FieldTypeDescription
descriptionstringShort description of component
authorstringAuthor name and email
licensestringLicense identifier (e.g., "MIT", "Apache-2.0")
homepagestringComponent website URL
repositoryobjectGit repository info
keywordsstring[]Search keywords

Component Configuration

FieldTypeDefaultDescription
component.widthnumber200Default width in pixels
component.heightnumber150Default height in pixels
component.minWidthnumber100Minimum width
component.minHeightnumber100Minimum height
component.maxWidthnumber1000Maximum width
component.maxHeightnumber1000Maximum height
component.resizablebooleantrueCan user resize?
component.aspectRatiostring"free""free", "fixed", or ratio like "16:9"

Props Schema (component.schema)

Define the data your component accepts using JSON Schema.

json
{
  "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

html
<!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:

javascript
// 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:

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

Emitting Events

Send events to other components:

javascript
function emitEvent(eventName, data) {
  window.parent.postMessage({
    type: 'component-event',
    event: eventName,
    data: data
  }, '*');
}

// Usage
emitEvent('item-clicked', { itemId: 123 });

Listening to Board Events

javascript
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.woff2

Referencing Assets

Use relative paths from your entry point:

html
<!-- 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:

html
<!-- 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

Smallest bundle size, fastest load time:

html
<script>
  const app = {
    data: window.componentProps || {},

    render() {
      document.getElementById('root').innerHTML = `
        <h1>${this.data.title}</h1>
      `;
    }
  };

  app.render();
</script>

Vue 3 (via CDN)

html
<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)

html
<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:

bash
# package.json
{
  "scripts": {
    "build": "vite build"
  }
}

Point entry to built file:

json
{
  "entry": "dist/index.html"
}

Best Practices

1. Scope Your Styles

Prevent CSS conflicts:

css
/* Bad - global styles */
h1 { color: red; }

/* Good - scoped */
.my-component h1 { color: red; }

2. Handle Missing Props

Always provide defaults:

javascript
const title = props.title || 'Untitled';
const items = props.items || [];

3. Responsive Design

Support different sizes:

css
.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:

javascript
let loading = true;

async function loadData() {
  loading = true;
  render();

  const data = await fetch('/api/data');
  loading = false;
  render();
}

5. Error Handling

Gracefully handle errors:

javascript
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:

javascript
// 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:

FieldTypeRequiredDescription
typestringYesMust be 'component-event'
eventstringYesEvent name (use colon notation: namespace:action)
dataobjectNoEvent 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:pop is better than pop

Listening to Events

To receive events from other components:

javascript
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.

javascript
// 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.

javascript
// 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.

javascript
// 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)

javascript
// 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)

javascript
// 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)

javascript
// 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:

javascript
event: 'click'       // Too generic
event: 'update'      // Too vague

Good:

javascript
event: 'action:click'     // Clear intent
event: 'data:update'      // Specific purpose
event: 'balloon:pop'      // Component-specific

2. Include Metadata

Always include useful context in event data:

javascript
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:

javascript
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:

javascript
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:

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

board-ready

Sent when board finishes loading:

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

Debugging Events

Use browser console to monitor events:

javascript
// 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:

External Data Integration

Components can fetch data from external sources through BoardAPI's data provider system.

How It Works

mermaid
graph LR
    A[Component] -->|Request| B[BoardAPI]
    B -->|Webhook| C[Your Backend]
    C -->|JSON Response| B
    B -->|externalData| A

Flow:

  1. Component requests data via window.externalData
  2. BoardAPI calls your configured webhook/data provider
  3. Your backend returns JSON data
  4. BoardAPI passes data to component

Accessing External Data

In your component:

javascript
// 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:

json
{
  "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:

json
{
  "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:

javascript
// 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:

json
{
  "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:

javascript
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

  1. Validate data structure - Don't assume fields exist
  2. Show loading states - Data might arrive asynchronously
  3. Handle errors gracefully - Network issues can occur
  4. Use TypeScript - Define interfaces for expected data
  5. Test with mock data - Use sandbox mode during development

Example: Restaurant Menu Component

See the complete tutorial: Restaurant Menu with External Data

Next Steps