native-document 1.0.166 → 1.0.168
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/.vitepress/config.js +166 -0
- package/CHANGELOG.md +153 -0
- package/components.js +2 -1
- package/dist/native-document.components.min.js +495 -228
- package/dist/native-document.dev.js +7 -0
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +213 -608
- package/docs/anchor.md +173 -312
- package/docs/cache.md +95 -803
- package/docs/cli.md +179 -0
- package/docs/components/accordion.md +172 -0
- package/docs/components/alert.md +99 -0
- package/docs/components/avatar.md +160 -0
- package/docs/components/badge.md +102 -0
- package/docs/components/breadcrumb.md +89 -0
- package/docs/components/button.md +183 -0
- package/docs/components/card.md +69 -0
- package/docs/components/context-menu.md +118 -0
- package/docs/components/data-table.md +345 -0
- package/docs/components/dropdown.md +214 -0
- package/docs/components/form/autocomplete-field.md +81 -0
- package/docs/components/form/checkbox-field.md +41 -0
- package/docs/components/form/checkbox-group-field.md +54 -0
- package/docs/components/form/color-field.md +64 -0
- package/docs/components/form/date-field.md +92 -0
- package/docs/components/form/field-collection.md +63 -0
- package/docs/components/form/file-field.md +203 -0
- package/docs/components/form/form-control.md +87 -0
- package/docs/components/form/image-field.md +90 -0
- package/docs/components/form/index.md +115 -0
- package/docs/components/form/number-field.md +65 -0
- package/docs/components/form/radio-field.md +51 -0
- package/docs/components/form/select-field.md +123 -0
- package/docs/components/form/slider.md +136 -0
- package/docs/components/form/string-field.md +134 -0
- package/docs/components/form/textarea-field.md +65 -0
- package/docs/components/form-fields.md +372 -0
- package/docs/components/getting-started.md +264 -0
- package/docs/components/index.md +337 -0
- package/docs/components/layout.md +279 -0
- package/docs/components/list.md +73 -0
- package/docs/components/menu.md +215 -0
- package/docs/components/modal.md +156 -0
- package/docs/components/pagination.md +95 -0
- package/docs/components/popover.md +131 -0
- package/docs/components/progress.md +111 -0
- package/docs/components/shortcut-manager.md +221 -0
- package/docs/components/simple-table.md +107 -0
- package/docs/components/skeleton.md +155 -0
- package/docs/components/spinner.md +100 -0
- package/docs/components/splitter.md +133 -0
- package/docs/components/stepper.md +163 -0
- package/docs/components/switch.md +113 -0
- package/docs/components/tabs.md +153 -0
- package/docs/components/toast.md +119 -0
- package/docs/components/tooltip.md +151 -0
- package/docs/components/traits.md +261 -0
- package/docs/conditional-rendering.md +170 -588
- package/docs/contributing.md +300 -25
- package/docs/core-concepts.md +205 -374
- package/docs/elements.md +251 -367
- package/docs/extending-native-document-element.md +192 -207
- package/docs/filters.md +153 -1122
- package/docs/getting-started.md +193 -267
- package/docs/i18n.md +241 -0
- package/docs/index.md +76 -0
- package/docs/lifecycle-events.md +143 -75
- package/docs/list-rendering.md +227 -852
- package/docs/memory-management.md +134 -47
- package/docs/native-document-element.md +337 -186
- package/docs/native-fetch.md +99 -630
- package/docs/observable-resource.md +364 -0
- package/docs/observables.md +592 -526
- package/docs/routing.md +244 -653
- package/docs/state-management.md +134 -241
- package/docs/svg-elements.md +231 -0
- package/docs/theming.md +409 -0
- package/docs/tutorials/.gitkeep +0 -0
- package/docs/validation.md +95 -97
- package/docs/vitepress-conventions.md +219 -0
- package/package.json +34 -13
- package/readme.md +269 -89
- package/src/components/card/Card.js +93 -39
- package/src/components/card/index.js +1 -1
- package/src/components/list/HasListItem.js +171 -0
- package/src/components/list/List.js +41 -107
- package/src/components/list/ListDivider.js +39 -0
- package/src/components/list/ListGroup.js +76 -59
- package/src/components/list/ListItem.js +117 -69
- package/src/components/list/index.js +3 -1
- package/src/components/list/types/ListItem.d.ts +45 -34
- package/src/components/spacer/Spacer.js +1 -1
- package/src/core/data/ObservableResource.js +5 -0
- package/src/core/data/observable-helpers/observable.prototypes.js +2 -0
- package/src/ui/components/card/CardRender.js +133 -0
- package/src/ui/components/card/card.css +169 -0
- package/src/ui/components/contextmenu/ContextmenuRender.js +1 -1
- package/src/ui/components/list/ListRender.js +18 -0
- package/src/ui/components/list/divider/ListDividerRender.js +10 -0
- package/src/ui/components/list/divider/list-divider.css +12 -0
- package/src/ui/components/list/group/ListGroupRender.js +61 -0
- package/src/ui/components/list/group/list-group.css +62 -0
- package/src/ui/components/list/item/ListItemRender.js +238 -0
- package/src/ui/components/list/item/list-item.css +191 -0
- package/src/ui/components/list/list.css +24 -0
- package/src/ui/components/spacer/SpacerRender.js +10 -0
- package/src/ui/index.js +8 -0
package/docs/core-concepts.md
CHANGED
|
@@ -1,524 +1,355 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Core Concepts
|
|
3
|
+
description: The fundamental concepts and philosophy behind NativeDocument - observables, elements, reactivity, and component patterns
|
|
4
|
+
---
|
|
5
|
+
|
|
1
6
|
# Core Concepts
|
|
2
7
|
|
|
3
|
-
This guide covers the fundamental concepts and philosophy behind NativeDocument.
|
|
8
|
+
This guide covers the fundamental concepts and philosophy behind NativeDocument.
|
|
9
|
+
|
|
10
|
+
---
|
|
4
11
|
|
|
5
12
|
## Philosophy
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
### Native-First
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
Unlike frameworks that abstract away the DOM, NativeDocument embraces it. Every element you create is a real DOM node, and every interaction happens through native browser APIs. This means:
|
|
16
|
+
NativeDocument embraces the DOM rather than abstracting it away. Every element you create is a real DOM node, and every interaction uses native browser APIs:
|
|
11
17
|
|
|
12
18
|
- No virtual DOM overhead
|
|
13
|
-
- Direct access to all browser
|
|
14
|
-
- Familiar debugging
|
|
19
|
+
- Direct access to all browser APIs
|
|
20
|
+
- Familiar debugging in DevTools
|
|
15
21
|
- Better performance for DOM-heavy applications
|
|
16
22
|
|
|
17
23
|
### Reactive by Design
|
|
18
|
-
Reactivity is built into the core of NativeDocument through observables. When data changes, the UI updates automatically without manual DOM manipulation:
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
const { Div } = NativeDocument.elements;
|
|
22
|
-
const { Observable } = NativeDocument;
|
|
25
|
+
Reactivity is built into the core through observables. When data changes, the UI updates automatically:
|
|
23
26
|
|
|
27
|
+
```javascript
|
|
24
28
|
const message = Observable('Hello World');
|
|
25
29
|
const display = Div(message);
|
|
26
30
|
|
|
27
|
-
// UI updates automatically
|
|
28
|
-
message.set('Hello NativeDocument!');
|
|
31
|
+
message.set('Hello NativeDocument!'); // UI updates automatically
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
### Zero Build Requirement
|
|
32
|
-
|
|
35
|
+
|
|
36
|
+
NativeDocument works without a build step - load from CDN and start immediately:
|
|
33
37
|
|
|
34
38
|
```html
|
|
35
39
|
<script src="https://cdn.jsdelivr.net/gh/afrocodeur/native-document@latest/dist/native-document.min.js"></script>
|
|
36
40
|
<script>
|
|
37
|
-
|
|
38
|
-
const {
|
|
39
|
-
// Your app here
|
|
41
|
+
const { Div, Button } = NativeDocument.elements;
|
|
42
|
+
const { Observable } = NativeDocument;
|
|
40
43
|
</script>
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
For production, use the CLI for optimal bundle size via tree-shaking:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
nd create MyApp
|
|
50
|
+
```
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
---
|
|
46
53
|
|
|
47
|
-
Observables
|
|
54
|
+
## Observables
|
|
55
|
+
|
|
56
|
+
Observables wrap values and notify the UI when they change. They are the reactive foundation of NativeDocument.
|
|
48
57
|
|
|
49
|
-
#### Basic Observable
|
|
50
58
|
```javascript
|
|
59
|
+
import { Observable } from 'native-document';
|
|
60
|
+
|
|
51
61
|
const count = Observable(0);
|
|
52
62
|
|
|
53
|
-
//
|
|
54
|
-
count
|
|
55
|
-
|
|
56
|
-
});
|
|
63
|
+
count.set(5); // triggers update
|
|
64
|
+
count.$value = 5; // same thing
|
|
65
|
+
count.set(v => v + 1); // function form
|
|
57
66
|
|
|
58
|
-
//
|
|
59
|
-
count
|
|
67
|
+
count.val(); // read current value
|
|
68
|
+
count.$value; // same
|
|
60
69
|
```
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
### Object observables
|
|
72
|
+
|
|
63
73
|
```javascript
|
|
64
74
|
const user = Observable({ name: 'John', age: 25 });
|
|
65
75
|
|
|
66
|
-
// Access values
|
|
67
|
-
console.log(user.val().name); // "John"
|
|
68
|
-
console.log(user.$value.name); // "John" (proxy syntax)
|
|
69
|
-
|
|
70
|
-
// Update object - replaces entire value
|
|
71
76
|
user.set({ ...user.val(), name: 'Jane' });
|
|
77
|
+
user.set(data => ({ ...data, name: 'Jane' }));
|
|
72
78
|
|
|
73
|
-
//
|
|
74
|
-
// user.name = 'Jane'; // Wrong!
|
|
79
|
+
// user.name = 'Jane' -- wrong, won't trigger update
|
|
75
80
|
```
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
For per-property reactivity:
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
const user = Observable.object({ name: 'John', age: 25 });
|
|
86
|
+
user.name.set('Jane'); // only name updates
|
|
87
|
+
user.$value; // { name: 'Jane', age: 25 }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Array observables
|
|
91
|
+
|
|
78
92
|
```javascript
|
|
79
93
|
const todos = Observable.array([]);
|
|
80
94
|
|
|
81
|
-
|
|
82
|
-
todos.
|
|
83
|
-
todos.pop();
|
|
95
|
+
todos.push({ id: 1, text: 'Buy milk', done: false });
|
|
96
|
+
todos.splice(0, 1);
|
|
84
97
|
todos.sort((a, b) => a.text.localeCompare(b.text));
|
|
85
|
-
|
|
86
|
-
// Access like normal array
|
|
87
|
-
console.log(todos.val().length);
|
|
88
98
|
```
|
|
89
99
|
|
|
90
|
-
|
|
100
|
+
### Computed observables
|
|
101
|
+
|
|
102
|
+
The callback receives dependency values as arguments in order:
|
|
103
|
+
|
|
91
104
|
```javascript
|
|
92
105
|
const firstName = Observable('John');
|
|
93
|
-
const lastName
|
|
106
|
+
const lastName = Observable('Doe');
|
|
94
107
|
|
|
95
|
-
const fullName = Observable.computed(() => {
|
|
96
|
-
return `${
|
|
108
|
+
const fullName = Observable.computed((first, last) => {
|
|
109
|
+
return `${first} ${last}`;
|
|
97
110
|
}, [firstName, lastName]);
|
|
98
111
|
|
|
99
|
-
//
|
|
100
|
-
firstName.set('Jane'); // fullName becomes "Jane Doe"
|
|
112
|
+
firstName.set('Jane'); // fullName -> "Jane Doe"
|
|
101
113
|
```
|
|
102
114
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Elements in NativeDocument are functions that create and return DOM nodes. They follow a consistent pattern:
|
|
106
|
-
|
|
107
|
-
#### Basic Element Creation
|
|
108
|
-
```javascript
|
|
109
|
-
const { Div, Button, Input } = NativeDocument.elements;
|
|
110
|
-
|
|
111
|
-
// Element with no attributes or children
|
|
112
|
-
const simpleDiv = Div();
|
|
115
|
+
See [Observables](./observables.md) for the full reference.
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
const styledDiv = Div({ class: 'container', id: 'main' });
|
|
117
|
+
---
|
|
116
118
|
|
|
117
|
-
|
|
118
|
-
const textDiv = Div('Hello World');
|
|
119
|
-
const arrayDiv = Div(['Hello ', 'World']);
|
|
119
|
+
## Elements
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
const fullDiv = Div({ class: 'card' }, [
|
|
123
|
-
'Content here'
|
|
124
|
-
]);
|
|
125
|
-
```
|
|
121
|
+
Elements are functions that create and return real DOM nodes:
|
|
126
122
|
|
|
127
|
-
#### Reactive Attributes
|
|
128
123
|
```javascript
|
|
129
|
-
|
|
130
|
-
const theme = Observable('dark');
|
|
131
|
-
|
|
132
|
-
const element = Div({
|
|
133
|
-
class: {
|
|
134
|
-
'visible': isVisible,
|
|
135
|
-
'dark-theme': theme.check(t => t === 'dark')
|
|
136
|
-
},
|
|
137
|
-
style: {
|
|
138
|
-
opacity: isVisible.check(v => v ? 1 : 0.5)
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
```
|
|
124
|
+
import { Div, Button, Input, H1, P } from 'native-document/elements';
|
|
142
125
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const counter = Observable(0);
|
|
126
|
+
// No attributes
|
|
127
|
+
const simple = Div('Hello World');
|
|
146
128
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
counter.set(counter.val() + 1);
|
|
150
|
-
});
|
|
129
|
+
// With attributes
|
|
130
|
+
const styled = Div({ class: 'card', id: 'main' }, 'Content');
|
|
151
131
|
|
|
152
|
-
//
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
132
|
+
// With reactive attributes
|
|
133
|
+
const isVisible = Observable(true);
|
|
134
|
+
const box = Div({
|
|
135
|
+
class: { 'hidden': isVisible.isFalsy() },
|
|
136
|
+
style: { opacity: isVisible.format(v => v ? 1 : 0.5) }
|
|
137
|
+
}, 'Content');
|
|
138
|
+
|
|
139
|
+
// Children - text, numbers, observables, elements, closures, or arrays
|
|
140
|
+
const mixed = Div([
|
|
141
|
+
H1('Title'),
|
|
142
|
+
'Some text',
|
|
143
|
+
P(count),
|
|
144
|
+
() => Button('Dynamic')
|
|
145
|
+
]);
|
|
165
146
|
```
|
|
166
147
|
|
|
167
|
-
###
|
|
168
|
-
|
|
169
|
-
NativeDocument includes automatic memory management to prevent memory leaks.
|
|
148
|
+
### Event handling
|
|
170
149
|
|
|
171
|
-
#### Element Lifecycle
|
|
172
150
|
```javascript
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
.nd
|
|
178
|
-
|
|
151
|
+
Button('Click me').nd.onClick(() => console.log('Clicked'));
|
|
152
|
+
|
|
153
|
+
// Multi-line callback
|
|
154
|
+
Button('Submit')
|
|
155
|
+
.nd
|
|
156
|
+
.onClick(e => {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
submitForm();
|
|
179
159
|
});
|
|
180
|
-
```
|
|
181
160
|
|
|
182
|
-
|
|
183
|
-
|
|
161
|
+
// Multiple events - chain after first .nd
|
|
162
|
+
Input({ type: 'text' })
|
|
163
|
+
.nd
|
|
164
|
+
.onFocus(() => console.log('Focused'))
|
|
165
|
+
.onBlur(() => console.log('Blurred'))
|
|
166
|
+
.onInput(e => console.log('Value:', e.target.value));
|
|
167
|
+
```
|
|
184
168
|
|
|
185
|
-
|
|
186
|
-
const observable = Observable(0);
|
|
169
|
+
See [Elements](./elements.md) for the full reference.
|
|
187
170
|
|
|
188
|
-
|
|
189
|
-
const unsubscribe = observable.subscribe(value => {
|
|
190
|
-
console.log('Value:', value);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// Clean up when needed
|
|
194
|
-
unsubscribe();
|
|
195
|
-
```
|
|
171
|
+
---
|
|
196
172
|
|
|
197
173
|
## Reactivity Model
|
|
198
174
|
|
|
199
|
-
|
|
200
|
-
NativeDocument follows a unidirectional data flow:
|
|
175
|
+
Data flows in one direction:
|
|
201
176
|
|
|
202
|
-
1. **State
|
|
203
|
-
2. **Notification
|
|
204
|
-
3. **DOM
|
|
205
|
-
4. **Event
|
|
177
|
+
1. **State change** - an observable is updated
|
|
178
|
+
2. **Notification** - all subscribers are notified
|
|
179
|
+
3. **DOM update** - UI elements update automatically
|
|
180
|
+
4. **Event handling** - user interactions trigger new state changes
|
|
206
181
|
|
|
207
182
|
```javascript
|
|
208
183
|
const items = Observable.array(['Apple', 'Banana']);
|
|
209
184
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
185
|
+
Ul(
|
|
186
|
+
ForEach(items, item =>
|
|
187
|
+
Li([
|
|
188
|
+
item,
|
|
189
|
+
Button('Remove')
|
|
190
|
+
.nd.onClick(() => items.removeItem(item))
|
|
191
|
+
])
|
|
192
|
+
)
|
|
193
|
+
)
|
|
220
194
|
```
|
|
221
195
|
|
|
222
|
-
### Reactive
|
|
223
|
-
Observables can depend on other observables, creating reactive chains:
|
|
196
|
+
### Reactive chains
|
|
224
197
|
|
|
225
198
|
```javascript
|
|
226
|
-
const price
|
|
199
|
+
const price = Observable(100);
|
|
227
200
|
const quantity = Observable(2);
|
|
228
|
-
const
|
|
201
|
+
const tax = Observable(0.2);
|
|
229
202
|
|
|
230
|
-
const subtotal = Observable.computed(() =>
|
|
231
|
-
|
|
232
|
-
}, [price, quantity]);
|
|
203
|
+
const subtotal = Observable.computed((p, q) => p * q, [price, quantity]);
|
|
204
|
+
const total = Observable.computed((sub, t) => sub * (1 + t), [subtotal, tax]);
|
|
233
205
|
|
|
234
|
-
|
|
235
|
-
return subtotal.val() * (1 - discount.val());
|
|
236
|
-
}, [subtotal, discount]);
|
|
237
|
-
|
|
238
|
-
// Changing any value updates the chain
|
|
239
|
-
price.set(150); // subtotal and total update automatically
|
|
206
|
+
price.set(150); // subtotal and total both update
|
|
240
207
|
```
|
|
241
208
|
|
|
209
|
+
---
|
|
210
|
+
|
|
242
211
|
## Component Patterns
|
|
243
212
|
|
|
244
|
-
### Functional
|
|
245
|
-
Create reusable components as functions:
|
|
213
|
+
### Functional components
|
|
246
214
|
|
|
247
215
|
```javascript
|
|
248
|
-
function UserCard(
|
|
216
|
+
function UserCard({ name, email }) {
|
|
249
217
|
return Div({ class: 'user-card' }, [
|
|
250
|
-
Div({ class: 'name' },
|
|
251
|
-
Div({ class: 'email' },
|
|
252
|
-
Button('Edit').nd.onClick(() =>
|
|
253
|
-
// Handle edit
|
|
254
|
-
})
|
|
218
|
+
Div({ class: 'name' }, name),
|
|
219
|
+
Div({ class: 'email' }, email),
|
|
220
|
+
Button('Edit').nd.onClick(() => editUser(name))
|
|
255
221
|
]);
|
|
256
222
|
}
|
|
257
223
|
|
|
258
|
-
|
|
259
|
-
const user = { name: 'John Doe', email: 'john@example.com' };
|
|
260
|
-
const card = UserCard(user);
|
|
224
|
+
document.body.appendChild(UserCard({ name: 'John', email: 'john@example.com' }));
|
|
261
225
|
```
|
|
262
226
|
|
|
263
|
-
### Stateful
|
|
264
|
-
Components with internal state:
|
|
227
|
+
### Stateful components
|
|
265
228
|
|
|
266
229
|
```javascript
|
|
267
230
|
function Counter(initialValue = 0) {
|
|
268
231
|
const count = Observable(initialValue);
|
|
269
|
-
|
|
232
|
+
|
|
270
233
|
return Div({ class: 'counter' }, [
|
|
271
234
|
Div(['Count: ', count]),
|
|
272
|
-
Button('-').nd.onClick(() => count
|
|
273
|
-
Button('+').nd.onClick(() => count
|
|
235
|
+
Button('-').nd.onClick(() => count.$value--),
|
|
236
|
+
Button('+').nd.onClick(() => count.$value++),
|
|
274
237
|
Button('Reset').nd.onClick(() => count.set(initialValue))
|
|
275
238
|
]);
|
|
276
239
|
}
|
|
277
|
-
|
|
278
|
-
// Usage
|
|
279
|
-
const myCounter = Counter(10);
|
|
280
240
|
```
|
|
281
241
|
|
|
282
|
-
###
|
|
283
|
-
Components that enhance other components:
|
|
242
|
+
### Exposing methods to parent via `.nd.with()`
|
|
284
243
|
|
|
285
244
|
```javascript
|
|
286
|
-
function
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
245
|
+
function Timer() {
|
|
246
|
+
const count = Observable(0);
|
|
247
|
+
let interval = null;
|
|
248
|
+
|
|
249
|
+
return Div(['Time: ', count])
|
|
250
|
+
.nd.with({
|
|
251
|
+
start() {
|
|
252
|
+
if (interval) return this;
|
|
253
|
+
interval = setInterval(() => count.$value++, 1000);
|
|
254
|
+
return this;
|
|
255
|
+
},
|
|
256
|
+
stop() {
|
|
257
|
+
clearInterval(interval);
|
|
258
|
+
interval = null;
|
|
259
|
+
return this;
|
|
260
|
+
},
|
|
261
|
+
reset() {
|
|
262
|
+
this.stop();
|
|
263
|
+
count.set(0);
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
296
267
|
}
|
|
297
268
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
269
|
+
const refs = {};
|
|
270
|
+
Div([
|
|
271
|
+
Timer().nd.refSelf(refs, 'timer'),
|
|
272
|
+
Button('Start').nd.onClick(() => refs.timer.start()),
|
|
273
|
+
Button('Stop').nd.onClick(() => refs.timer.stop()),
|
|
274
|
+
Button('Reset').nd.onClick(() => refs.timer.reset())
|
|
275
|
+
]);
|
|
302
276
|
```
|
|
303
277
|
|
|
278
|
+
---
|
|
279
|
+
|
|
304
280
|
## State Management Patterns
|
|
305
281
|
|
|
306
|
-
### Local
|
|
307
|
-
|
|
282
|
+
### Local state
|
|
283
|
+
|
|
284
|
+
Use observables directly for component-specific state:
|
|
308
285
|
|
|
309
286
|
```javascript
|
|
310
|
-
function
|
|
311
|
-
const
|
|
312
|
-
const isValid =
|
|
313
|
-
|
|
287
|
+
function SearchBox(onSearch) {
|
|
288
|
+
const query = Observable('');
|
|
289
|
+
const isValid = query.isNotEmpty();
|
|
290
|
+
|
|
314
291
|
return Div([
|
|
315
|
-
Input({ placeholder: '
|
|
316
|
-
Button('
|
|
292
|
+
Input({ placeholder: 'Search...', value: query }),
|
|
293
|
+
Button('Search')
|
|
317
294
|
.nd.onClick(() => {
|
|
318
295
|
if (isValid.val()) {
|
|
319
|
-
|
|
320
|
-
text.set('');
|
|
296
|
+
onSearch(query.val());
|
|
321
297
|
}
|
|
322
298
|
})
|
|
323
299
|
]);
|
|
324
300
|
}
|
|
325
301
|
```
|
|
326
302
|
|
|
327
|
-
###
|
|
328
|
-
For state shared between components, create a store:
|
|
303
|
+
### Global state with Store
|
|
329
304
|
|
|
330
305
|
```javascript
|
|
331
|
-
|
|
332
|
-
const TodoStore = {
|
|
333
|
-
todos: Observable.array([]),
|
|
334
|
-
|
|
335
|
-
addTodo(text) {
|
|
336
|
-
this.todos.push({
|
|
337
|
-
id: Date.now(),
|
|
338
|
-
text: text,
|
|
339
|
-
done: false
|
|
340
|
-
});
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
removeTodo(id) {
|
|
344
|
-
const index = this.todos.val().findIndex(todo => todo.id === id);
|
|
345
|
-
if (index !== -1) {
|
|
346
|
-
this.todos.splice(index, 1);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Use in components
|
|
352
|
-
function TodoList() {
|
|
353
|
-
return ForEach(TodoStore.todos, (todo) =>
|
|
354
|
-
Div([
|
|
355
|
-
todo.text,
|
|
356
|
-
Button('Delete').nd.onClick(() => {
|
|
357
|
-
TodoStore.removeTodo(todo.id);
|
|
358
|
-
})
|
|
359
|
-
])
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
### Global State with Store
|
|
365
|
-
For complex applications, use the built-in Store:
|
|
306
|
+
import { Store } from 'native-document';
|
|
366
307
|
|
|
367
|
-
|
|
368
|
-
const { Store } = NativeDocument;
|
|
308
|
+
Store.create('theme', 'light');
|
|
369
309
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
310
|
+
const UserStore = Store.group('user', g => {
|
|
311
|
+
g.createResettable('session', { id: null, name: '', isLoggedIn: false });
|
|
312
|
+
});
|
|
373
313
|
|
|
374
|
-
// Use in components
|
|
375
314
|
function Header() {
|
|
376
|
-
const
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
return Div({ class: theme.
|
|
380
|
-
ShowIf(
|
|
381
|
-
Div(['Welcome, ',
|
|
315
|
+
const theme = Store.use('theme');
|
|
316
|
+
const session = UserStore.use('session');
|
|
317
|
+
|
|
318
|
+
return Div({ class: theme.format(t => `theme-${t}`) }, [
|
|
319
|
+
ShowIf(session.is(s => s.isLoggedIn),
|
|
320
|
+
() => Div(['Welcome, ', session.select(s => s.name)])
|
|
382
321
|
)
|
|
383
322
|
]);
|
|
384
323
|
}
|
|
385
324
|
```
|
|
386
325
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
### Graceful Error Handling
|
|
390
|
-
Wrap potentially failing operations:
|
|
326
|
+
---
|
|
391
327
|
|
|
392
|
-
|
|
393
|
-
function SafeComponent() {
|
|
394
|
-
try {
|
|
395
|
-
return riskyOperation();
|
|
396
|
-
} catch (error) {
|
|
397
|
-
console.error('Component error:', error);
|
|
398
|
-
return Div({ class: 'error' }, 'Something went wrong');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Error Boundaries
|
|
404
|
-
Use error boundaries for robust applications:
|
|
405
|
-
|
|
406
|
-
```javascript
|
|
407
|
-
function withErrorBoundary(component) {
|
|
408
|
-
return component.errorBoundary((error) => {
|
|
409
|
-
console.error('Error caught:', error);
|
|
410
|
-
return Div({ class: 'error-boundary' }, [
|
|
411
|
-
'An error occurred. Please try again.'
|
|
412
|
-
]);
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
## Performance Considerations
|
|
418
|
-
|
|
419
|
-
### Efficient Updates
|
|
420
|
-
NativeDocument optimizes updates automatically, but you can help:
|
|
421
|
-
|
|
422
|
-
```javascript
|
|
423
|
-
// Good: Batch related updates
|
|
424
|
-
function updateUser(newData) {
|
|
425
|
-
user.set({ ...user.val(), ...newData });
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Less efficient: Multiple separate updates
|
|
429
|
-
function updateUserSeparately(name, email) {
|
|
430
|
-
user.set({ ...user.val(), name });
|
|
431
|
-
user.set({ ...user.val(), email });
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### List Rendering
|
|
436
|
-
Use key functions for efficient list updates:
|
|
437
|
-
|
|
438
|
-
```javascript
|
|
439
|
-
ForEach(items, (item) =>
|
|
440
|
-
Div(['Item: ', item.name]),
|
|
441
|
-
// Key function for efficient updates
|
|
442
|
-
(item) => item.id
|
|
443
|
-
);
|
|
444
|
-
// or
|
|
445
|
-
ForEach(items, (item) =>
|
|
446
|
-
Div(['Item: ', item.name]),
|
|
447
|
-
// Key property for efficient updates
|
|
448
|
-
'id'
|
|
449
|
-
);
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
## Best Practices
|
|
453
|
-
|
|
454
|
-
### 1. Keep Components Small and Focused
|
|
455
|
-
```javascript
|
|
456
|
-
// Good: Focused component
|
|
457
|
-
function UserName(user) {
|
|
458
|
-
return Div({ class: 'user-name' }, user.name);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Less ideal: Component doing too much
|
|
462
|
-
function UserEverything(user) {
|
|
463
|
-
// Handles name, email, avatar, settings, etc.
|
|
464
|
-
}
|
|
465
|
-
```
|
|
466
|
-
|
|
467
|
-
### 2. Use Computed Values for Derived State
|
|
468
|
-
```javascript
|
|
469
|
-
// Good: Computed value
|
|
470
|
-
const filteredItems = Observable.computed(() => {
|
|
471
|
-
return items.val().filter(item => item.visible);
|
|
472
|
-
}, [items]);
|
|
473
|
-
|
|
474
|
-
// Less efficient: Manual filtering on each render
|
|
475
|
-
```
|
|
328
|
+
## Error Handling
|
|
476
329
|
|
|
477
|
-
### 3. Separate Concerns
|
|
478
330
|
```javascript
|
|
479
|
-
//
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
function UserProfile(userId) {
|
|
487
|
-
// UI rendering logic
|
|
488
|
-
}
|
|
331
|
+
// Error boundary on a component function
|
|
332
|
+
const SafeWidget = Widget.errorBoundary((error, { caller, args }) => {
|
|
333
|
+
console.error('Widget error:', error);
|
|
334
|
+
return Div({ class: 'error' }, 'Something went wrong');
|
|
335
|
+
});
|
|
489
336
|
```
|
|
490
337
|
|
|
491
|
-
|
|
492
|
-
```javascript
|
|
493
|
-
// Good: Clear naming
|
|
494
|
-
const isUserLoggedIn = Observable(false);
|
|
495
|
-
const currentUserName = Observable('');
|
|
496
|
-
|
|
497
|
-
// Less clear
|
|
498
|
-
const flag = Observable(false);
|
|
499
|
-
const data = Observable('');
|
|
500
|
-
```
|
|
338
|
+
---
|
|
501
339
|
|
|
502
340
|
## Next Steps
|
|
503
341
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
- **[
|
|
507
|
-
- **[
|
|
508
|
-
- **[
|
|
509
|
-
- **[
|
|
510
|
-
- **[
|
|
511
|
-
- **[
|
|
512
|
-
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
513
|
-
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
514
|
-
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
515
|
-
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
516
|
-
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
517
|
-
- **[Memory Management](memory-management.md)** - Memory management
|
|
518
|
-
- **[Anchor](anchor.md)** - Anchor
|
|
342
|
+
- **[Observables](./observables.md)** - Full reactive state reference
|
|
343
|
+
- **[Elements](./elements.md)** - Creating and composing UI
|
|
344
|
+
- **[Conditional Rendering](./conditional-rendering.md)** - ShowIf, Match, Switch
|
|
345
|
+
- **[List Rendering](./list-rendering.md)** - ForEach and dynamic lists
|
|
346
|
+
- **[Routing](./routing.md)** - Navigation and URL management
|
|
347
|
+
- **[State Management](./state-management.md)** - Global state with Store
|
|
348
|
+
- **[NDElement](./native-document-element.md)** - Full `.nd` API reference
|
|
349
|
+
- **[CLI](./cli.md)** - Project scaffolding
|
|
519
350
|
|
|
520
351
|
## Utilities
|
|
521
352
|
|
|
522
|
-
- **[Cache](
|
|
523
|
-
- **[NativeFetch](
|
|
524
|
-
- **[Filters](
|
|
353
|
+
- **[Cache](./cache.md)** - Lazy initialization and singleton patterns
|
|
354
|
+
- **[NativeFetch](./native-fetch.md)** - HTTP client with interceptors
|
|
355
|
+
- **[Filters](./filters.md)** - Data filtering helpers
|