@base-framework/base
Version:
This is a javascript framework.
808 lines (717 loc) • 33.6 kB
Markdown
# Copilot instructions for this repo
Purpose: concise, project-specific guidance so AI agents are instantly productive in this codebase.
## Architecture (what to know first)
- **Render UI from plain JS objects** ("layouts")—no templates, no JSX. Parser turns objects into DOM (browser) or HTML strings (server).
- **Public API** re-exported by `src/base.js`: `Builder`, `Component`, `Unit`, `Jot`, `Pod`, `Atom`, `Data`, `SimpleData`, `Model`, `StateTracker`, `Import`, `NavLink`, `router`, `Html`, `Directives`, `Ajax`.
- **Runtime renderer switch**: `modules/layout/render/*` via `RenderController.setup()` → `BrowserRender` in browser, `ServerRender` for SSR.
- **Component hierarchy**: `Unit` (lifecycle/context) → `Component` (adds state via `StateTracker` and events).
- **Shorthand APIs**: `Jot` (functional components), `Pod` (stateful functional), `Atom` (reusable layouts).
- **Reactive data**: `Data` (deep proxy), `SimpleData` (shallow proxy), `Model` (server-backed). `WatcherHelper` powers `[[prop.path]]` bindings.
- **State management**: `StateTracker` for global state, component-level states via `setupStates()`.
- **Routing**: Client-side with History API, reactive `router.data.path`, automatic route parameters.
- **HTTP requests**: `Ajax` module with shorthand methods (`Ajax.get()`, `Ajax.post()`).
- **Dynamic imports**: `Import` wrapper for lazy loading with dependencies.
## CRITICAL: Common Mistakes to Avoid
1. **DON'T use templates or JSX** - Base uses plain JavaScript objects for layouts
2. **DON'T call `render()` directly** - Components call it internally; you return layout objects from `render()`
3. **DON'T mutate props** - Props are read-only; use `this.data` or `this.state` for mutable values
4. **DON'T use `this.setState()`** - Use `this.state.set('key', value)` or `this.state.increment('key')`
5. **DON'T forget `new` with Components** - Always: `new MyComponent()`, `new Component()`, never just `MyComponent()`
6. **DON'T use `new` with Atoms** - Always: `Button()`, never `new Button()`. Atoms are functions, not classes
7. **DON'T use `new` with Jot/Pod** - Call the returned class: `const MyJot = Jot({...}); new MyJot()` not `new Jot({})`
8. **DON'T mix data initialization locations** - Use `setData()` for initial setup, not `beforeSetup` or constructor
9. **DON'T bind without data** - `bind` directive requires `this.data` to be initialized via `setData()`
10. **DON'T use wrong state methods** - State keys must be defined in `setupStates()` before using `increment`, `decrement`, `toggle`
11. **DON'T forget Import function form** - Use `Import(() => import('./file.js'))` not `Import('./file.js')` for bundler support
12. **DON'T use element.remove()** - Use `Html.removeElement(element)` or `Builder.removeNode(element)` for proper cleanup
13. **DON'T access DOM before afterSetup** - `this.panel` and `this.elem` are only available after `afterSetup()` lifecycle hook
14. **DON'T return arrays from render()** - Wrap multiple elements: `return { children: [elem1, elem2] }` not `return [elem1, elem2]`
15. **DON'T use `await` in render()** - Load data in lifecycle hooks, render() must be synchronous
## Authoring layouts (house rules)
- **Shape**: `{ tag: 'div', class: 'name', children: [...] }`. Shorthands: `nest` → `children`; `text` creates text node; `html|innerHTML` sets raw HTML.
- **Default tag is 'div'** - Omit `tag` for divs: `{ class: 'container' }` renders as `<div class="container"></div>`
- **Button default** - Buttons default to `type: 'button'` (not 'submit')
- **Events on elements** receive `(event, parentComponent)` and are bound on the element: `{ click(e, parent) { parent.doThing(); } }`
- **Event names are lowercase** - Use `click`, `mouseover`, `change`, `input`, `submit`, not `onClick` or `CLICK`
- **Watchers** become `watch` directives automatically:
- **PREFERRED**: Current component data: `{ class: 'counter-[[count]]' }` (simplest, watches `this.data` or `this.state`)
- Specific data source: `{ value: ['[[path]]', data] }` (when you need different data than `this.data`)
- Multi-source with callback: `{ class: ['[[a]] [[b]]', [dataA, dataB], ([a,b]) => `${a}-${b}`] }` (advanced use)
- **Null/undefined props are ignored** - Use `{ class: condition ? 'active' : null }` to conditionally add attributes
- **Arrays in children** - Flatten automatically: `{ children: [elem1, [elem2, elem3], elem4] }` works
- **Function children** - Return layout objects: `{ children: [() => ({ tag: 'span', text: 'Dynamic' })] }`
- **Conditional rendering** - Use logical operators: `{ children: [condition && element, other || fallback] }`
## Common directives (quick cookbook)
- **bind** (two-way by default, binds to `this.data` in component):
- Text input: `{ tag: 'input', type: 'text', bind: 'form.name' }` (binds to `this.data.form.name`)
- Checkbox: `{ tag: 'input', type: 'checkbox', bind: 'form.accepted' }` (binds to boolean)
- Radio: `{ tag: 'input', type: 'radio', name: 'color', value: 'red', bind: 'form.color' }`
- Select + options: `{ tag: 'select', bind: 'form.color', children: [{ map: ['[[colors]]', data, (c) => ({ tag:'option', value:c, text:c })] }] }`
- **IMPORTANT**: `bind` requires the component to have `this.data` set via `setData()`
- Custom attribute: `{ tag: 'a', bind: 'href:link.url' }` (binds to href instead of value)
- With filter: `{ bind: ['count', (v) => Math.round(v)] }` (transform displayed value)
- One-way: `{ oneway: 'propPath' }` (element → data only)
- **map** (render lists from arrays, signature: `[watcherString, dataSource, callback]`):
- Basic: `{ tag: 'ul', children: [{ map: ['[[items]]', data, (item, i) => ({ tag:'li', text:item.name })] }] }`
- Callback receives: `(item, index)` - use both for keyed lists
- **IMPORTANT**: The callback must return a layout object, not a string
- With keys: Use `key: item.id` in returned layout for better performance
- **for** (repeat element N times):
- Basic: `{ for: [5, (i) => ({ tag: 'div', text: `Item ${i}` })] }`
- With data: `{ for: [['[[count]]', data], (i) => ({ tag: 'span', text: i })] }`
- **if** (conditional rendering):
- Basic: `{ if: [() => condition, { tag: 'div', text: 'Shown' }] }`
- With data: `{ if: [['[[isVisible]]', data], { tag: 'div', text: 'Visible' }] }`
- **NOTE**: Use regular JavaScript `condition && layout` for simpler cases
- **Watchers** (one-way binding, auto-updates when data changes):
- Simple: `{ class: 'status-[[status]]' }` (watches `this.data.status` or `this.state.status`)
- Multiple: `{ text: 'User: [[name]] Age: [[age]]' }` (watches multiple props)
- Deep paths: `{ text: '[[user.profile.name]]' }` (nested object access)
- In arrays: `{ class: ['theme-[[theme]]', 'page'] }` (combines static and dynamic)
- **Lifecycle hooks** (element-level, different from component lifecycle):
- `{ onCreated: (el, parent) => {/* el is DOM node, parent is component */} }`
- `{ onDestroyed: (el, parent) => {/* cleanup before removal */} }`
- **IMPORTANT**: These fire for each element, not once per component
- **State/Data hooks** (access parent state/data):
- `{ useData: data }` - use specific data source for watchers in this subtree
- `{ useState: state }` - use specific state source for watchers in this subtree
- `{ useContext: context }` - use specific context for this subtree
- `{ useParent: component }` - explicitly set parent component reference
- **NOTE**: These propagate to child elements
- **onSet** (reactive callback for data changes):
- Basic: `{ onSet: ['propPath', (value, oldValue) => ({ tag: 'div', text: value })] }`
- Multiple props: `{ onSet: [['prop1', 'prop2'], ([val1, val2]) => layout] }`
- With data source: `{ onSet: ['propPath', data, (value) => layout] }`
- **Component integration**:
- `{ addState: { count: 0 } }` - add state properties to component
- `{ addEvent: { myEvent: (data) => {} } }` - add event to component
- `{ addContext: { theme: 'dark' } }` - add context to component
- **Animation**:
- Enter: `{ animateEnter: 'fadeIn' }` or `{ animateEnter: { name: 'slide', duration: 300 } }`
- Exit: `{ animateExit: 'fadeOut' }`
- Move: `{ animateMove: 'slide' }`
- **Accessibility**:
- `{ a11yHide: true }` - hide from screen readers
- `{ a11yLabel: 'descriptive text' }` - set aria-label
- `{ a11yRole: 'button' }` - set role
- `{ a11yDescribe: 'longer description' }` - set aria-describedby
- **Cache/persist**:
- `cache: 'propName'` - stores DOM element in `this.propName` (use for later access)
- `persist: true` on parent preserves child component instances across re-renders
- Child can opt-out with `persist: false`
## Components and state/data
- Extend `Component` and implement `render()` - return layout object (never call `render()` manually)
- Root element auto-cached as `this.panel` - access after `afterSetup()` lifecycle hook
- Use `this.getId('child')` for stable DOM IDs across re-renders
- Helper methods available on Component instances:
- `this.if(condition, layout)` - conditional rendering helper
- `this.map(array, callback)` - array mapping helper
- `this.declareProps(schema)` - prop validation and defaults
### State Management
- Override `setupStates()` to define reactive state properties:
```javascript
setupStates() {
return {
count: 0, // Initial value
active: { state: false, callBack: (val) => {/* fires on change */} },
items: [] // Arrays work too
};
}
```
- Access via `this.state.count` or `this.state.get('count')`
- Update methods:
- `this.state.set('count', 5)` or `this.state.set({ count: 5, active: true })`
- `this.state.increment('count', amount?)` - add to number (default +1)
- `this.state.decrement('count', amount?)` - subtract from number (default -1)
- `this.state.toggle('active')` - flip boolean
- `this.state.push('items', item)` - add to array
- `this.state.splice('items', index, count)` - remove from array
- **NEVER** use `this.setState()` - that method doesn't exist
- **IMPORTANT**: State keys must be defined in `setupStates()` before using helper methods
### Global State (StateTracker)
- Create global state with `StateTracker.create(id, initialState)`:
```javascript
import { StateTracker } from '@base-framework/base';
const appState = StateTracker.create('app', { user: null, theme: 'light' });
```
- Access in components:
```javascript
setupStateTarget() {
this.state = StateTracker.get('app');
}
```
- All state methods work on global state: `this.state.set('theme', 'dark')`
- Use `useState` directive to connect elements to global state
### Data Management
- Override `setData()` to attach reactive data (runs during component initialization):
```javascript
setData() {
this.data = new Data({ name: '', items: [] });
// Optional: local storage persistence
this.data.setKey('MY_STORAGE_KEY');
this.data.resume({ name: '', items: [] }); // Load or use defaults
}
```
- **CRITICAL**: Initialize data in `setData()`, NOT in `beforeSetup()` or constructor
- Use `Data` for deep nested objects, `SimpleData` for flat objects, `Model` for server-backed data
- Access/modify: `this.data.name = 'test'` or `this.data.set('name', 'test')` or `this.data.set({ name: 'test', items: [] })`
- Components with `route`/`switch` directives automatically receive `this.route` (a bindable Data object with route params)
- Array methods: `push`, `pop`, `shift`, `unshift`, `splice` (all trigger reactivity)
- Other methods:
- `this.data.get('nested.path')` - get nested value
- `this.data.refresh('key')` - trigger watchers without changing value
- `this.data.delete('key')` - remove property
- `this.data.ifNull('key', defaultValue)` - return default if null/undefined
- `this.data.getIndex('array', predicate)` - find array index
- `this.data.concat('array', items)` - append to array
- `this.data.on('change', callback)` - subscribe to changes
- `this.data.off('change', callback)` - unsubscribe
- `this.data.store()` - save to local storage (if key set)
- `this.data.resume(defaults)` - load from local storage
- `this.data.revert()` - undo changes (Data only, not SimpleData)
- `this.data.link(otherData, 'prop')` - two-way sync with another data source
### Lifecycle Execution Order
1. `onCreated()` - component instance created, props available, NO DOM yet
2. `beforeSetup()` - before render, good for computed props
3. `setData()` - initialize reactive data (runs automatically)
4. `setupStates()` - define state properties (runs automatically)
5. `setupStateTarget()` - connect to global state (runs automatically if defined)
6. `render()` - return layout object (called automatically, NEVER call manually)
7. `afterSetup()` - DOM created but not in document, `this.panel` available
8. `afterRender()` - alias for afterSetup
9. `afterLayout()` - DOM in document, safe for measurements/animations
10. `beforeDestroy()` - cleanup before removal
11. `onDestroyed()` - final cleanup after removal
### Atoms (Reusable Layouts)
- Create with `Atom((props, children) => layoutObject)`:
```javascript
const Button = Atom((props, children) => ({
tag: 'button',
type: 'button',
...props,
children
}));
```
- Call without `new`: `Button({ class: 'primary', click: handler }, 'Click Me')`
- Support flexible argument order: `Button('text')`, `Button({ class: 'btn' })`, `Button({ class: 'btn' }, 'text')`
- **DON'T** use `new` with Atoms - they are functions, not classes
- Atoms can merge props, children, and have watchers: `Button({ class: 'btn-[[size]]' }, 'Text')`
### Jot (Functional Components)
- Create lightweight components: `const MyJot = Jot({ render() { return { tag: 'div' }; } })`
- Returns a Component class: `new MyJot()` to instantiate
- Supports all component features: `setData()`, `setupStates()`, lifecycle hooks
- Auto-wrapped for non-Component objects in `Builder.render()`
- **DON'T** use `new Jot({})` - call the returned class: `const MyJot = Jot({}); new MyJot()`
### Pod (Stateful Functional Components)
- Like Jot but with built-in state setup:
```javascript
const Counter = Pod({
states: { count: 0 },
render() {
return { tag: 'div', text: 'Count: [[count]]' };
}
});
```
- Use `states` property to define state (equivalent to `setupStates()`)
- Instantiate: `new Counter()`
### Persistence & Component Reuse
- Parent `persist: true` keeps child component instances alive during re-renders
- Child can opt-out: `persist: false`
- **WARNING**: Data initialized in `beforeSetup` can cause issues with persistence
## HTTP Requests (Ajax Module)
- **Shorthand methods** (recommended for simple requests):
```javascript
import { Ajax } from '@base-framework/base';
// GET request
Ajax.get('/api/users').then(data => console.log(data));
// POST request
Ajax.post('/api/users', { name: 'John' }).then(data => console.log(data));
// PUT request
Ajax.put('/api/users/123', { name: 'Jane' });
// DELETE request
Ajax.delete('/api/users/123');
// HEAD request
Ajax.head('/api/status');
```
- **Object syntax** (for advanced options):
```javascript
Ajax({
url: '/api/users',
method: 'POST',
data: { name: 'John' },
responseType: 'json', // 'json', 'text', 'blob', 'arraybuffer'
headers: { 'X-Custom': 'value' },
success: (data) => console.log('Success:', data),
error: (xhr) => console.error('Error:', xhr.status),
progress: (e) => console.log('Progress:', e.loaded / e.total)
});
```
- **Response types**:
- `'json'` (default) - auto-parse JSON response
- `'text'` - get response as string
- `'blob'` - for binary data (files, images)
- `'arraybuffer'` - for raw binary data
- **Global configuration**:
```javascript
// Add fixed params to all requests
Ajax.addFixedParams({ apiKey: 'abc123' });
// Pre-request hook
Ajax.beforeSend((xhr) => {
xhr.setRequestHeader('Authorization', 'Bearer token');
});
// Default settings
Ajax.ajaxSettings({
baseURL: '/api',
timeout: 5000,
withCredentials: true
});
```
- **In components**:
```javascript
class UserList extends Component {
onCreated() {
Ajax.get('/api/users').then(users => {
this.data.set('users', users);
});
}
}
```
## Dynamic Module Loading (Import)
- **Basic usage** (lazy load components/modules):
```javascript
import { Import } from '@base-framework/base';
// In layouts (function form - works with bundlers)
{ children: [Import(() => import('./components/heavy.js'))] }
// String path (for non-bundler scenarios)
{ children: [Import('./components/simple.js')] }
```
- **With dependencies** (load CSS/JS before module):
```javascript
Import({
src: () => import('./components/Chart.js'),
depends: [
'./styles/chart.css',
'./vendor/chart-lib.js'
],
callback: (module) => console.log('Chart loaded:', module)
});
```
- **Route-based lazy loading**:
```javascript
{
switch: [
{ uri: '/dashboard', component: Import(() => import('./pages/Dashboard.js')) },
{ uri: '/profile', component: Import(() => import('./pages/Profile.js')) },
{ uri: '/settings', component: Import(() => import('./pages/Settings.js')) }
]
}
```
- **Persistent modules** (keep loaded after parent destroyed):
```javascript
Import({
src: () => import('./services/Analytics.js'),
persist: true // Stays loaded globally
});
```
- **CRITICAL**: Always use function form `() => import()` for bundler support (Vite, Webpack)
- **DON'T** use string paths with bundlers: `Import('./file.js')` won't code-split
- **DO** use function form: `Import(() => import('./file.js'))` enables code-splitting
### Persistence & Component Reuse
- Parent `persist: true` keeps child component instances alive during re-renders
- Child can opt-out: `persist: false`
- **WARNING**: Data initialized in `beforeSetup` can cause issues with persistence
### Component Helper Methods (Available in all Components)
```javascript
class MyComponent extends Component {
someMethod() {
// Conditional rendering helper
this.if(this.state.show, { tag: 'div', text: 'Visible' });
// Array mapping helper
const items = this.map([1, 2, 3], (num) => ({ tag: 'span', text: num }));
// Get stable DOM ID
const id = this.getId('element-name'); // 'component-123-element-name'
// Prop validation (optional)
this.declareProps({
title: { type: 'string', required: true },
count: { type: 'number', default: 0 }
});
}
}
```
### Lifecycle Execution Order
1. `onCreated()` - component instance created, props available, NO DOM yet
2. `beforeSetup()` - before render, good for computed props
3. `setData()` - initialize reactive data (runs automatically)
4. `setupStates()` - define state properties (runs automatically)
5. `setupStateTarget()` - connect to global state (runs automatically if defined)
6. `render()` - return layout object (called automatically, NEVER call manually)
7. `afterSetup()` - DOM created but not in document, `this.panel` available
8. `afterRender()` - alias for afterSetup
9. `afterLayout()` - DOM in document, safe for measurements/animations
10. `beforeDestroy()` - cleanup before removal
11. `onDestroyed()` - final cleanup after removal
### Atoms (Reusable Layouts)
- Create with `Atom((props, children) => layoutObject)`:
```javascript
const Button = Atom((props, children) => ({
tag: 'button',
type: 'button',
...props,
children
}));
```
- Call without `new`: `Button({ class: 'primary', click: handler }, 'Click Me')`
- Support flexible argument order: `Button('text')`, `Button({ class: 'btn' })`, `Button({ class: 'btn' }, 'text')`
- **DON'T** use `new` with Atoms - they are functions, not classes
- Atoms can merge props, children, and have watchers: `Button({ class: 'btn-[[size]]' }, 'Text')`
### Jot (Functional Components)
- Create lightweight components: `const MyJot = Jot({ render() { return { tag: 'div' }; } })`
- Returns a Component class: `new MyJot()` to instantiate
- Supports all component features: `setData()`, `setupStates()`, lifecycle hooks
- Auto-wrapped for non-Component objects in `Builder.render()`
- **DON'T** use `new Jot({})` - call the returned class: `const MyJot = Jot({}); new MyJot()`
### Pod (Stateful Functional Components)
- Like Jot but with built-in state setup:
```javascript
const Counter = Pod({
states: { count: 0 },
render() {
return { tag: 'div', text: 'Count: [[count]]' };
}
});
```
- Use `states` property to define state (equivalent to `setupStates()`)
- Instantiate: `new Counter()`
- **State helpers** (only work on keys defined in `setupStates()`):
- `this.state.set('key', value)` - set any value
- `this.state.toggle('key')` - flip boolean
- `this.state.increment('key', amount?)` - add to number (default +1)
- `this.state.decrement('key', amount?)` - subtract from number (default -1)
- **DON'T** try to use these on undefined state keys
- **Data helpers** (work on `Data` and `SimpleData`):
- Get: `data.name` or `data.get('nested.path')`
- Set: `data.name = val` or `data.set('nested.path', val)` or `data.set({ key1: val1, key2: val2 })`
- Arrays: `data.push('arr', item)`, `data.splice('arr', idx, count)`, `data.unshift()`, `data.shift()`, `data.pop()`
- Refresh: `data.refresh('key')` - trigger watchers without changing value
- Revert: `data.revert()` - undo changes since last commit (Data only)
- Delete: `data.delete('key')` - remove property
- **Linking data sources** (two-way sync):
- Full link: `data.link(otherData)` - sync all properties
- Single prop: `data.link(otherData, 'propName')` - sync one property
- **Local storage persistence**:
```javascript
setData() {
this.data = new Data({ count: 0 });
this.data.setKey('MY_STORAGE_KEY');
this.data.resume({ count: 0 }); // Load or use defaults
}
// Save when needed
saveData() {
this.data.store();
}
```
## Rendering and routing
- Render anything (function/layout/Unit/Component): `Builder.render(x, container, parent?)`. Non-Unit inputs are wrapped in a `Jot` component.
- Router: `router.data.path` is reactive; navigate via `router.navigate(uri, data?, replace?)`. `NavLink` uses `[value: ['[[path]]', router.data]]` to track active path.
- Routes via directives: `route` renders all matches; `switch` renders the first match. Both can lazy-load components via `import`.
- Lazy imports: use `Import` or dynamic `import()` in route/switch children to defer loading.
- NavLink patterns: `{ tag:'a', route:'/users', children:['Users'], useData: router.data }` or use `NavLink` which watches `router.data.path` for active state.
### Router Setup (do this FIRST)
```javascript
import { router } from '@base-framework/base';
router.setup('/base-url/', 'App Title');
```
### Route Directive (renders ALL matching routes)
```javascript
{
route: [
{ uri: '/users', component: UsersList },
{ uri: '/users/:id', component: UserDetail },
{ uri: '/users/:id/edit', component: UserEdit }
]
}
// All matching routes render simultaneously
```
### Switch Directive (renders FIRST match only)
```javascript
{
switch: [
{ uri: '/login', component: Login },
{ uri: '/dashboard', component: Dashboard },
{ component: NotFound } // No uri = default/fallback route
]
}
// Only one route renders
```
### Route Patterns
- **Exact match**: `/users` - matches only `/users`
- **Wildcard**: `/users*` - matches `/users`, `/users/123`, `/users/123/edit`
- **Required param**: `/users/:id` - matches `/users/123`, extracts `id: '123'`
- **Optional param**: `/users/:id?` - matches `/users` and `/users/123`
- **Multi-params**: `/users/:id/posts/:postId?*` - combines patterns
### Accessing Route Data in Components
```javascript
class UserDetail extends Component {
render() {
// this.route is automatically injected by route/switch directives
const userId = this.route.id; // From /users/:id
return { text: `User ID: [[id]]` }; // Watches this.route.id
}
}
```
### Lazy Loading Routes
```javascript
{
switch: [
{ uri: '/heavy', import: () => import('./components/heavy.js') }
]
}
// Component loads only when route matches
```
### NavLink Component
```javascript
import { NavLink } from '@base-framework/base';
new NavLink({
href: '/users',
text: 'Users',
exact: true, // false = matches /users*
activeClass: 'active' // Class added when route matches
})
```
## Build and types
- Build to `dist/` with esbuild + TypeScript declarations: `npm run build` (bundles `src/base.js`, ESM, minified, sourcemaps; emits `dist/types/*.d.ts`).
- Consumers import from package root; `exports` maps ESM/CJS to `dist/base.js`.
## Complete Working Examples
### Example 1: Simple Counter Component (State + Watchers)
```javascript
import { Component, Atom } from '@base-framework/base';
const Button = Atom((props, children) => ({
tag: 'button',
type: 'button',
...props,
children
}));
class Counter extends Component {
setupStates() {
return {
count: 0
};
}
render() {
return {
class: 'counter',
children: [
{ tag: 'h2', text: 'Count: [[count]]' }, // Watcher on this.state
Button({
click: () => this.state.increment('count'),
}, 'Increment'),
Button({
click: () => this.state.decrement('count'),
}, 'Decrement')
]
};
}
}
// Usage
import { Builder } from '@base-framework/base';
Builder.render(new Counter(), document.body);
```
### Example 2: Form with Data Binding
```javascript
import { Component, Data } from '@base-framework/base';
class UserForm extends Component {
setData() {
this.data = new Data({
form: {
name: '',
email: '',
role: 'user',
newsletter: false
}
});
}
render() {
return {
class: 'user-form',
children: [
{
tag: 'input',
type: 'text',
placeholder: 'Name',
bind: 'form.name' // Two-way binding to this.data.form.name
},
{
tag: 'input',
type: 'email',
placeholder: 'Email',
bind: 'form.email'
},
{
tag: 'select',
bind: 'form.role',
children: [
{ tag: 'option', value: 'user', text: 'User' },
{ tag: 'option', value: 'admin', text: 'Admin' }
]
},
{
tag: 'label',
children: [
{ tag: 'input', type: 'checkbox', bind: 'form.newsletter' },
{ tag: 'span', text: 'Subscribe to newsletter' }
]
},
{
tag: 'button',
type: 'button',
text: 'Submit',
click: () => this.handleSubmit()
},
// Preview with watchers
{ tag: 'pre', text: 'Name: [[form.name]]\nEmail: [[form.email]]' }
]
};
}
handleSubmit() {
console.log('Form data:', this.data.form);
}
}
```
### Example 3: List with Map Directive
```javascript
import { Component, Data } from '@base-framework/base';
class TodoList extends Component {
setData() {
this.data = new Data({
newTodo: '',
todos: [
{ id: 1, text: 'Learn Base', done: false },
{ id: 2, text: 'Build app', done: false }
]
});
}
render() {
return {
class: 'todo-list',
children: [
{
tag: 'input',
type: 'text',
bind: 'newTodo',
placeholder: 'New todo...'
},
{
tag: 'button',
type: 'button',
text: 'Add',
click: () => this.addTodo()
},
{
tag: 'ul',
children: [{
map: ['[[todos]]', this.data, (todo, index) => ({
tag: 'li',
class: todo.done ? 'done' : '',
children: [
{
tag: 'input',
type: 'checkbox',
checked: todo.done,
change: (e) => {
this.data.todos[index].done = e.target.checked;
this.data.refresh('todos');
}
},
{ tag: 'span', text: todo.text },
{
tag: 'button',
type: 'button',
text: '×',
click: () => this.removeTodo(index)
}
]
})]
}]
}
]
};
}
addTodo() {
if (!this.data.newTodo.trim()) return;
this.data.push('todos', {
id: Date.now(),
text: this.data.newTodo,
done: false
});
this.data.newTodo = '';
}
removeTodo(index) {
this.data.splice('todos', index, 1);
}
}
```
### Example 4: Routing App
```javascript
import { Component, router, NavLink } from '@base-framework/base';
// Setup router FIRST
router.setup('/app/', 'My App');
// Page components
class HomePage extends Component {
render() {
return { class: 'home', children: [{ tag: 'h1', text: 'Home' }] };
}
}
class UserDetail extends Component {
render() {
// this.route is automatically available from router
return {
class: 'user-detail',
children: [
{ tag: 'h1', text: 'User Details' },
{ tag: 'p', text: 'User ID: [[id]]' } // Watches this.route.id
]
};
}
}
class NotFound extends Component {
render() {
return { class: 'not-found', children: [{ tag: 'h1', text: '404' }] };
}
}
// Main app with navigation
class App extends Component {
render() {
return {
class: 'app',
children: [
{
tag: 'nav',
children: [
new NavLink({ href: '/', text: 'Home', exact: true }),
new NavLink({ href: '/users', text: 'Users' }),
new NavLink({ href: '/about', text: 'About' })
]
},
{
tag: 'main',
// Use switch for mutually exclusive routes
switch: [
{ uri: '/', component: HomePage },
{ uri: '/users/:id', component: UserDetail },
{ uri: '/about', import: () => import('./about.js') },
{ component: NotFound } // Fallback route
]
}
]
};
}
}
// Render app
import { Builder } from '@base-framework/base';
Builder.render(new App(), document.body);
```
## Pointers to examples
- Rendering: `modules/layout/render/{browser-render,server-render}.js`; Parser: `modules/layout/element/parser.js` + `modules/layout/watcher-helper.js`.
- Components: `modules/component/{unit.js,component.js}`; Directives registry: `modules/layout/directives/core/default-directives.js`.
- Data: `modules/data/types/**`; Router: `modules/router/router.js`, `modules/router/nav-link.js`.
Questions or gaps? Open an issue or add comments here with file pointers so we can refine these rules.