@base-framework/base 3.0.508 → 3.0.509
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/copilot.md +481 -97
- package/package.json +1 -1
package/copilot.md
CHANGED
|
@@ -1,97 +1,481 @@
|
|
|
1
|
-
# Copilot
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
-
|
|
7
|
-
- Public API
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
- map
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
- `{
|
|
36
|
-
-
|
|
37
|
-
- `{
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
|
|
57
|
-
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
# Copilot instructions for this repo
|
|
2
|
+
|
|
3
|
+
Purpose: concise, project-specific guidance so AI agents are instantly productive in this codebase.
|
|
4
|
+
|
|
5
|
+
## Architecture (what to know first)
|
|
6
|
+
- Render UI from plain JS objects ("layouts")—no templates. Parser turns objects into DOM (browser) or HTML strings (server).
|
|
7
|
+
- Public API re-exported by `src/base.js` (e.g., `Builder`, `Component`, `Atom`, `Data`, `SimpleData`, `Model`, `Import`, `NavLink`, `router`, `Html`, `Directives`).
|
|
8
|
+
- Runtime renderer switch: `modules/layout/render/*` via `RenderController.setup()` → `BrowserRender` in browser, `ServerRender` otherwise.
|
|
9
|
+
- Component stack: `Unit` → `Component`. `Unit` handles lifecycle/context; `Component` adds state (`StateTracker`) and events.
|
|
10
|
+
- Reactive data: `modules/data` provides `Data` (deep), `SimpleData` (shallow), `Model` (server-backed). `WatcherHelper` powers `[[prop.path]]` in strings/arrays.
|
|
11
|
+
|
|
12
|
+
## CRITICAL: Common Mistakes to Avoid
|
|
13
|
+
1. **DON'T use templates or JSX** - Base uses plain JavaScript objects for layouts
|
|
14
|
+
2. **DON'T call `render()` directly** - Components call it internally; you return layout objects from `render()`
|
|
15
|
+
3. **DON'T mutate props** - Props are read-only; use `this.data` or `this.state` for mutable values
|
|
16
|
+
4. **DON'T use `this.setState()`** - Use `this.state.set('key', value)` or `this.state.increment('key')`
|
|
17
|
+
5. **DON'T forget `new` with Components** - Always: `new MyComponent()`, never just `MyComponent()`
|
|
18
|
+
6. **DON'T use `new` with Atoms** - Always: `Button()`, never `new Button()`
|
|
19
|
+
7. **DON'T mix data initialization locations** - Use `setData()` for initial setup, not `beforeSetup` or constructor
|
|
20
|
+
|
|
21
|
+
## Authoring layouts (house rules)
|
|
22
|
+
- Shape: `{ tag: 'div', class: 'name', children: [...] }`. Shorthands: `nest` → `children`; `text` creates text node; `html|innerHTML` sets raw HTML; buttons default `type: 'button'`.
|
|
23
|
+
- **Default tag is 'div'** - Omit `tag` for divs: `{ class: 'container' }` renders as `<div class="container"></div>`
|
|
24
|
+
- Events on elements receive `(event, parentComponent)` and are bound on the element: `{ click(e, parent) { parent.doThing(); } }`.
|
|
25
|
+
- **Event names are lowercase** - Use `click`, `mouseover`, `change`, not `onClick` or `CLICK`
|
|
26
|
+
- Watchers become `watch` directives automatically:
|
|
27
|
+
- **PREFERRED**: Current component data: `{ class: 'counter-[[count]]' }` (simplest, use this by default)
|
|
28
|
+
- Specific data source: `{ value: ['[[path]]', data] }` (when you need different data than `this.data`)
|
|
29
|
+
- Multi-source with callback: `{ class: ['[[a]] [[b]]', [dataA, dataB], ([a,b]) => `${a}-${b}`] }` (advanced use)
|
|
30
|
+
- Directives are just attrs mapped in `modules/layout/directives/core/default-directives.js` (e.g., `bind`, `watch`, `map`, `for`, `route`, `switch`, `useState`, `useData`, `onCreated`, `onDestroyed`).
|
|
31
|
+
- **Null/undefined props are ignored** - Use `{ class: condition ? 'active' : null }` to conditionally add attributes
|
|
32
|
+
|
|
33
|
+
## Common directives (quick cookbook)
|
|
34
|
+
- **bind** (two-way by default, binds to `this.data` in component):
|
|
35
|
+
- Text input: `{ tag: 'input', type: 'text', bind: 'form.name' }` (binds to `this.data.form.name`)
|
|
36
|
+
- Checkbox: `{ tag: 'input', type: 'checkbox', bind: 'form.accepted' }` (binds to boolean)
|
|
37
|
+
- Select + options: `{ tag: 'select', bind: 'form.color', children: [{ map: ['[[colors]]', data, (c) => ({ tag:'option', value:c, text:c })] }] }`
|
|
38
|
+
- **IMPORTANT**: `bind` requires the component to have `this.data` set via `setData()`
|
|
39
|
+
- Custom attribute: `{ tag: 'a', bind: 'href:link.url' }` (binds to href instead of value)
|
|
40
|
+
- With filter: `{ bind: ['count', (v) => Math.round(v)] }` (transform displayed value)
|
|
41
|
+
|
|
42
|
+
- **map** (render lists from arrays, signature: `[watcherString, dataSource, callback]`):
|
|
43
|
+
- Basic: `{ tag: 'ul', children: [{ map: ['[[items]]', data, (item, i) => ({ tag:'li', text:item.name })] }] }`
|
|
44
|
+
- Callback receives: `(item, index)` - use both for keyed lists
|
|
45
|
+
- **IMPORTANT**: The callback must return a layout object, not a string
|
|
46
|
+
|
|
47
|
+
- **Watchers** (one-way binding, auto-updates when data changes):
|
|
48
|
+
- Simple: `{ class: 'status-[[status]]' }` (watches `this.data.status`)
|
|
49
|
+
- Multiple: `{ text: 'User: [[name]] Age: [[age]]' }` (watches multiple props)
|
|
50
|
+
- Deep paths: `{ text: '[[user.profile.name]]' }` (nested object access)
|
|
51
|
+
|
|
52
|
+
- **Lifecycle hooks** (element-level, different from component lifecycle):
|
|
53
|
+
- `{ onCreated: (el, parent) => {/* el is DOM node, parent is component */} }`
|
|
54
|
+
- `{ onDestroyed: (el, parent) => {/* cleanup before removal */} }`
|
|
55
|
+
- **IMPORTANT**: These fire for each element, not once per component
|
|
56
|
+
|
|
57
|
+
- **Cache/persist**:
|
|
58
|
+
- `cache: 'propName'` - stores DOM element in `this.propName` (use for later access)
|
|
59
|
+
- `persist: true` on parent preserves child component instances across re-renders
|
|
60
|
+
- Child can opt-out with `persist: false`
|
|
61
|
+
|
|
62
|
+
## Components and state/data
|
|
63
|
+
- Extend `Component` and implement `render()` - return layout object (never call `render()` manually)
|
|
64
|
+
- Root element auto-cached as `this.panel` - access after `afterSetup()` lifecycle hook
|
|
65
|
+
- Use `this.getId('child')` for stable DOM IDs across re-renders
|
|
66
|
+
|
|
67
|
+
### State Management
|
|
68
|
+
- Override `setupStates()` to define reactive state properties:
|
|
69
|
+
```javascript
|
|
70
|
+
setupStates() {
|
|
71
|
+
return {
|
|
72
|
+
count: 0, // Initial value
|
|
73
|
+
active: { state: false, callBack: (val) => {/* fires on change */} }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
- Access via `this.state.count` or `this.state.get('count')`
|
|
78
|
+
- Update methods: `this.state.set('count', 5)`, `this.state.increment('count')`, `this.state.toggle('active')`
|
|
79
|
+
- **NEVER** use `this.setState()` - that method doesn't exist
|
|
80
|
+
|
|
81
|
+
### Data Management
|
|
82
|
+
- Override `setData()` to attach reactive data (runs during component initialization):
|
|
83
|
+
```javascript
|
|
84
|
+
setData() {
|
|
85
|
+
this.data = new Data({ name: '', items: [] });
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
- **CRITICAL**: Initialize data in `setData()`, NOT in `beforeSetup()` or constructor
|
|
89
|
+
- Use `Data` for deep nested objects, `SimpleData` for flat objects, `Model` for server-backed data
|
|
90
|
+
- Access/modify: `this.data.name = 'test'` or `this.data.set('name', 'test')`
|
|
91
|
+
- Components with `route`/`switch` directives automatically receive `this.route` (a bindable Data object)
|
|
92
|
+
|
|
93
|
+
### Lifecycle Execution Order
|
|
94
|
+
1. `onCreated()` - component instance created, props available, NO DOM yet
|
|
95
|
+
2. `beforeSetup()` - before render, good for computed props
|
|
96
|
+
3. `render()` - return layout object (called automatically)
|
|
97
|
+
4. `afterSetup()` - DOM created but not in document, `this.panel` available
|
|
98
|
+
5. `afterLayout()` - DOM in document, safe for measurements/animations
|
|
99
|
+
6. `beforeDestroy()` - cleanup before removal
|
|
100
|
+
|
|
101
|
+
### Persistence
|
|
102
|
+
- Parent `persist: true` keeps child component instances alive during re-renders
|
|
103
|
+
- Child can opt-out: `persist: false`
|
|
104
|
+
- **WARNING**: Data initialized in `beforeSetup` can cause issues with persistence
|
|
105
|
+
|
|
106
|
+
### Jot (Lightweight Components)
|
|
107
|
+
- Wrap objects/functions as components: `Jot({ render() { return {...}; } })`
|
|
108
|
+
- Non-Unit inputs to `Builder.render()` auto-wrapped with Jot
|
|
109
|
+
|
|
110
|
+
### Useful state/data ops
|
|
111
|
+
- **State helpers** (only work on keys defined in `setupStates()`):
|
|
112
|
+
- `this.state.set('key', value)` - set any value
|
|
113
|
+
- `this.state.toggle('key')` - flip boolean
|
|
114
|
+
- `this.state.increment('key', amount?)` - add to number (default +1)
|
|
115
|
+
- `this.state.decrement('key', amount?)` - subtract from number (default -1)
|
|
116
|
+
- **DON'T** try to use these on undefined state keys
|
|
117
|
+
|
|
118
|
+
- **Data helpers** (work on `Data` and `SimpleData`):
|
|
119
|
+
- Get: `data.name` or `data.get('nested.path')`
|
|
120
|
+
- Set: `data.name = val` or `data.set('nested.path', val)` or `data.set({ key1: val1, key2: val2 })`
|
|
121
|
+
- Arrays: `data.push('arr', item)`, `data.splice('arr', idx, count)`, `data.unshift()`, `data.shift()`, `data.pop()`
|
|
122
|
+
- Refresh: `data.refresh('key')` - trigger watchers without changing value
|
|
123
|
+
- Revert: `data.revert()` - undo changes since last commit (Data only)
|
|
124
|
+
- Delete: `data.delete('key')` - remove property
|
|
125
|
+
|
|
126
|
+
- **Linking data sources** (two-way sync):
|
|
127
|
+
- Full link: `data.link(otherData)` - sync all properties
|
|
128
|
+
- Single prop: `data.link(otherData, 'propName')` - sync one property
|
|
129
|
+
|
|
130
|
+
- **Local storage persistence**:
|
|
131
|
+
```javascript
|
|
132
|
+
setData() {
|
|
133
|
+
this.data = new Data({ count: 0 });
|
|
134
|
+
this.data.setKey('MY_STORAGE_KEY');
|
|
135
|
+
this.data.resume({ count: 0 }); // Load or use defaults
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Save when needed
|
|
139
|
+
saveData() {
|
|
140
|
+
this.data.store();
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Rendering and routing
|
|
145
|
+
- Render anything (function/layout/Unit/Component): `Builder.render(x, container, parent?)`. Non-Unit inputs are wrapped in a `Jot` component.
|
|
146
|
+
- Router: `router.data.path` is reactive; navigate via `router.navigate(uri, data?, replace?)`. `NavLink` uses `[value: ['[[path]]', router.data]]` to track active path.
|
|
147
|
+
- Routes via directives: `route` renders all matches; `switch` renders the first match. Both can lazy-load components via `import`.
|
|
148
|
+
- Lazy imports: use `Import` or dynamic `import()` in route/switch children to defer loading.
|
|
149
|
+
- NavLink patterns: `{ tag:'a', route:'/users', children:['Users'], useData: router.data }` or use `NavLink` which watches `router.data.path` for active state.
|
|
150
|
+
|
|
151
|
+
### Router Setup (do this FIRST)
|
|
152
|
+
```javascript
|
|
153
|
+
import { router } from '@base-framework/base';
|
|
154
|
+
router.setup('/base-url/', 'App Title');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Route Directive (renders ALL matching routes)
|
|
158
|
+
```javascript
|
|
159
|
+
{
|
|
160
|
+
route: [
|
|
161
|
+
{ uri: '/users', component: UsersList },
|
|
162
|
+
{ uri: '/users/:id', component: UserDetail },
|
|
163
|
+
{ uri: '/users/:id/edit', component: UserEdit }
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
// All matching routes render simultaneously
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Switch Directive (renders FIRST match only)
|
|
170
|
+
```javascript
|
|
171
|
+
{
|
|
172
|
+
switch: [
|
|
173
|
+
{ uri: '/login', component: Login },
|
|
174
|
+
{ uri: '/dashboard', component: Dashboard },
|
|
175
|
+
{ component: NotFound } // No uri = default/fallback route
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
// Only one route renders
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Route Patterns
|
|
182
|
+
- **Exact match**: `/users` - matches only `/users`
|
|
183
|
+
- **Wildcard**: `/users*` - matches `/users`, `/users/123`, `/users/123/edit`
|
|
184
|
+
- **Required param**: `/users/:id` - matches `/users/123`, extracts `id: '123'`
|
|
185
|
+
- **Optional param**: `/users/:id?` - matches `/users` and `/users/123`
|
|
186
|
+
- **Multi-params**: `/users/:id/posts/:postId?*` - combines patterns
|
|
187
|
+
|
|
188
|
+
### Accessing Route Data in Components
|
|
189
|
+
```javascript
|
|
190
|
+
class UserDetail extends Component {
|
|
191
|
+
render() {
|
|
192
|
+
// this.route is automatically injected by route/switch directives
|
|
193
|
+
const userId = this.route.id; // From /users/:id
|
|
194
|
+
return { text: `User ID: [[id]]` }; // Watches this.route.id
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Lazy Loading Routes
|
|
200
|
+
```javascript
|
|
201
|
+
{
|
|
202
|
+
switch: [
|
|
203
|
+
{ uri: '/heavy', import: () => import('./components/heavy.js') }
|
|
204
|
+
]
|
|
205
|
+
}
|
|
206
|
+
// Component loads only when route matches
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### NavLink Component
|
|
210
|
+
```javascript
|
|
211
|
+
import { NavLink } from '@base-framework/base';
|
|
212
|
+
|
|
213
|
+
new NavLink({
|
|
214
|
+
href: '/users',
|
|
215
|
+
text: 'Users',
|
|
216
|
+
exact: true, // false = matches /users*
|
|
217
|
+
activeClass: 'active' // Class added when route matches
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Build and types
|
|
222
|
+
- Build to `dist/` with esbuild + TypeScript declarations: `npm run build` (bundles `src/base.js`, ESM, minified, sourcemaps; emits `dist/types/*.d.ts`).
|
|
223
|
+
- Consumers import from package root; `exports` maps ESM/CJS to `dist/base.js`.
|
|
224
|
+
|
|
225
|
+
## Complete Working Examples
|
|
226
|
+
|
|
227
|
+
### Example 1: Simple Counter Component (State + Watchers)
|
|
228
|
+
```javascript
|
|
229
|
+
import { Component, Atom } from '@base-framework/base';
|
|
230
|
+
|
|
231
|
+
const Button = Atom((props, children) => ({
|
|
232
|
+
tag: 'button',
|
|
233
|
+
type: 'button',
|
|
234
|
+
...props,
|
|
235
|
+
children
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
class Counter extends Component {
|
|
239
|
+
setupStates() {
|
|
240
|
+
return {
|
|
241
|
+
count: 0
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
render() {
|
|
246
|
+
return {
|
|
247
|
+
class: 'counter',
|
|
248
|
+
children: [
|
|
249
|
+
{ tag: 'h2', text: 'Count: [[count]]' }, // Watcher on this.state
|
|
250
|
+
Button({
|
|
251
|
+
click: () => this.state.increment('count'),
|
|
252
|
+
}, 'Increment'),
|
|
253
|
+
Button({
|
|
254
|
+
click: () => this.state.decrement('count'),
|
|
255
|
+
}, 'Decrement')
|
|
256
|
+
]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Usage
|
|
262
|
+
import { Builder } from '@base-framework/base';
|
|
263
|
+
Builder.render(new Counter(), document.body);
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Example 2: Form with Data Binding
|
|
267
|
+
```javascript
|
|
268
|
+
import { Component, Data } from '@base-framework/base';
|
|
269
|
+
|
|
270
|
+
class UserForm extends Component {
|
|
271
|
+
setData() {
|
|
272
|
+
this.data = new Data({
|
|
273
|
+
form: {
|
|
274
|
+
name: '',
|
|
275
|
+
email: '',
|
|
276
|
+
role: 'user',
|
|
277
|
+
newsletter: false
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
render() {
|
|
283
|
+
return {
|
|
284
|
+
class: 'user-form',
|
|
285
|
+
children: [
|
|
286
|
+
{
|
|
287
|
+
tag: 'input',
|
|
288
|
+
type: 'text',
|
|
289
|
+
placeholder: 'Name',
|
|
290
|
+
bind: 'form.name' // Two-way binding to this.data.form.name
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
tag: 'input',
|
|
294
|
+
type: 'email',
|
|
295
|
+
placeholder: 'Email',
|
|
296
|
+
bind: 'form.email'
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
tag: 'select',
|
|
300
|
+
bind: 'form.role',
|
|
301
|
+
children: [
|
|
302
|
+
{ tag: 'option', value: 'user', text: 'User' },
|
|
303
|
+
{ tag: 'option', value: 'admin', text: 'Admin' }
|
|
304
|
+
]
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
tag: 'label',
|
|
308
|
+
children: [
|
|
309
|
+
{ tag: 'input', type: 'checkbox', bind: 'form.newsletter' },
|
|
310
|
+
{ tag: 'span', text: 'Subscribe to newsletter' }
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
tag: 'button',
|
|
315
|
+
type: 'button',
|
|
316
|
+
text: 'Submit',
|
|
317
|
+
click: () => this.handleSubmit()
|
|
318
|
+
},
|
|
319
|
+
// Preview with watchers
|
|
320
|
+
{ tag: 'pre', text: 'Name: [[form.name]]\nEmail: [[form.email]]' }
|
|
321
|
+
]
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
handleSubmit() {
|
|
326
|
+
console.log('Form data:', this.data.form);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Example 3: List with Map Directive
|
|
332
|
+
```javascript
|
|
333
|
+
import { Component, Data } from '@base-framework/base';
|
|
334
|
+
|
|
335
|
+
class TodoList extends Component {
|
|
336
|
+
setData() {
|
|
337
|
+
this.data = new Data({
|
|
338
|
+
newTodo: '',
|
|
339
|
+
todos: [
|
|
340
|
+
{ id: 1, text: 'Learn Base', done: false },
|
|
341
|
+
{ id: 2, text: 'Build app', done: false }
|
|
342
|
+
]
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
render() {
|
|
347
|
+
return {
|
|
348
|
+
class: 'todo-list',
|
|
349
|
+
children: [
|
|
350
|
+
{
|
|
351
|
+
tag: 'input',
|
|
352
|
+
type: 'text',
|
|
353
|
+
bind: 'newTodo',
|
|
354
|
+
placeholder: 'New todo...'
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
tag: 'button',
|
|
358
|
+
type: 'button',
|
|
359
|
+
text: 'Add',
|
|
360
|
+
click: () => this.addTodo()
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
tag: 'ul',
|
|
364
|
+
children: [{
|
|
365
|
+
map: ['[[todos]]', this.data, (todo, index) => ({
|
|
366
|
+
tag: 'li',
|
|
367
|
+
class: todo.done ? 'done' : '',
|
|
368
|
+
children: [
|
|
369
|
+
{
|
|
370
|
+
tag: 'input',
|
|
371
|
+
type: 'checkbox',
|
|
372
|
+
checked: todo.done,
|
|
373
|
+
change: (e) => {
|
|
374
|
+
this.data.todos[index].done = e.target.checked;
|
|
375
|
+
this.data.refresh('todos');
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{ tag: 'span', text: todo.text },
|
|
379
|
+
{
|
|
380
|
+
tag: 'button',
|
|
381
|
+
type: 'button',
|
|
382
|
+
text: '×',
|
|
383
|
+
click: () => this.removeTodo(index)
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
})]
|
|
387
|
+
}]
|
|
388
|
+
}
|
|
389
|
+
]
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
addTodo() {
|
|
394
|
+
if (!this.data.newTodo.trim()) return;
|
|
395
|
+
this.data.push('todos', {
|
|
396
|
+
id: Date.now(),
|
|
397
|
+
text: this.data.newTodo,
|
|
398
|
+
done: false
|
|
399
|
+
});
|
|
400
|
+
this.data.newTodo = '';
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
removeTodo(index) {
|
|
404
|
+
this.data.splice('todos', index, 1);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Example 4: Routing App
|
|
410
|
+
```javascript
|
|
411
|
+
import { Component, router, NavLink } from '@base-framework/base';
|
|
412
|
+
|
|
413
|
+
// Setup router FIRST
|
|
414
|
+
router.setup('/app/', 'My App');
|
|
415
|
+
|
|
416
|
+
// Page components
|
|
417
|
+
class HomePage extends Component {
|
|
418
|
+
render() {
|
|
419
|
+
return { class: 'home', children: [{ tag: 'h1', text: 'Home' }] };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
class UserDetail extends Component {
|
|
424
|
+
render() {
|
|
425
|
+
// this.route is automatically available from router
|
|
426
|
+
return {
|
|
427
|
+
class: 'user-detail',
|
|
428
|
+
children: [
|
|
429
|
+
{ tag: 'h1', text: 'User Details' },
|
|
430
|
+
{ tag: 'p', text: 'User ID: [[id]]' } // Watches this.route.id
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class NotFound extends Component {
|
|
437
|
+
render() {
|
|
438
|
+
return { class: 'not-found', children: [{ tag: 'h1', text: '404' }] };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Main app with navigation
|
|
443
|
+
class App extends Component {
|
|
444
|
+
render() {
|
|
445
|
+
return {
|
|
446
|
+
class: 'app',
|
|
447
|
+
children: [
|
|
448
|
+
{
|
|
449
|
+
tag: 'nav',
|
|
450
|
+
children: [
|
|
451
|
+
new NavLink({ href: '/', text: 'Home', exact: true }),
|
|
452
|
+
new NavLink({ href: '/users', text: 'Users' }),
|
|
453
|
+
new NavLink({ href: '/about', text: 'About' })
|
|
454
|
+
]
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
tag: 'main',
|
|
458
|
+
// Use switch for mutually exclusive routes
|
|
459
|
+
switch: [
|
|
460
|
+
{ uri: '/', component: HomePage },
|
|
461
|
+
{ uri: '/users/:id', component: UserDetail },
|
|
462
|
+
{ uri: '/about', import: () => import('./about.js') },
|
|
463
|
+
{ component: NotFound } // Fallback route
|
|
464
|
+
]
|
|
465
|
+
}
|
|
466
|
+
]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Render app
|
|
472
|
+
import { Builder } from '@base-framework/base';
|
|
473
|
+
Builder.render(new App(), document.body);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Pointers to examples
|
|
477
|
+
- Rendering: `modules/layout/render/{browser-render,server-render}.js`; Parser: `modules/layout/element/parser.js` + `modules/layout/watcher-helper.js`.
|
|
478
|
+
- Components: `modules/component/{unit.js,component.js}`; Directives registry: `modules/layout/directives/core/default-directives.js`.
|
|
479
|
+
- Data: `modules/data/types/**`; Router: `modules/router/router.js`, `modules/router/nav-link.js`.
|
|
480
|
+
|
|
481
|
+
Questions or gaps? Open an issue or add comments here with file pointers so we can refine these rules.
|