FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
c15r
c15rChat
Public
Like
Chat
Home
Code
18
backend
1
frontend
6
shared
2
test
4
AFFORDANCE-COMPONENT-GUIDE.md
AFFORDANCE-FRAMEWORK.md
AFFORDANCE-IMPLEMENTATION-SUMMARY.md
COMMAND-PALETTE-REVIEW.md
DEPENDENCY-INJECTION-REVIEW.md
IMPLEMENTATION-SUMMARY-AFFORDANCES.md
IMPLEMENTATION-SUMMARY.md
NextSteps-Examples.md
NextSteps-README.md
README.md
ResourceViewer-README.md
TESTING.md
package.json
H
test-runner.ts
Branches
1
Pull requests
Remixes
1
History
Environment variables
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data โ€“ all from the browser, and deployed in milliseconds.
Sign up now
Code
/
AFFORDANCE-COMPONENT-GUIDE.md
Code
/
AFFORDANCE-COMPONENT-GUIDE.md
Search
6/27/2025
Viewing readonly version of main branch: v1234
View latest version
AFFORDANCE-COMPONENT-GUIDE.md

Affordance Component Development Guide

Quick Start

To create a component that works with the affordance system, you need JavaScript/TypeScript code that exports a class implementing the AffordanceComponent interface. This code can come from:

  • MCP file sources (recommended) - Use MCP tools to create/manage component files
  • Local project files - Files in the /frontend/components/affordances/ directory
  • Any accessible source - The system will try MCP first, then fall back to local files

Required Interface

interface AffordanceComponent { mount(container: HTMLElement, config: AffordanceConfig): Promise<void>; unmount(): Promise<void>; getPublicMethods(): Record<string, Function>; [customMethod: string]: any; }

Minimal Example

/** @jsxImportSource https://esm.sh/react@18.2.0 */ import React from "https://esm.sh/react@18.2.0"; import { createRoot } from "https://esm.sh/react-dom@18.2.0/client"; import { AffordanceComponent, AffordanceConfig } from "../../../shared/affordance-types.ts"; // Your React component const MyComponent: React.FC<{message: string}> = ({ message }) => { return <div style={{padding: '1rem'}}>{message}</div>; }; // Affordance wrapper class (REQUIRED) export default class MyAffordance implements AffordanceComponent { private root: any = null; private container: HTMLElement | null = null; private message: string = "Hello!"; async mount(container: HTMLElement, config: AffordanceConfig): Promise<void> { this.container = container; this.message = config.message || "Hello!"; this.root = createRoot(container); this.root.render(<MyComponent message={this.message} />); } async unmount(): Promise<void> { if (this.root) { this.root.unmount(); this.root = null; } this.container = null; } getPublicMethods(): Record<string, Function> { return { setMessage: this.setMessage.bind(this), getMessage: this.getMessage.bind(this) }; } // Custom methods setMessage(newMessage: string): void { this.message = newMessage; if (this.root) { this.root.render(<MyComponent message={this.message} />); } } getMessage(): string { return this.message; } }

File Requirements

  1. Content: Must contain JavaScript/TypeScript code that exports a class
  2. Export: Must have a default export of the affordance class
  3. Dependencies: Use ESM imports from https://esm.sh/ for compatibility
  4. React: Pin React version to 18.2.0 for consistency
  5. Source: Can be from MCP files, local files, or any accessible source

Loading Priority

The system tries to load components in this order:

  1. MCP source - Uses files-get tool to fetch from MCP server
  2. Local file - Falls back to local project files
  3. Error - If neither source works, provides detailed error message

Available Example Components

  1. TestAffordance (/frontend/components/affordances/TestAffordance.tsx)

    • Simple test component with click counter
    • Methods: getMessage(), setMessage(msg), ping()
  2. CounterAffordance (/frontend/components/affordances/CounterAffordance.tsx)

    • Counter with increment/decrement
    • Methods: getValue(), setValue(n), increment(step?), decrement(step?), reset()
  3. StatusDashboard (/frontend/components/affordances/StatusDashboard.tsx)

    • Status monitoring dashboard
    • Methods: addItem(item), updateItem(id, updates), removeItem(id), clearItems(), getItems()
  4. NotificationCenter (/frontend/components/affordances/NotificationCenter.tsx)

    • Notification management
    • Methods: addNotification(notif), removeNotification(id), clearAll(), getCount()

Container Types & Best Practices

Overlay

  • Best for: Modals, dialogs, forms, image viewers
  • Config: modal: true/false, backdrop: true/false, position: 'center'|'top'|'bottom'
  • Size: Use maxWidth, maxHeight for responsive design

Sidebar

  • Best for: Tool palettes, navigation, dashboards, file browsers
  • Config: position: 'left'|'right', width: '300px', collapsible: true
  • Size: Fixed width recommended (250px-400px)

Header

  • Best for: Status indicators, breadcrumbs, quick actions
  • Config: position: 'left'|'right', priority: number
  • Size: Keep compact, height auto-adjusts

Footer

  • Best for: Status bars, progress indicators, quick stats
  • Config: position: 'left'|'right', priority: number
  • Size: Keep compact, integrates with existing footer

Inline

  • Best for: Widgets, charts, interactive content in chat
  • Config: width: '100%', height: '300px'
  • Size: Responsive width, fixed height often works best

Usage Examples

Using MCP File Source

// 1. Create component using MCP tools (files-create, etc.) // 2. Register the component by file key const id = await register_affordance('sidebar', 'components/my-dashboard.js', // MCP file key { title: 'My Dashboard', position: 'right' } );

Using Local File

// Register a local component file const id = await register_affordance('overlay', '/frontend/components/affordances/TestAffordance.tsx', { title: 'Test Widget' } );

Check Methods and Interact

// Check available methods const methods = await call_affordance_method(id, 'getPublicMethods', []); // Call component methods await call_affordance_method(id, 'setMessage', ['Hello World!']);

Common Patterns

State Management

class MyAffordance implements AffordanceComponent { private data: any[] = []; addData(item: any): void { this.data.push(item); this.render(); // Re-render with new data } private render(): void { if (this.root) { this.root.render(<MyComponent data={this.data} />); } } }

Configuration Handling

async mount(container: HTMLElement, config: AffordanceConfig): Promise<void> { // Extract config with defaults const title = config.title || "Default Title"; const theme = config.theme || "light"; const autoRefresh = config.autoRefresh || false; // Use config in component this.root.render(<MyComponent title={title} theme={theme} />); }

Cleanup

async unmount(): Promise<void> { // Clear timers if (this.timer) clearInterval(this.timer); // Remove event listeners if (this.eventHandler) { document.removeEventListener('click', this.eventHandler); } // Unmount React if (this.root) { this.root.unmount(); this.root = null; } }

Error Handling

The affordance system will catch and report:

  • Module loading errors (wrong file type, syntax errors)
  • Missing interface methods
  • Runtime errors in mount/unmount
  • Method call errors

Always test your components with:

// Register component const id = await register_affordance('overlay', '/path/to/component.tsx', {title: 'Test'}); // Test methods await call_affordance_method(id, 'getPublicMethods', []);

Debugging Tips

  1. Check Console: All affordance operations are logged with [AffordanceManager] prefix
  2. Use getPublicMethods: Always call this first to see available methods
  3. Test Incrementally: Start with simple components, add complexity gradually
  4. Validate Config: Check that your config properties are being used correctly
  5. React DevTools: Use browser dev tools to inspect React component tree
Go to top
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Product
FeaturesPricing
Developers
DocsStatusAPI ExamplesNPM Package Examples
Explore
ShowcaseTemplatesNewest ValsTrending ValsNewsletter
Company
AboutBlogCareersBrandhi@val.town
Terms of usePrivacy policyAbuse contact
ยฉ 2025 Val Town, Inc.