# AppRun Development Guide for AI Coding Assistants ## Quick Decision Tree: What Component Should I Create? **START HERE:** Ask yourself these questions in order: 1. **Does it manage its own state and handle user interactions?** - YES → Use **Stateful Class Component** (Pattern A) - NO → Go to question 2 2. **Is it a popup/modal/overlay that appears on demand?** - YES → Use **Popup Component** (Pattern B) - NO → Go to question 3 3. **Does it only display data passed from parent?** - YES → Use **Functional Component** (Pattern C) - NO → You might need a combination - start with Pattern A --- ## Pattern A: Stateful Class Component (Self-Contained) **Use for:** Forms, interactive widgets, components with internal logic ### Template Structure ```typescript // 1. IMPORTS import { app, Component } from 'apprun'; import type { YourDataType } from '../types'; import api from '../api'; // 2. INTERFACES (Always define these first) interface ComponentProps { requiredProp: string; optionalProp?: string; parentComponent?: any; } export interface ComponentState { // Always include these three loading: boolean; error: string | null; successMessage?: string | null; // Your specific state formData: Partial; mode: 'create' | 'edit' | 'delete'; } // 3. HELPER FUNCTIONS (Export for testing) const getStateFromProps = (props: ComponentProps): ComponentState => ({ loading: false, error: null, formData: props.data || {}, mode: props.mode || 'create' }); // 4. ACTION FUNCTIONS (Export for $onclick references) export const saveData = async function* (state: ComponentState): AsyncGenerator { // Validation first if (!state.formData.name?.trim()) { yield { ...state, error: 'Name is required' }; return; } yield { ...state, loading: true, error: null }; try { if (state.mode === 'create') { await api.create(state.formData); } else { await api.update(state.formData.id, state.formData); } yield { ...state, loading: false, successMessage: 'Saved successfully!' }; // Notify parent after 2 seconds setTimeout(() => { state.parentComponent?.run('data-saved'); }, 2000); } catch (error: any) { yield { ...state, loading: false, error: error.message || 'Save failed' }; } }; export const deleteData = async function* (state: ComponentState): AsyncGenerator { yield { ...state, loading: true, error: null }; try { await api.delete(state.formData.id); yield { ...state, loading: false, successMessage: 'Deleted successfully!' }; setTimeout(() => state.parentComponent?.run('data-deleted'), 2000); } catch (error: any) { yield { ...state, loading: false, error: error.message || 'Delete failed' }; } }; export const closeComponent = (): void => { app.run('close-component'); }; // 5. COMPONENT CLASS export default class YourComponent extends Component { declare props: Readonly; mounted = (props: ComponentProps): ComponentState => getStateFromProps(props); view = (state: ComponentState) => { // GUARD CLAUSES FIRST (early returns) if (state.successMessage) { return (

{state.successMessage}

Closing...
); } if (state.error) { return (

Error: {state.error}

); } if (state.loading) { return
Loading...
; } // MAIN CONTENT return (
); }; } ``` --- ## Pattern B: Popup Component (Modal) **Use for:** Any overlay that appears on demand ```typescript export default class ModalComponent extends Component { declare props: Readonly; mounted = (props: ModalProps): ModalState => getStateFromProps(props); view = (state: ModalState) => { // Success message auto-closes if (state.successMessage) { return (
e.stopPropagation()}>

Success!

{state.successMessage}

Closing...
); } return (
e.stopPropagation()}>

{state.title}

{state.error &&
{state.error}
}
{/* Form fields */}
); }; } ``` ## Pattern C: Functional Component (Display Only) **Use for:** Components that only render data from props ```typescript export interface ComponentProps { data: DataType[]; selectedItem?: DataType | null; loading?: boolean; onItemClick?: (item: DataType) => void; } export default function DisplayComponent(props: ComponentProps) { // Destructure with defaults const { data = [], selectedItem = null, loading = false, onItemClick } = props; // Guard clauses if (loading) { return
Loading...
; } if (data.length === 0) { return
No data available
; } // Main render return (
{data.map((item, index) => { const isSelected = selectedItem?.id === item.id; return (
onItemClick?.(item)} >
{item.name}
{item.description}
); })}
); } ``` --- ## Parent-Child Integration Patterns ### Parent Component (Coordinates Children) ```typescript export default class ParentComponent extends Component { view = (state: ParentState) => (
{/* Main content */} this.run('select-item', item)} /> {/* Conditional popup rendering */} {state.showModal && }
); update = { 'select-item': (state, item) => ({ ...state, selectedItem: item }), 'open-create-modal': (state) => ({ ...state, showModal: true, modalMode: 'create', selectedItemForEdit: null }), 'close-modal': (state) => ({ ...state, showModal: false }), // Global events from children 'data-saved': (state) => { location.reload(); // Simple refresh } }; } ``` --- ## Essential Rules & Checklists ### Event Handling Rules (Critical) | Pattern | Use Case | Example | |---------|----------|---------| | `$bind="field"` | Form fields (preferred) | `` | | `$onclick={[func]}` | Direct function call | `