apprun
Version:
JavaScript library that has Elm inspired architecture, event pub-sub and components
291 lines (245 loc) • 8.71 kB
Markdown
---
mode: agent
---
When creating AppRun components, follow these guidelines for consistent, maintainable code.
- **HTML templates**: Always use `html` tagged literals for UI rendering
- **State management**: Handle state through event handlers that return new state objects
- **Event handling**: Use `event-name` syntax in templates with handlers `(state, param) => newState`
- **Local events**: Use `run()` for component-local events (no registration needed)
- **Immutable updates**: Always return new state objects, never mutate existing state
## Component Creation Patterns
### 1. Functional Components (Pure UI Components)
**When to use**: For reusable UI pieces that don't manage their own state.
```js
// Create pure function components that take props and return HTML
export const AgentModal = (agent, onClose) => {
return html`
<div class="modal-overlay" @click=${run(onClose, false)}>
<div class="modal-content" @click=${(e) => e.stopPropagation()}>
<!-- Use conditional rendering for dynamic content -->
${agent.status ? html`<h2>${agent.name}</h2>` : html`
<input value="${agent.name || ''}" @input=${(e) => agent.name = e.target.value}>
`}
</div>
</div>
`;
};
```
**Rules for functional components**:
- Export as pure functions
- Accept props as parameters
- Return HTML template literals
- Handle events through callback props
- No internal state management
**When to use**: For main application views that manage state and handle complex interactions.
**Step 1: Create async state initialization**
```js
// Always use async state function for API calls and data loading
const state = async () => {
const data = await api.getData();
return {
...initialState,
data,
loading: false
};
};
```
**Step 2: Define event handlers as separate functions**
```js
// Create specific handlers for each user interaction
const selectWorld = async (state, worldName) => {
if (worldName === state.worldName) return state;
const agents = await api.getAgents(worldName);
return ({ ...state, worldName, agents });
};
const openModal = (state, item = null) => {
return ({
...state,
editingItem: item || { name: 'New Item' },
showModal: true
});
};
```
**Step 3: Create view function with proper rendering patterns**
```js
// View function should be pure - only render, never mutate
const view = (state) => {
return html`
<div class="container">
${state.items.map(item => html`
<div class="item" @click=${run(openModal, item)}>
${item.name}
</div>
`)}
${state.showModal ? ModalComponent(state.editingItem, closeModal) : ''}
</div>
`;
};
```
**Step 4: Define minimal update object for routing**
```js
// Keep update object minimal - most events handled by run()
const update = {
'/,#': state => state,
// Only add global events or routing here
};
```
**Step 5: Export Component instance**
```js
// Create and export component instance without class syntax
// enable global events with {global_event: true}
export default new Component(state, view, update, {global_event: true});
// Mount to DOM element
component.start('#main'); // For visible components
component.mount('#modal'); // For modal/hidden components
```
**✅ Correct patterns**:
```js
// Simple event with parameter
@click=${run(selectWorld, world.name)}
// Event with callback function
@click=${run(closeModal, false)}
// Global event
@input=${run('updateModalAgentName')}
```
**❌ Common mistakes to avoid**:
```js
// DON'T wrap run() in arrow function - prevents re-rendering
@click=${() => run(selectWorld, world.name)}
// DON'T manually pass event in arrow function
@click=${(e) => run(updateName, e)}
```
```js
// Event handlers automatically receive event as last parameter
const updateModalAgentName = (state, eventParam) => {
const name = eventParam.target.value;
return ({...state, agentName: name });
};
// Handle event.stopPropagation() in handler when needed
const openAgentModal = (state, agent, e) => {
e.stopPropagation();
return ({...state, editingAgent: agent, showModal: true });
};
```
```js
// Simple boolean conditions
${state.loading ? html`<div>Loading...</div>` : ''}
// Complex conditional with nested HTML blocks
${agent.status ? html`
<h2>${agent.name}</h2>
<p>Status: Active</p>
` : html`
<input value="${agent.name || ''}" @input=${run(updateName)}>
<button @click=${run(saveAgent)}>Save</button>
`}
```
```js
// Basic list rendering
${state.items.map(item => html`
<div class="${item.active ? 'active' : ''}">${item.name}</div>
`)}
// Complex list items with events
${state.agents.map(agent => html`
<div class="agent-card" @click=${run(selectAgent, agent.id)}>
<h3>${agent.name}</h3>
<button @click=${run(editAgent, agent)}
@click=${(e) => e.stopPropagation()}>
Edit
</button>
</div>
`)}
```
```js
// Pass data and callbacks to child components
${state.showModal ? AgentModal(state.editingAgent, closeModal) : ''}
// Conditional component rendering with fallbacks
${state.currentView === 'list'
? AgentList(state.agents, selectAgent)
: AgentDetail(state.selectedAgent, goBack)
}
```
```js
// ✅ Always spread existing state for updates
return ({ ...state, newProperty: value });
// ✅ Update nested objects immutably
return ({
...state,
user: { ...state.user, name: newName },
items: [...state.items, newItem]
});
// ❌ Never mutate state directly
state.newProperty = value; // Wrong!
state.items.push(newItem); // Wrong!
```
```js
// Standard async handler
const handler = async (state, param) => {
try {
const data = await api.call(param);
return ({ ...state, data, loading: false });
} catch (error) {
return ({ ...state, error: error.message, loading: false });
}
};
// Generator for progressive updates
const handler = async function* (state, param) {
yield ({ ...state, loading: true });
try {
const data = await api.call(param);
yield ({ ...state, data, loading: false });
} catch (error) {
yield ({ ...state, error: error.message, loading: false });
}
};
```
```js
// Always handle errors in async operations
const saveData = async (state, data) => {
try {
await api.saveData(data);
return ({ ...state, showModal: false, saved: true });
} catch (error) {
return ({ ...state, error: `Save failed: ${error.message}` });
}
};
```
When creating any AppRun component, ensure you follow these rules:
- ✅ **Event handlers must return new state** - No return = no re-render
- ✅ **View functions are pure** - Only render, never mutate state
- ✅ **Use `run()` for local events** - No registration needed in update object
- ✅ **Use `app.run()` for global events** - Cross-component communication
- ✅ **Immutable state updates** - Always spread existing state `{...state, newProp}`
- ✅ **HTML template literals** - Use `html` tagged templates for all UI
- ✅ **Conditional rendering** - Use ternary operators and template conditionals
- ✅ **Functional components**: Export pure functions that take props and return HTML
- ✅ **Stateful components**: Use async state, separate handlers, Component instance
- ✅ **Error boundaries**: Always handle async errors in try/catch blocks
- ✅ **Component composition**: Pass data and callbacks to child components
- ✅ **Event delegation**: Use event.stopPropagation() when needed in handlers
- ✅ **One component per file** - Keep components focused and reusable
- ✅ **Named exports for functions** - `export const ComponentName = (...) => html`
- ✅ **Default export for pages** - `export default new Component(...)`
- ✅ **Import dependencies** - Use static imports, omit `.js`/`.ts` extensions
### Performance Considerations
- ✅ **Minimal update object** - Only include routing and global events
- ✅ **Efficient rendering** - Use conditional rendering to avoid unnecessary DOM updates
- ✅ **State normalization** - Keep state flat when possible for easier updates
- ✅ **Event handler optimization** - Define handlers outside render when possible