apprun 3.37.3 → 3.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/WHATSNEW.md +6 -0
- package/ai/apprun.prompt.md +545 -109
- package/apprun.d.ts +17 -5
- package/dist/apprun-code.js +1 -1
- package/dist/apprun-code.js.map +1 -1
- package/dist/apprun-dev-tools.js +1 -1
- package/dist/apprun-dev-tools.js.map +1 -1
- package/dist/apprun-html.esm.js +7 -7
- package/dist/apprun-html.esm.js.map +1 -1
- package/dist/apprun-html.js +1 -1
- package/dist/apprun-html.js.map +1 -1
- package/dist/apprun-play-html.esm.js +7 -7
- package/dist/apprun-play-html.esm.js.map +1 -1
- package/dist/apprun-play.js +1 -1
- package/dist/apprun-play.js.map +1 -1
- package/dist/apprun.esm.js +1 -1
- package/dist/apprun.esm.js.map +1 -1
- package/dist/apprun.js +1 -1
- package/dist/apprun.js.map +1 -1
- package/dist/createState.js.map +1 -1
- package/esm/apprun-play.js.map +1 -1
- package/esm/apprun.js.map +1 -1
- package/esm/directive.js +129 -8
- package/esm/directive.js.map +1 -1
- package/esm/version.js +1 -1
- package/jsx-runtime.js +1 -1
- package/jsx-runtime.js.map +1 -1
- package/package.json +1 -1
- package/src/apprun-play.tsx +1 -1
- package/src/apprun.ts +2 -2
- package/src/component.ts +1 -1
- package/src/createState.ts +1 -1
- package/src/directive.ts +132 -7
- package/src/global.d.ts +2 -2
- package/src/types.ts +3 -3
- package/src/version.ts +1 -1
package/ai/apprun.prompt.md
CHANGED
|
@@ -1,166 +1,602 @@
|
|
|
1
|
-
---
|
|
2
|
-
mode: agent
|
|
3
|
-
---
|
|
4
1
|
# AppRun Component Creation Guide
|
|
5
2
|
|
|
6
|
-
## Core
|
|
3
|
+
## Core Architecture Patterns
|
|
7
4
|
|
|
8
|
-
AppRun follows the State-View-Update architecture
|
|
5
|
+
AppRun follows the State-View-Update architecture with TypeScript support. Choose the appropriate component pattern based on your needs:
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
### **Stateful Class Components (Self-Contained)**
|
|
8
|
+
Use for components that manage their own state internally, handle side effects, and coordinate with parents via global events. Modern pattern using `mounted` lifecycle.
|
|
11
9
|
|
|
12
|
-
### **
|
|
13
|
-
|
|
14
|
-
2. Initialize with sensible defaults including loading/error states
|
|
15
|
-
3. Plan for immutable updates using spread operator
|
|
16
|
-
4. Include states for: loading, error, data, UI state
|
|
10
|
+
### **Container Class Components (Legacy)**
|
|
11
|
+
Use for components that manage state, handle side effects, and coordinate data flow using traditional state initialization.
|
|
17
12
|
|
|
13
|
+
### **Functional Components (Presentation/Dumb Components)**
|
|
14
|
+
Use for components that only render UI based on props with minimal logic.
|
|
15
|
+
|
|
16
|
+
## Component Patterns
|
|
17
|
+
|
|
18
|
+
### **Pattern 1: Stateful Class Component (Modern - Recommended)**
|
|
19
|
+
|
|
20
|
+
Use `mounted` lifecycle to receive props and convert to initial state. Functions defined at module level for better testing.
|
|
21
|
+
|
|
22
|
+
#### **Module-Level Functions (separate file)**
|
|
18
23
|
```typescript
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
// component-functions.ts
|
|
25
|
+
export interface ComponentState {
|
|
26
|
+
mode: 'create' | 'edit' | 'delete';
|
|
27
|
+
formData: FormType;
|
|
22
28
|
loading: boolean;
|
|
23
29
|
error: string | null;
|
|
30
|
+
successMessage: string | null;
|
|
24
31
|
}
|
|
25
|
-
```
|
|
26
32
|
|
|
27
|
-
|
|
33
|
+
export interface ComponentProps {
|
|
34
|
+
mode?: string;
|
|
35
|
+
initialData?: FormType;
|
|
36
|
+
}
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
export const initializeState = (props: ComponentProps): ComponentState => ({
|
|
39
|
+
mode: props.mode || 'create',
|
|
40
|
+
formData: props.initialData || getDefaultFormData(),
|
|
41
|
+
loading: false,
|
|
42
|
+
error: null,
|
|
43
|
+
successMessage: null
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const saveData = async function* (state: ComponentState): AsyncGenerator<ComponentState> {
|
|
47
|
+
yield { ...state, loading: true, error: null };
|
|
48
|
+
try {
|
|
49
|
+
await performSave(state.formData);
|
|
50
|
+
yield {
|
|
51
|
+
...state,
|
|
52
|
+
loading: false,
|
|
53
|
+
successMessage: 'Data saved successfully!'
|
|
54
|
+
};
|
|
55
|
+
setTimeout(() => app.run('data-saved'), 2000);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
yield { ...state, loading: false, error: error.message };
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const closeModal = (): void => {
|
|
62
|
+
app.run('close-modal');
|
|
63
|
+
};
|
|
64
|
+
```
|
|
34
65
|
|
|
66
|
+
#### **Component Structure**
|
|
35
67
|
```typescript
|
|
68
|
+
// component.tsx
|
|
69
|
+
import { initializeState, saveData, closeModal, type ComponentState, type ComponentProps } from './component-functions';
|
|
70
|
+
|
|
36
71
|
export default class MyComponent extends Component<ComponentState> {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
declare props: Readonly<ComponentProps>;
|
|
73
|
+
mounted = (props: ComponentProps): ComponentState => initializeState(props);
|
|
74
|
+
|
|
75
|
+
view = (state: ComponentState) => {
|
|
76
|
+
// Guard clauses
|
|
77
|
+
if (state.successMessage) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="modal-backdrop" $onclick={[closeModal]}>
|
|
80
|
+
<div className="success-message">
|
|
81
|
+
<p>{state.successMessage}</p>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="modal-backdrop" $onclick={[closeModal]}>
|
|
89
|
+
<form>
|
|
90
|
+
<input
|
|
91
|
+
value={state.formData.name}
|
|
92
|
+
$bind="formData.name"
|
|
93
|
+
disabled={state.loading}
|
|
94
|
+
/>
|
|
95
|
+
<button
|
|
96
|
+
$onclick={[saveData]}
|
|
97
|
+
disabled={state.loading}
|
|
98
|
+
>
|
|
99
|
+
{state.loading ? 'Saving...' : 'Save'}
|
|
100
|
+
</button>
|
|
101
|
+
<button $onclick={[closeModal]}>Cancel</button>
|
|
102
|
+
</form>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
46
106
|
}
|
|
47
107
|
```
|
|
48
108
|
|
|
49
|
-
|
|
109
|
+
### **Pattern 2: Legacy Container Component**
|
|
50
110
|
|
|
51
|
-
|
|
111
|
+
Traditional pattern with state initialization function.
|
|
112
|
+
|
|
113
|
+
#### **Component Structure**
|
|
52
114
|
```typescript
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
115
|
+
export default class MyComponent extends Component<ComponentState> {
|
|
116
|
+
state = async (): Promise<ComponentState> => {
|
|
117
|
+
// Initial state with async data loading
|
|
118
|
+
try {
|
|
119
|
+
const data = await loadData();
|
|
120
|
+
return {
|
|
121
|
+
data,
|
|
122
|
+
loading: false,
|
|
123
|
+
error: null,
|
|
124
|
+
selectedItem: null,
|
|
125
|
+
isEditing: false
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
data: [],
|
|
130
|
+
loading: false,
|
|
131
|
+
error: error.message,
|
|
132
|
+
selectedItem: null,
|
|
133
|
+
isEditing: false
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
view = (state: ComponentState) => {
|
|
139
|
+
// Guard clauses for early returns
|
|
140
|
+
if (state.loading) return <div>Loading...</div>;
|
|
141
|
+
if (state.error) return <div>Error: {state.error}</div>;
|
|
142
|
+
if (state.data.length === 0) return <div>No data</div>;
|
|
143
|
+
|
|
144
|
+
// Main content
|
|
145
|
+
return (
|
|
146
|
+
<div>
|
|
147
|
+
{/* Render main UI */}
|
|
148
|
+
<PresentationComponent
|
|
149
|
+
data={state.data}
|
|
150
|
+
selectedItem={state.selectedItem}
|
|
151
|
+
onSelect={(item) => this.run('select-item', item)}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
update = {
|
|
158
|
+
'select-item': (state: ComponentState, item: DataType): ComponentState => ({
|
|
159
|
+
...state,
|
|
160
|
+
selectedItem: item
|
|
161
|
+
}),
|
|
162
|
+
|
|
163
|
+
'async-action': async function* (state: ComponentState): AsyncGenerator<ComponentState> {
|
|
164
|
+
try {
|
|
165
|
+
yield { ...state, loading: true, error: null };
|
|
166
|
+
const result = await performAsyncAction();
|
|
167
|
+
yield { ...state, loading: false, data: result };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
yield { ...state, loading: false, error: error.message };
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
'side-effect-action': (state: ComponentState): void => {
|
|
174
|
+
// No return value = no re-render
|
|
175
|
+
window.location.href = '/new-page';
|
|
176
|
+
}
|
|
177
|
+
};
|
|
61
178
|
}
|
|
62
179
|
```
|
|
63
180
|
|
|
64
|
-
### **
|
|
65
|
-
- State-driven conditional rendering with guard clauses
|
|
66
|
-
- Early returns for cleaner code flow
|
|
67
|
-
- Dedicated views for each state (loading, error, empty, main)
|
|
181
|
+
### **Pattern 3: Functional Presentation Component**
|
|
68
182
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
183
|
+
```typescript
|
|
184
|
+
interface ComponentProps {
|
|
185
|
+
data: DataType[];
|
|
186
|
+
selectedItem: DataType | null;
|
|
187
|
+
loading?: boolean;
|
|
188
|
+
error?: string | null;
|
|
189
|
+
// Event handlers
|
|
190
|
+
onSelect?: (item: DataType) => void;
|
|
191
|
+
onDelete?: (id: string) => void;
|
|
192
|
+
}
|
|
76
193
|
|
|
77
|
-
|
|
194
|
+
export default function MyPresentationComponent(props: ComponentProps) {
|
|
195
|
+
const {
|
|
196
|
+
data,
|
|
197
|
+
selectedItem,
|
|
198
|
+
loading = false,
|
|
199
|
+
error = null,
|
|
200
|
+
onSelect,
|
|
201
|
+
onDelete
|
|
202
|
+
} = props;
|
|
203
|
+
|
|
204
|
+
// Guard clauses
|
|
205
|
+
if (loading) return <div>Loading...</div>;
|
|
206
|
+
if (error) return <div>Error: {error}</div>;
|
|
207
|
+
if (data.length === 0) return <div>No data</div>;
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div>
|
|
211
|
+
{data.map(item => (
|
|
212
|
+
<div
|
|
213
|
+
key={item.id}
|
|
214
|
+
$onclick={['select-item', item]}
|
|
215
|
+
className={selectedItem?.id === item.id ? 'selected' : ''}
|
|
216
|
+
>
|
|
217
|
+
{item.name}
|
|
218
|
+
<button $onclick={['delete-item', item.id]}>Delete</button>
|
|
219
|
+
</div>
|
|
220
|
+
))}
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Event Handling Rules
|
|
78
227
|
|
|
79
|
-
###
|
|
228
|
+
### **✅ DO: Use $on Directives for State Updates**
|
|
80
229
|
|
|
81
|
-
#### Action Pattern
|
|
82
230
|
```typescript
|
|
83
|
-
// String actions
|
|
84
|
-
$onclick=
|
|
231
|
+
// String actions (handled in parent's update object)
|
|
232
|
+
$onclick="action-name"
|
|
233
|
+
$oninput="update-field"
|
|
85
234
|
|
|
86
|
-
// Tuple actions
|
|
235
|
+
// Tuple actions (pass data to handler)
|
|
87
236
|
$onclick={['action-name', data]}
|
|
237
|
+
$oninput={['update-field', 'fieldName']}
|
|
238
|
+
$onchange={['update-dropdown', 'provider']}
|
|
239
|
+
|
|
240
|
+
// Direct function references (modern pattern - recommended)
|
|
241
|
+
$onclick={[saveFunction]}
|
|
242
|
+
$onclick={[deleteFunction]}
|
|
243
|
+
$onclick={[closeModal]}
|
|
88
244
|
```
|
|
89
245
|
|
|
90
|
-
|
|
91
|
-
1. Use `$onclick` for all click events
|
|
92
|
-
2. Simple actions: string literals
|
|
93
|
-
3. Data passing: tuple format `[action, data]`
|
|
94
|
-
4. Conditional actions: ternary in JSX
|
|
246
|
+
### **✅ DO: Use $bind for Two-Way Data Binding**
|
|
95
247
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
248
|
+
```typescript
|
|
249
|
+
// Automatic form field binding (modern pattern)
|
|
250
|
+
<input
|
|
251
|
+
value={state.formData.name}
|
|
252
|
+
$bind="formData.name"
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
<textarea
|
|
256
|
+
value={state.formData.description}
|
|
257
|
+
$bind="formData.description"
|
|
258
|
+
/>
|
|
259
|
+
|
|
260
|
+
<select
|
|
261
|
+
value={state.formData.provider}
|
|
262
|
+
$bind="formData.provider"
|
|
263
|
+
>
|
|
264
|
+
<option value="openai">OpenAI</option>
|
|
265
|
+
<option value="anthropic">Anthropic</option>
|
|
266
|
+
</select>
|
|
267
|
+
```
|
|
100
268
|
|
|
101
|
-
|
|
269
|
+
### **✅ DO: Use Regular Properties for Non-State Actions**
|
|
102
270
|
|
|
103
|
-
### **Update Function Signatures**
|
|
104
271
|
```typescript
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
272
|
+
// DOM manipulation only
|
|
273
|
+
onclick={(e) => e.stopPropagation()}
|
|
274
|
+
onmouseenter={(e) => e.target.focus()}
|
|
108
275
|
|
|
109
|
-
|
|
110
|
-
|
|
276
|
+
// Side effects (functions that call app.run() or navigate)
|
|
277
|
+
onclick={handleSideEffect} // function calls app.run() internally
|
|
278
|
+
onclick={() => window.open('/new-page')}
|
|
111
279
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
280
|
+
// Event prevention
|
|
281
|
+
onsubmit={(e) => e.preventDefault()}
|
|
115
282
|
```
|
|
116
283
|
|
|
117
|
-
###
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
284
|
+
### **❌ DON'T: Mix Patterns Incorrectly**
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
// ❌ Don't use $on with app.run() calls
|
|
288
|
+
$onclick={() => app.run('action')}
|
|
289
|
+
$onclick={(e) => this.run('action', e.target.value)}
|
|
290
|
+
|
|
291
|
+
// ❌ Don't use regular props for state updates
|
|
292
|
+
onclick="action-name" // Use $onclick for state updates
|
|
293
|
+
|
|
294
|
+
// ❌ Don't use arrow functions for simple state updates
|
|
295
|
+
$onclick={(e) => ({ ...state, field: e.target.value })}
|
|
121
296
|
|
|
122
|
-
|
|
297
|
+
// ❌ Don't use manual form handlers when $bind is available
|
|
298
|
+
$oninput={(e) => updateField('name', e)} // Use $bind="formData.name" instead
|
|
299
|
+
```
|
|
123
300
|
|
|
124
|
-
|
|
125
|
-
- Use async state function for initial data loading
|
|
126
|
-
- Async generator pattern for progressive updates during user interactions
|
|
127
|
-
- Graceful handling of loading transitions
|
|
301
|
+
## Update Function Patterns
|
|
128
302
|
|
|
303
|
+
### **Module-Level State Update Functions (Modern - Recommended)**
|
|
129
304
|
```typescript
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
305
|
+
// Export functions for direct references in $on directives
|
|
306
|
+
export const saveData = async function* (state: State): AsyncGenerator<State> {
|
|
307
|
+
yield { ...state, loading: true, error: null };
|
|
308
|
+
try {
|
|
309
|
+
await performSave(state.formData);
|
|
310
|
+
yield {
|
|
311
|
+
...state,
|
|
312
|
+
loading: false,
|
|
313
|
+
successMessage: 'Data saved successfully!'
|
|
314
|
+
};
|
|
315
|
+
// Global event for parent coordination
|
|
316
|
+
setTimeout(() => app.run('data-saved'), 2000);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
yield { ...state, loading: false, error: error.message };
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
export const deleteData = async function* (state: State): AsyncGenerator<State> {
|
|
323
|
+
yield { ...state, loading: true, error: null };
|
|
324
|
+
try {
|
|
325
|
+
await performDelete(state.formData.id);
|
|
326
|
+
yield {
|
|
327
|
+
...state,
|
|
328
|
+
loading: false,
|
|
329
|
+
successMessage: 'Data deleted successfully!'
|
|
330
|
+
};
|
|
331
|
+
setTimeout(() => app.run('data-deleted'), 2000);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
yield { ...state, loading: false, error: error.message };
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
export const closeModal = (): void => {
|
|
338
|
+
app.run('close-modal'); // Global event for parent
|
|
339
|
+
};
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### **Legacy Update Functions (Traditional Pattern)**
|
|
343
|
+
```typescript
|
|
344
|
+
// Synchronous state update
|
|
345
|
+
'action-name': (state: State, payload?: any): State => ({
|
|
346
|
+
...state,
|
|
347
|
+
// immutable updates
|
|
348
|
+
field: newValue
|
|
349
|
+
}),
|
|
350
|
+
|
|
351
|
+
// Async progressive updates
|
|
352
|
+
'async-action': async function* (state: State, payload?: any): AsyncGenerator<State> {
|
|
353
|
+
try {
|
|
354
|
+
yield { ...state, loading: true, error: null };
|
|
355
|
+
const result = await asyncOperation(payload);
|
|
356
|
+
yield { ...state, loading: false, data: result };
|
|
357
|
+
} catch (error) {
|
|
358
|
+
yield { ...state, loading: false, error: error.message };
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
// Side effect (no re-render)
|
|
363
|
+
'navigate-action': (state: State, path: string): void => {
|
|
364
|
+
window.location.href = path;
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
// Form field updates (common pattern)
|
|
368
|
+
'update-form-field': (state: State, field: string, event: Event): State => {
|
|
369
|
+
const target = event.target as HTMLInputElement;
|
|
370
|
+
const value = target.type === 'number' ? parseFloat(target.value) || 0 : target.value;
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
...state,
|
|
374
|
+
formData: {
|
|
375
|
+
...state.formData,
|
|
376
|
+
[field]: value
|
|
139
377
|
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Component Composition Patterns
|
|
383
|
+
|
|
384
|
+
### **Parent-Child with Global Events (Modern - Recommended)**
|
|
385
|
+
```typescript
|
|
386
|
+
// Parent Component (simplified state)
|
|
387
|
+
export default class WorldComponent extends Component<WorldState> {
|
|
388
|
+
view = (state: WorldState) => (
|
|
389
|
+
<div>
|
|
390
|
+
{/* Core world UI */}
|
|
391
|
+
<div className="world-content">
|
|
392
|
+
{state.agents.map(agent => (
|
|
393
|
+
<div key={agent.id} $onclick={['open-agent-edit', agent]}>
|
|
394
|
+
{agent.name}
|
|
395
|
+
</div>
|
|
396
|
+
))}
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* Conditional modal rendering */}
|
|
400
|
+
{state.showAgentEdit &&
|
|
401
|
+
<AgentEdit
|
|
402
|
+
agent={state.selectedAgent}
|
|
403
|
+
mode={state.editMode}
|
|
404
|
+
worldName={state.worldName}
|
|
405
|
+
/>
|
|
406
|
+
}
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
update = {
|
|
411
|
+
'open-agent-edit': (state, agent) => ({
|
|
412
|
+
...state,
|
|
413
|
+
showAgentEdit: true,
|
|
414
|
+
editMode: 'edit',
|
|
415
|
+
selectedAgent: agent
|
|
416
|
+
}),
|
|
417
|
+
|
|
418
|
+
'close-agent-edit': (state) => ({
|
|
419
|
+
...state,
|
|
420
|
+
showAgentEdit: false
|
|
421
|
+
}),
|
|
422
|
+
|
|
423
|
+
// Global events from child components
|
|
424
|
+
'agent-saved': async (state) => {
|
|
425
|
+
const agents = await getAgents(state.worldName);
|
|
426
|
+
return { ...state, agents, showAgentEdit: false };
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
'agent-deleted': async (state) => {
|
|
430
|
+
const agents = await getAgents(state.worldName);
|
|
431
|
+
return { ...state, agents, showAgentEdit: false };
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Self-contained child component
|
|
437
|
+
export default class AgentEdit extends Component<AgentEditState> {
|
|
438
|
+
mounted = (props) => initializeState(props);
|
|
439
|
+
|
|
440
|
+
view = (state) => (
|
|
441
|
+
<div className="modal-backdrop" $onclick={[closeModal]}>
|
|
442
|
+
<form>
|
|
443
|
+
<input $bind="formData.name" />
|
|
444
|
+
<button $onclick={[saveAgent]}>Save</button>
|
|
445
|
+
</form>
|
|
446
|
+
</div>
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### **Container + Presentation Pattern (Legacy)**
|
|
452
|
+
```typescript
|
|
453
|
+
// Container Component (manages state)
|
|
454
|
+
export default class WorldComponent extends Component<WorldState> {
|
|
455
|
+
view = (state: WorldState) => (
|
|
456
|
+
<div>
|
|
457
|
+
<WorldChat
|
|
458
|
+
messages={state.messages}
|
|
459
|
+
userInput={state.userInput}
|
|
460
|
+
onSendMessage={(text) => this.run('send-message', text)}
|
|
461
|
+
/>
|
|
462
|
+
<WorldSettings
|
|
463
|
+
world={state.world}
|
|
464
|
+
selectedAgent={state.selectedAgent}
|
|
465
|
+
onEditAgent={(agent) => this.run('edit-agent', agent)}
|
|
466
|
+
/>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Presentation Components (stateless)
|
|
472
|
+
function WorldChat(props: WorldChatProps) { /* render only */ }
|
|
473
|
+
function WorldSettings(props: WorldSettingsProps) { /* render only */ }
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## State Management Rules
|
|
477
|
+
|
|
478
|
+
### **✅ DO: Immutable Updates**
|
|
479
|
+
```typescript
|
|
480
|
+
// Spread operator for updates
|
|
481
|
+
{ ...state, field: newValue }
|
|
482
|
+
|
|
483
|
+
// Nested object updates
|
|
484
|
+
{
|
|
485
|
+
...state,
|
|
486
|
+
nested: {
|
|
487
|
+
...state.nested,
|
|
488
|
+
field: newValue
|
|
140
489
|
}
|
|
141
490
|
}
|
|
491
|
+
|
|
492
|
+
// Array updates
|
|
493
|
+
{
|
|
494
|
+
...state,
|
|
495
|
+
items: [...state.items, newItem],
|
|
496
|
+
filteredItems: state.items.filter(item => item.id !== deletedId)
|
|
497
|
+
}
|
|
142
498
|
```
|
|
143
499
|
|
|
144
|
-
|
|
500
|
+
### **✅ DO: Defensive Programming**
|
|
501
|
+
```typescript
|
|
502
|
+
// Safe array operations
|
|
503
|
+
messages: state.messages || []
|
|
504
|
+
count: (state.items || []).length
|
|
145
505
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
2. Yield error states in generators
|
|
149
|
-
3. Provide user-friendly error messages
|
|
150
|
-
4. Include retry mechanisms
|
|
151
|
-
5. Defensive programming with null checks
|
|
506
|
+
// Safe object access
|
|
507
|
+
selectedItem: state.selectedItem?.name || 'None'
|
|
152
508
|
|
|
153
|
-
|
|
509
|
+
// Default props in functional components
|
|
510
|
+
const { data = [], loading = false } = props;
|
|
511
|
+
```
|
|
154
512
|
|
|
155
|
-
###
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
513
|
+
### **❌ DON'T: Mutate State**
|
|
514
|
+
```typescript
|
|
515
|
+
// ❌ Don't mutate existing state
|
|
516
|
+
state.field = newValue;
|
|
517
|
+
state.items.push(newItem);
|
|
518
|
+
state.nested.field = value;
|
|
519
|
+
|
|
520
|
+
// ❌ Don't use non-immutable array methods
|
|
521
|
+
state.items.sort();
|
|
522
|
+
state.items.reverse();
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Error Handling Patterns
|
|
160
526
|
|
|
527
|
+
### **Component Error States**
|
|
161
528
|
```typescript
|
|
162
|
-
|
|
163
|
-
|
|
529
|
+
// State interface includes error
|
|
530
|
+
interface State {
|
|
531
|
+
data: DataType[];
|
|
532
|
+
loading: boolean;
|
|
533
|
+
error: string | null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// View handles error states
|
|
537
|
+
view = (state: State) => {
|
|
538
|
+
if (state.error) {
|
|
539
|
+
return (
|
|
540
|
+
<div className="error-state">
|
|
541
|
+
<p>Error: {state.error}</p>
|
|
542
|
+
<button $onclick="retry-action">Retry</button>
|
|
543
|
+
</div>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
// ... rest of view
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Update functions handle errors
|
|
550
|
+
'load-data': async function* (state: State): AsyncGenerator<State> {
|
|
551
|
+
try {
|
|
552
|
+
yield { ...state, loading: true, error: null };
|
|
553
|
+
const data = await fetchData();
|
|
554
|
+
yield { ...state, loading: false, data, error: null };
|
|
555
|
+
} catch (error: any) {
|
|
556
|
+
yield { ...state, loading: false, error: error.message || 'Unknown error' };
|
|
557
|
+
}
|
|
164
558
|
}
|
|
165
559
|
```
|
|
166
560
|
|
|
561
|
+
## Best Practices Summary
|
|
562
|
+
|
|
563
|
+
### **Component Design (Modern Patterns)**
|
|
564
|
+
- **Prefer stateful class components** with `mounted` lifecycle for self-contained components
|
|
565
|
+
- **Use module-level functions** for state updates to enable easy testing
|
|
566
|
+
- **Use $bind for form fields** instead of manual event handlers
|
|
567
|
+
- **Use direct function references** in $on directives: `$onclick={[saveFunction]}`
|
|
568
|
+
- **Use global events** for parent-child coordination
|
|
569
|
+
- Implement guard clauses for loading/error/empty states
|
|
570
|
+
- Follow single responsibility principle
|
|
571
|
+
|
|
572
|
+
### **Event Handling (Updated Rules)**
|
|
573
|
+
- **$bind for two-way data binding**: `$bind="formData.fieldName"`
|
|
574
|
+
- **$on directives with direct function references**: `$onclick={[functionRef]}`
|
|
575
|
+
- **String/tuple actions for legacy patterns**: `$onclick="action-name"` or `$onclick={['action', data]}`
|
|
576
|
+
- **Regular properties for DOM manipulation**: `onclick={(e) => e.stopPropagation()}`
|
|
577
|
+
- **Global events for coordination**: `app.run('component-saved')`
|
|
578
|
+
|
|
579
|
+
### **State Management (Enhanced)**
|
|
580
|
+
- **Module-level functions** return new state or use async generators
|
|
581
|
+
- **$bind automatically handles** form field updates
|
|
582
|
+
- **Global events coordinate** between parent and child components
|
|
583
|
+
- Always use immutable updates with spread operator
|
|
584
|
+
- Include loading, error, and success message states
|
|
585
|
+
- Use async generators for progressive updates
|
|
586
|
+
- Implement defensive programming with null checks
|
|
587
|
+
|
|
588
|
+
### **Architecture Patterns**
|
|
589
|
+
- **Self-contained components** manage their own form state using `mounted`
|
|
590
|
+
- **Parent components** use simple boolean flags for conditional rendering
|
|
591
|
+
- **Module-level functions** enable easy unit testing
|
|
592
|
+
- **Global events** provide loose coupling between components
|
|
593
|
+
- **Success messages** with auto-close functionality
|
|
594
|
+
- **Modal patterns** with backdrop click to close
|
|
595
|
+
|
|
596
|
+
### **Type Safety**
|
|
597
|
+
- Define comprehensive state interfaces with success message states
|
|
598
|
+
- Use proper TypeScript types for all module-level functions
|
|
599
|
+
- Type component props correctly for mounted pattern
|
|
600
|
+
- Provide default values for optional props
|
|
601
|
+
|
|
602
|
+
This guide covers modern AppRun patterns with emphasis on stateful components, module-level functions, $bind for forms, and global event coordination.
|