FeaturesTemplatesShowcaseTownie
AI
BlogDocsPricing
Log inSign up
c15r
c15rChat
Public
Like
Chat
Home
Code
20
backend
1
frontend
6
shared
2
test
4
AFFORDANCE-COMPONENT-GUIDE.md
AFFORDANCE-FRAMEWORK.md
AFFORDANCE-IMPLEMENTATION-SUMMARY.md
AFFORDANCE-MANAGER-INITIALIZATION.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
STREAMING-IMPROVEMENTS.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/30/2025
Viewing readonly version of main branch: v1328
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 AffordanceMethodSchema { description: string; inputSchema: { type: "object"; properties: Record<string, any>; required?: string[]; }; } interface AffordanceComponent { mount(container: HTMLElement, config: AffordanceConfig, mcpClientPool?: any): Promise<void>; unmount(): Promise<void>; getPublicMethods(): Record<string, AffordanceMethodSchema>; [methodName: string]: any; // Methods can be called directly on the component }

Key Points:

  • getPublicMethods() returns method schemas (not function references)
  • Methods are called directly on the component instance
  • The schemas help the assistant understand what methods are available and how to call them

How Method Calling Works

  1. Schema Definition: getPublicMethods() returns an object describing available methods:

    getPublicMethods(): Record<string, AffordanceMethodSchema> { return { methodName: { description: "What this method does", inputSchema: { type: "object", properties: { param1: { type: "string", description: "First parameter" }, param2: { type: "number", description: "Second parameter" } }, required: ["param1"] } } }; }
  2. Method Implementation: Implement the actual methods on your class:

    methodName(param1: string, param2?: number): any { // Your method implementation return result; }
  3. Method Calling: The affordance manager calls methods directly on your component:

    // Assistant calls this: await call_affordance_method(domId, 'methodName', ['value1', 42]); // Which translates to: component.methodName('value1', 42);

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, AffordanceMethodSchema } 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, mcpClientPool?: any): 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, AffordanceMethodSchema> { return { setMessage: { description: "Set a new message to display", inputSchema: { type: "object", properties: { newMessage: { type: "string", description: "The new message to display" } }, required: ["newMessage"] } }, getMessage: { description: "Get the current message", inputSchema: { type: "object", properties: {}, required: [] } } }; } // These methods will be called directly by the affordance manager 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

No example components are currently included in the project. You can create your own affordance components by following the interface described above. The components should be saved as MCP files or created in the /frontend/components/affordances/ directory.

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 attach_affordance('sidebar', 'components/my-dashboard.js', // MCP file key { title: 'My Dashboard', position: 'right' } );

Using Local File

// 1. Create component file in /frontend/components/affordances/ // 2. Register the component by local path const id = await attach_affordance('overlay', '/frontend/components/affordances/MyComponent.tsx', { title: 'My Component', modal: true } );

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


### Check Methods and Interact
```javascript
// 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.