native-document 1.0.92 → 1.0.94
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/dist/native-document.components.min.js +1088 -65
- package/dist/native-document.dev.js +695 -142
- package/dist/native-document.dev.js.map +1 -1
- package/dist/native-document.devtools.min.js +1 -1
- package/dist/native-document.min.js +1 -1
- package/docs/advanced-components.md +814 -0
- package/docs/anchor.md +71 -11
- package/docs/cache.md +888 -0
- package/docs/conditional-rendering.md +91 -1
- package/docs/core-concepts.md +9 -2
- package/docs/elements.md +127 -2
- package/docs/extending-native-document-element.md +7 -1
- package/docs/filters.md +1216 -0
- package/docs/getting-started.md +12 -3
- package/docs/lifecycle-events.md +10 -2
- package/docs/list-rendering.md +453 -54
- package/docs/memory-management.md +9 -7
- package/docs/native-document-element.md +30 -9
- package/docs/native-fetch.md +744 -0
- package/docs/observables.md +135 -6
- package/docs/routing.md +7 -1
- package/docs/state-management.md +7 -1
- package/docs/validation.md +8 -1
- package/elements.js +1 -0
- package/eslint.config.js +3 -3
- package/index.def.js +350 -0
- package/package.json +3 -2
- package/readme.md +53 -14
- package/src/components/$traits/HasItems.js +42 -1
- package/src/components/BaseComponent.js +4 -1
- package/src/components/accordion/Accordion.js +112 -8
- package/src/components/accordion/AccordionItem.js +93 -4
- package/src/components/alert/Alert.js +164 -4
- package/src/components/avatar/Avatar.js +236 -22
- package/src/components/menu/index.js +1 -2
- package/src/core/data/ObservableArray.js +120 -2
- package/src/core/data/ObservableChecker.js +50 -0
- package/src/core/data/ObservableItem.js +124 -4
- package/src/core/data/ObservableWhen.js +36 -6
- package/src/core/data/observable-helpers/array.js +12 -3
- package/src/core/data/observable-helpers/computed.js +17 -4
- package/src/core/data/observable-helpers/object.js +19 -3
- package/src/core/elements/content-formatter.js +138 -1
- package/src/core/elements/control/for-each-array.js +20 -2
- package/src/core/elements/control/for-each.js +17 -5
- package/src/core/elements/control/show-if.js +31 -15
- package/src/core/elements/control/show-when.js +23 -0
- package/src/core/elements/control/switch.js +40 -10
- package/src/core/elements/description-list.js +14 -0
- package/src/core/elements/form.js +188 -4
- package/src/core/elements/html5-semantics.js +44 -1
- package/src/core/elements/img.js +22 -10
- package/src/core/elements/index.js +5 -0
- package/src/core/elements/interactive.js +19 -1
- package/src/core/elements/list.js +28 -1
- package/src/core/elements/medias.js +29 -0
- package/src/core/elements/meta-data.js +34 -0
- package/src/core/elements/table.js +59 -0
- package/src/core/utils/cache.js +5 -0
- package/src/core/utils/helpers.js +7 -2
- package/src/core/utils/memoize.js +25 -16
- package/src/core/utils/prototypes.js +3 -2
- package/src/core/wrappers/AttributesWrapper.js +1 -1
- package/src/core/wrappers/HtmlElementWrapper.js +2 -2
- package/src/core/wrappers/NDElement.js +42 -2
- package/src/core/wrappers/NdPrototype.js +4 -0
- package/src/core/wrappers/TemplateCloner.js +14 -11
- package/src/core/wrappers/prototypes/bind-class-extensions.js +1 -1
- package/src/core/wrappers/prototypes/nd-element-extensions.js +3 -0
- package/src/router/Route.js +9 -4
- package/src/router/Router.js +28 -9
- package/src/router/errors/RouterError.js +0 -1
- package/types/control-flow.d.ts +9 -6
- package/types/elements.d.ts +496 -111
- package/types/filters/index.d.ts +4 -0
- package/types/forms.d.ts +85 -48
- package/types/images.d.ts +16 -9
- package/types/nd-element.d.ts +5 -238
- package/types/observable.d.ts +9 -3
- package/types/router.d.ts +5 -1
- package/types/template-cloner.ts +1 -0
- package/types/validator.ts +11 -1
- package/utils.d.ts +2 -1
- package/utils.js +4 -4
- package/src/core/utils/service.js +0 -6
|
@@ -0,0 +1,814 @@
|
|
|
1
|
+
# Advanced Components
|
|
2
|
+
|
|
3
|
+
NativeDocument provides advanced component patterns for optimizing rendering performance through template cloning and data binding. The `useCache()` utility creates reusable component templates with a sophisticated binding system that efficiently updates only the dynamic parts.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Advanced component utilities include:
|
|
8
|
+
- **`useCache(fn)`** - Create cached components with data binding
|
|
9
|
+
- **`useSingleton(fn)`** - Create singleton views with updateable sections
|
|
10
|
+
- **Binder API** - Powerful data binding system for templates
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
```javascript
|
|
14
|
+
import { useCache, useSingleton } from 'native-document';
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## useCache() - Cached Components with Binding
|
|
18
|
+
|
|
19
|
+
`useCache()` creates a component template that is built once and then cloned efficiently. The component function receives a **binder** object (`$binder`) that creates bindings for dynamic data.
|
|
20
|
+
|
|
21
|
+
### Basic Concept
|
|
22
|
+
```javascript
|
|
23
|
+
import { useCache } from 'native-document';
|
|
24
|
+
import { Div, Span } from 'native-document/src/elements';
|
|
25
|
+
|
|
26
|
+
// Component function receives $binder
|
|
27
|
+
const UserCard = useCache(($binder) => {
|
|
28
|
+
// Create bindings for dynamic data
|
|
29
|
+
const name = $binder.value('name');
|
|
30
|
+
const age = $binder.value('age');
|
|
31
|
+
|
|
32
|
+
// Build template with bindings
|
|
33
|
+
return Div({ class: 'user-card' }, [
|
|
34
|
+
Span('Name: '),
|
|
35
|
+
Span(name),
|
|
36
|
+
Span(' - Age: '),
|
|
37
|
+
Span(age)
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Usage - pass data object
|
|
42
|
+
const card1 = UserCard({ name: 'Alice', age: 25 });
|
|
43
|
+
const card2 = UserCard({ name: 'Bob', age: 30 });
|
|
44
|
+
// card2 is cloned from template, bindings update automatically
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### How Arguments Are Passed
|
|
48
|
+
|
|
49
|
+
Binder functions receive arguments exactly as the user passes them:
|
|
50
|
+
```javascript
|
|
51
|
+
import { useCache } from 'native-document';
|
|
52
|
+
import { Div } from 'native-document/src/elements';
|
|
53
|
+
|
|
54
|
+
const Component = useCache(($binder) => {
|
|
55
|
+
// Function receives arguments exactly as passed
|
|
56
|
+
const value = $binder.value((data) => {
|
|
57
|
+
// data = { name: 'Alice', age: 25 }
|
|
58
|
+
return data.name;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return Div(value);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Usage - object passed directly
|
|
65
|
+
const instance = Component({ name: 'Alice', age: 25 });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Binder Methods
|
|
69
|
+
|
|
70
|
+
### $binder.value() - Property Binding
|
|
71
|
+
|
|
72
|
+
Bind to a property name or use a transform function:
|
|
73
|
+
```javascript
|
|
74
|
+
import { useCache } from 'native-document';
|
|
75
|
+
import { Div, Span } from 'native-document/src/elements';
|
|
76
|
+
|
|
77
|
+
const ProductCard = useCache(($binder) => {
|
|
78
|
+
// Bind to property by key name
|
|
79
|
+
const name = $binder.value('name');
|
|
80
|
+
const price = $binder.value('price');
|
|
81
|
+
|
|
82
|
+
// Bind with transform function
|
|
83
|
+
// Function receives the data object directly
|
|
84
|
+
const formattedPrice = $binder.value((product) => {
|
|
85
|
+
return `$${product.price.toFixed(2)}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const status = $binder.value((product) => {
|
|
89
|
+
return product.stock > 0 ? 'In Stock' : 'Out of Stock';
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return Div({ class: 'product-card' }, [
|
|
93
|
+
Span({ class: 'name' }, name),
|
|
94
|
+
Span({ class: 'price' }, formattedPrice),
|
|
95
|
+
Span({ class: 'status' }, status)
|
|
96
|
+
]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Usage
|
|
100
|
+
const card = ProductCard({ name: 'Phone', price: 599, stock: 10 });
|
|
101
|
+
// Displays: Phone, $599.00, In Stock
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### $binder.property() - Alias for value()
|
|
105
|
+
```javascript
|
|
106
|
+
import { useCache } from 'native-document';
|
|
107
|
+
|
|
108
|
+
const Component = useCache(($binder) => {
|
|
109
|
+
// property() is an alias for value()
|
|
110
|
+
const name = $binder.property('name');
|
|
111
|
+
const title = $binder.value('title');
|
|
112
|
+
|
|
113
|
+
// Both work the same way
|
|
114
|
+
return Div([Span(name), Span(title)]);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### $binder.class() - Class Binding
|
|
119
|
+
|
|
120
|
+
Bind CSS classes dynamically based on data:
|
|
121
|
+
```javascript
|
|
122
|
+
import { useCache } from 'native-document';
|
|
123
|
+
import { Div, Span } from 'native-document/src/elements';
|
|
124
|
+
|
|
125
|
+
const TaskItem = useCache(($binder) => {
|
|
126
|
+
const text = $binder.value('text');
|
|
127
|
+
|
|
128
|
+
// Function receives the data object directly
|
|
129
|
+
const completedClass = $binder.class((task) => {
|
|
130
|
+
return task.completed;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const priorityClass = $binder.class((task) => {
|
|
134
|
+
return task.priority === 'high';
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return Div({
|
|
138
|
+
class: {
|
|
139
|
+
'task': true,
|
|
140
|
+
'completed': completedClass,
|
|
141
|
+
'high-priority': priorityClass
|
|
142
|
+
}
|
|
143
|
+
}, [
|
|
144
|
+
Span(text)
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Usage
|
|
149
|
+
const task = TaskItem({ text: 'Fix bug', completed: true, priority: 'high' });
|
|
150
|
+
// <div class="task completed high-priority">...</div>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### $binder.class() with Observable.when()
|
|
154
|
+
```javascript
|
|
155
|
+
import { useCache } from 'native-document';
|
|
156
|
+
import { Tr, Td } from 'native-document/src/elements';
|
|
157
|
+
import { Observable } from 'native-document';
|
|
158
|
+
|
|
159
|
+
const selectedId = Observable(null);
|
|
160
|
+
|
|
161
|
+
const TableRow = useCache(($binder) => {
|
|
162
|
+
const id = $binder.value('id');
|
|
163
|
+
const name = $binder.value('name');
|
|
164
|
+
|
|
165
|
+
// Bind class to observable condition
|
|
166
|
+
const isSelected = $binder.class((item) => {
|
|
167
|
+
return selectedId.when(item.id);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return Tr({
|
|
171
|
+
class: { 'selected': isSelected }
|
|
172
|
+
}, [
|
|
173
|
+
Td(id),
|
|
174
|
+
Td(name)
|
|
175
|
+
]);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Usage
|
|
179
|
+
const row1 = TableRow({ id: 1, name: 'Item 1' });
|
|
180
|
+
const row2 = TableRow({ id: 2, name: 'Item 2' });
|
|
181
|
+
|
|
182
|
+
// Change selection - classes update automatically
|
|
183
|
+
selectedId.set(1); // row1 gets 'selected' class
|
|
184
|
+
selectedId.set(2); // row2 gets 'selected' class, row1 loses it
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### $binder.style() - Style Binding
|
|
188
|
+
|
|
189
|
+
Bind inline styles dynamically:
|
|
190
|
+
```javascript
|
|
191
|
+
import { useCache } from 'native-document';
|
|
192
|
+
import { Div } from 'native-document/src/elements';
|
|
193
|
+
|
|
194
|
+
const ProgressBar = useCache(($binder) => {
|
|
195
|
+
const percentage = $binder.value('percentage');
|
|
196
|
+
|
|
197
|
+
// Function receives the data object directly
|
|
198
|
+
const widthStyle = $binder.style((progress) => {
|
|
199
|
+
return progress.percentage + '%';
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const colorStyle = $binder.style((progress) => {
|
|
203
|
+
return progress.percentage >= 100 ? 'green' : 'blue';
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return Div({ class: 'progress-bar' }, [
|
|
207
|
+
Div({
|
|
208
|
+
class: 'progress-fill',
|
|
209
|
+
style: {
|
|
210
|
+
width: widthStyle,
|
|
211
|
+
backgroundColor: colorStyle
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Usage
|
|
218
|
+
const bar = ProgressBar({ percentage: 75 });
|
|
219
|
+
// <div style="width: 75%; background-color: blue"></div>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### $binder.attr() - Attribute Binding
|
|
223
|
+
|
|
224
|
+
Bind element attributes dynamically. Takes only a function that returns the attribute value:
|
|
225
|
+
```javascript
|
|
226
|
+
import { useCache } from 'native-document';
|
|
227
|
+
import { Div, Img, A } from 'native-document/src/elements';
|
|
228
|
+
|
|
229
|
+
const ProductCard = useCache(($binder) => {
|
|
230
|
+
const name = $binder.value('name');
|
|
231
|
+
|
|
232
|
+
// attr() takes ONLY a function
|
|
233
|
+
// Function receives the data object directly
|
|
234
|
+
const imageSrc = $binder.attr((product) => {
|
|
235
|
+
return product.imageUrl;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const detailsHref = $binder.attr((product) => {
|
|
239
|
+
return `/products/${product.id}`;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Use as object property with attribute name
|
|
243
|
+
return Div({ class: 'product' }, [
|
|
244
|
+
Img({ src: imageSrc }),
|
|
245
|
+
A({ href: detailsHref }, name)
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Usage
|
|
250
|
+
const card = ProductCard({
|
|
251
|
+
id: 123,
|
|
252
|
+
name: 'Phone',
|
|
253
|
+
imageUrl: '/images/phone.jpg'
|
|
254
|
+
});
|
|
255
|
+
// <img src="/images/phone.jpg">
|
|
256
|
+
// <a href="/products/123">Phone</a>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### $binder.attach() - Event Handler Binding
|
|
260
|
+
|
|
261
|
+
Attach event handlers that receive the event followed by user arguments:
|
|
262
|
+
```javascript
|
|
263
|
+
import { useCache } from 'native-document';
|
|
264
|
+
import { Div, Button, Span } from 'native-document/src/elements';
|
|
265
|
+
|
|
266
|
+
const TodoItem = useCache(($binder) => {
|
|
267
|
+
const text = $binder.value('text');
|
|
268
|
+
|
|
269
|
+
// Handler receives (event, ...userArguments)
|
|
270
|
+
const handleToggle = $binder.attach((event, todo) => {
|
|
271
|
+
console.log('Toggle clicked:', event);
|
|
272
|
+
console.log('Todo data:', todo);
|
|
273
|
+
updateTodo(todo.id, { completed: !todo.completed });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const handleDelete = $binder.attach((event, todo) => {
|
|
277
|
+
console.log('Delete clicked:', event);
|
|
278
|
+
deleteTodo(todo.id);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return Div({ class: 'todo' }, [
|
|
282
|
+
Span(text).nd.attach('onClick', handleToggle),
|
|
283
|
+
Button('Delete').nd.attach('onClick', handleDelete)
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Usage
|
|
288
|
+
const todo = TodoItem({ id: 1, text: 'Buy milk', completed: false });
|
|
289
|
+
// Clicking triggers handlers with the todo object
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Using .nd.attach()
|
|
293
|
+
|
|
294
|
+
The `.nd.attach(eventName, handler)` method connects binder handlers to elements:
|
|
295
|
+
```javascript
|
|
296
|
+
import { useCache } from 'native-document';
|
|
297
|
+
import { Button } from 'native-document/src/elements';
|
|
298
|
+
|
|
299
|
+
const ActionButton = useCache(($binder) => {
|
|
300
|
+
const label = $binder.value('label');
|
|
301
|
+
|
|
302
|
+
// Handler receives event + user data
|
|
303
|
+
const clickHandler = $binder.attach((event, data) => {
|
|
304
|
+
console.log('Action:', data.action);
|
|
305
|
+
console.log('Event:', event);
|
|
306
|
+
performAction(data.action);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Use .nd.attach(eventName, handler)
|
|
310
|
+
return Button(label).nd.attach('onClick', clickHandler);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Usage
|
|
314
|
+
const btn = ActionButton({ label: 'Save', action: 'save' });
|
|
315
|
+
// Clicking logs: "Action: save"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Complete Example
|
|
319
|
+
|
|
320
|
+
Complete table row component from the codebase:
|
|
321
|
+
```javascript
|
|
322
|
+
import { useCache } from 'native-document';
|
|
323
|
+
import { Tr, Td, Link, Button } from 'native-document/src/elements';
|
|
324
|
+
import { Observable, ForEachArray, TBody } from 'native-document';
|
|
325
|
+
|
|
326
|
+
// App state
|
|
327
|
+
const AppService = {
|
|
328
|
+
data: Observable.array([
|
|
329
|
+
{ id: 1, label: 'Item 1' },
|
|
330
|
+
{ id: 2, label: 'Item 2' },
|
|
331
|
+
{ id: 3, label: 'Item 3' }
|
|
332
|
+
]),
|
|
333
|
+
selected: Observable(null),
|
|
334
|
+
|
|
335
|
+
select(id) {
|
|
336
|
+
this.selected.set(id);
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
remove(id) {
|
|
340
|
+
const index = this.data.val().findIndex(item => item.id === id);
|
|
341
|
+
if (index > -1) {
|
|
342
|
+
this.data.remove(index);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// Cached table row with bindings
|
|
348
|
+
const TableRowBuilder = useCache(($binder) => {
|
|
349
|
+
// Class binding with Observable.when()
|
|
350
|
+
const isSelected = $binder.class((item) => {
|
|
351
|
+
return AppService.selected.when(item.id);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Value bindings
|
|
355
|
+
const id = $binder.value('id');
|
|
356
|
+
const label = $binder.value('label');
|
|
357
|
+
|
|
358
|
+
// Event handler bindings - receive (event, item)
|
|
359
|
+
const rowClick = $binder.attach((event, item) => {
|
|
360
|
+
AppService.select(item.id);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const removeClick = $binder.attach((event, item) => {
|
|
364
|
+
AppService.remove(item.id);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return Tr({ class: { 'selected': isSelected } }, [
|
|
368
|
+
Td({ class: 'col-md-1' }, id),
|
|
369
|
+
Td({ class: 'col-md-4' },
|
|
370
|
+
Link(label).nd.attach('onClick', rowClick)
|
|
371
|
+
),
|
|
372
|
+
Td({ class: 'col-md-1' },
|
|
373
|
+
Button('×').nd.attach('onClick', removeClick)
|
|
374
|
+
),
|
|
375
|
+
Td({ class: 'col-md-6' })
|
|
376
|
+
]);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Usage with ForEachArray
|
|
380
|
+
const TableBody = TBody(
|
|
381
|
+
ForEachArray(AppService.data, TableRowBuilder)
|
|
382
|
+
);
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
## Complex Binding Examples
|
|
386
|
+
|
|
387
|
+
### Multiple Bindings
|
|
388
|
+
```javascript
|
|
389
|
+
import { useCache } from 'native-document';
|
|
390
|
+
import { Div, H3, Span, Img } from 'native-document/src/elements';
|
|
391
|
+
|
|
392
|
+
const UserProfile = useCache(($binder) => {
|
|
393
|
+
// Simple value bindings
|
|
394
|
+
const email = $binder.value('email');
|
|
395
|
+
|
|
396
|
+
// Computed value bindings - function receives user object directly
|
|
397
|
+
const fullName = $binder.value((user) => {
|
|
398
|
+
return `${user.firstName} ${user.lastName}`;
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const memberSince = $binder.value((user) => {
|
|
402
|
+
const date = new Date(user.joinedAt);
|
|
403
|
+
return date.toLocaleDateString();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Attribute binding - function receives user object directly
|
|
407
|
+
const avatarSrc = $binder.attr((user) => {
|
|
408
|
+
return user.avatarUrl;
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Class binding - function receives user object directly
|
|
412
|
+
const isPremium = $binder.class((user) => {
|
|
413
|
+
return user.plan === 'premium';
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return Div({
|
|
417
|
+
class: {
|
|
418
|
+
'user-profile': true,
|
|
419
|
+
'premium': isPremium
|
|
420
|
+
}
|
|
421
|
+
}, [
|
|
422
|
+
Img({ src: avatarSrc, class: 'avatar' }),
|
|
423
|
+
H3(fullName),
|
|
424
|
+
Span({ class: 'email' }, email),
|
|
425
|
+
Span({ class: 'member-since' }, ['Member since: ', memberSince])
|
|
426
|
+
]);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Usage
|
|
430
|
+
const profile = UserProfile({
|
|
431
|
+
firstName: 'Alice',
|
|
432
|
+
lastName: 'Johnson',
|
|
433
|
+
email: 'alice@example.com',
|
|
434
|
+
avatarUrl: '/avatars/alice.jpg',
|
|
435
|
+
plan: 'premium',
|
|
436
|
+
joinedAt: '2023-01-15'
|
|
437
|
+
});
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Style Bindings
|
|
441
|
+
```javascript
|
|
442
|
+
import { useCache } from 'native-document';
|
|
443
|
+
import { Div } from 'native-document/src/elements';
|
|
444
|
+
|
|
445
|
+
const ColoredBox = useCache(($binder) => {
|
|
446
|
+
const text = $binder.value('text');
|
|
447
|
+
|
|
448
|
+
// Multiple style bindings - each receives box object
|
|
449
|
+
const bgColor = $binder.style((box) => {
|
|
450
|
+
return box.color;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const boxWidth = $binder.style((box) => {
|
|
454
|
+
return box.width + 'px';
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const boxHeight = $binder.style((box) => {
|
|
458
|
+
return box.height + 'px';
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
return Div({
|
|
462
|
+
style: {
|
|
463
|
+
backgroundColor: bgColor,
|
|
464
|
+
width: boxWidth,
|
|
465
|
+
height: boxHeight,
|
|
466
|
+
display: 'flex',
|
|
467
|
+
alignItems: 'center',
|
|
468
|
+
justifyContent: 'center'
|
|
469
|
+
}
|
|
470
|
+
}, text);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Usage
|
|
474
|
+
const box = ColoredBox({
|
|
475
|
+
text: 'Hello',
|
|
476
|
+
color: '#3498db',
|
|
477
|
+
width: 200,
|
|
478
|
+
height: 100
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Conditional Content
|
|
483
|
+
```javascript
|
|
484
|
+
import { useCache } from 'native-document';
|
|
485
|
+
import { Div, Span, Badge } from 'native-document/src/elements';
|
|
486
|
+
|
|
487
|
+
const ProductCard = useCache(($binder) => {
|
|
488
|
+
const name = $binder.value('name');
|
|
489
|
+
|
|
490
|
+
// Computed price - receives product object
|
|
491
|
+
const displayPrice = $binder.value((product) => {
|
|
492
|
+
return product.salePrice
|
|
493
|
+
? `$${product.salePrice} (was $${product.price})`
|
|
494
|
+
: `$${product.price}`;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// Conditional classes - receive product object
|
|
498
|
+
const isOnSale = $binder.class((product) => {
|
|
499
|
+
return product.salePrice !== null;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const isOutOfStock = $binder.class((product) => {
|
|
503
|
+
return product.stock === 0;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// Stock badge text - receives product object
|
|
507
|
+
const stockBadge = $binder.value((product) => {
|
|
508
|
+
if (product.stock === 0) return 'Out of Stock';
|
|
509
|
+
if (product.stock < 5) return 'Low Stock';
|
|
510
|
+
return 'In Stock';
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return Div({
|
|
514
|
+
class: {
|
|
515
|
+
'product': true,
|
|
516
|
+
'on-sale': isOnSale,
|
|
517
|
+
'out-of-stock': isOutOfStock
|
|
518
|
+
}
|
|
519
|
+
}, [
|
|
520
|
+
Span({ class: 'name' }, name),
|
|
521
|
+
Span({ class: 'price' }, displayPrice),
|
|
522
|
+
Badge(stockBadge)
|
|
523
|
+
]);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Usage
|
|
527
|
+
const product = ProductCard({
|
|
528
|
+
name: 'Phone',
|
|
529
|
+
price: 599,
|
|
530
|
+
salePrice: 499,
|
|
531
|
+
stock: 3
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Multiple Arguments
|
|
536
|
+
```javascript
|
|
537
|
+
import { useCache } from 'native-document';
|
|
538
|
+
import { Div, H3, P } from 'native-document/src/elements';
|
|
539
|
+
|
|
540
|
+
// Component that accepts multiple arguments
|
|
541
|
+
const ArticleCard = useCache(($binder) => {
|
|
542
|
+
// When using multiple args, functions receive them all
|
|
543
|
+
const title = $binder.value((article, options) => {
|
|
544
|
+
return options.uppercase ? article.title.toUpperCase() : article.title;
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const excerpt = $binder.value((article, options) => {
|
|
548
|
+
const length = options.excerptLength || 100;
|
|
549
|
+
return article.content.substring(0, length) + '...';
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return Div({ class: 'article' }, [
|
|
553
|
+
H3(title),
|
|
554
|
+
P(excerpt)
|
|
555
|
+
]);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Usage with multiple arguments
|
|
559
|
+
const card = ArticleCard(
|
|
560
|
+
{ title: 'My Article', content: 'Long article content...' },
|
|
561
|
+
{ uppercase: true, excerptLength: 150 }
|
|
562
|
+
);
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## useSingleton() - Singleton Views
|
|
566
|
+
|
|
567
|
+
`useSingleton()` creates a view that is rendered only once, then updated through named sections.
|
|
568
|
+
|
|
569
|
+
### Basic Usage
|
|
570
|
+
```javascript
|
|
571
|
+
import { useSingleton } from 'native-document';
|
|
572
|
+
import { Div, H1 } from 'native-document/src/elements';
|
|
573
|
+
|
|
574
|
+
const Dashboard = useSingleton((view) => {
|
|
575
|
+
return Div({ class: 'dashboard' }, [
|
|
576
|
+
H1('Dashboard'),
|
|
577
|
+
|
|
578
|
+
// Named section - can be updated later
|
|
579
|
+
view.createSection('content'),
|
|
580
|
+
|
|
581
|
+
Div({ class: 'footer' }, 'Footer')
|
|
582
|
+
]);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// First render - creates the view
|
|
586
|
+
const dashboard = Dashboard.render();
|
|
587
|
+
document.body.appendChild(dashboard);
|
|
588
|
+
|
|
589
|
+
// Update specific section
|
|
590
|
+
Dashboard.render([
|
|
591
|
+
Div(['Updated content at ', new Date().toLocaleTimeString()])
|
|
592
|
+
]);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### createSection(name, transformFn?)
|
|
596
|
+
|
|
597
|
+
Creates a named section that can be updated. Optional transform function wraps the content:
|
|
598
|
+
```javascript
|
|
599
|
+
import { useSingleton } from 'native-document';
|
|
600
|
+
import { Div, Section, Header, Main } from 'native-document/src/elements';
|
|
601
|
+
|
|
602
|
+
const Layout = useSingleton((view) => {
|
|
603
|
+
return Div([
|
|
604
|
+
Header([
|
|
605
|
+
// Section without wrapper
|
|
606
|
+
view.createSection('header')
|
|
607
|
+
]),
|
|
608
|
+
Main([
|
|
609
|
+
// Section with wrapper function
|
|
610
|
+
view.createSection('main', (content) => {
|
|
611
|
+
return Section({ class: 'main-section' }, content);
|
|
612
|
+
})
|
|
613
|
+
])
|
|
614
|
+
]);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Render layout
|
|
618
|
+
const layout = Layout.render();
|
|
619
|
+
document.body.appendChild(layout);
|
|
620
|
+
|
|
621
|
+
// Update header - content inserted directly
|
|
622
|
+
Layout.render([H1('My App')]);
|
|
623
|
+
|
|
624
|
+
// Update main - content wrapped in Section
|
|
625
|
+
Layout.render([P('Main content')]);
|
|
626
|
+
// Result: <section class="main-section"><p>Main content</p></section>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Multiple Sections
|
|
630
|
+
```javascript
|
|
631
|
+
import { useSingleton } from 'native-document';
|
|
632
|
+
import { Div, Header, Main, Aside, H1, P, Ul, Li } from 'native-document/src/elements';
|
|
633
|
+
|
|
634
|
+
const AppLayout = useSingleton((view) => {
|
|
635
|
+
return Div({ class: 'app' }, [
|
|
636
|
+
Header([
|
|
637
|
+
view.createSection('header')
|
|
638
|
+
]),
|
|
639
|
+
Div({ class: 'container' }, [
|
|
640
|
+
Main([
|
|
641
|
+
view.createSection('main')
|
|
642
|
+
]),
|
|
643
|
+
Aside([
|
|
644
|
+
view.createSection('sidebar')
|
|
645
|
+
])
|
|
646
|
+
])
|
|
647
|
+
]);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// Render initial layout
|
|
651
|
+
const app = AppLayout.render();
|
|
652
|
+
document.body.appendChild(app);
|
|
653
|
+
|
|
654
|
+
// Update sections sequentially
|
|
655
|
+
Dashboard.render([H1('My App')]); // Updates header
|
|
656
|
+
Dashboard.render([P('Main content')]); // Updates main
|
|
657
|
+
Dashboard.render([Ul([Li('Item 1')])]); // Updates sidebar
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Best Practices
|
|
661
|
+
|
|
662
|
+
### 1. Use Descriptive Binding Names
|
|
663
|
+
```javascript
|
|
664
|
+
import { useCache } from 'native-document';
|
|
665
|
+
|
|
666
|
+
// ✅ Good: Clear variable names
|
|
667
|
+
const UserCard = useCache(($binder) => {
|
|
668
|
+
const userName = $binder.value('name');
|
|
669
|
+
const userEmail = $binder.value('email');
|
|
670
|
+
const isPremiumUser = $binder.class((user) => user.premium);
|
|
671
|
+
|
|
672
|
+
return Div({ class: { 'premium': isPremiumUser } }, [
|
|
673
|
+
Span(userName),
|
|
674
|
+
Span(userEmail)
|
|
675
|
+
]);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// ❌ Bad: Generic names
|
|
679
|
+
const UserCard = useCache(($binder) => {
|
|
680
|
+
const v1 = $binder.value('name');
|
|
681
|
+
const v2 = $binder.value('email');
|
|
682
|
+
const c1 = $binder.class((user) => user.premium);
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### 2. Remember Arguments Are Passed Directly
|
|
687
|
+
```javascript
|
|
688
|
+
import { useCache } from 'native-document';
|
|
689
|
+
|
|
690
|
+
// ✅ Good: Direct access to data
|
|
691
|
+
const Card = useCache(($binder) => {
|
|
692
|
+
const value = $binder.value((item) => {
|
|
693
|
+
return item.property; // item is the object passed
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Component call
|
|
698
|
+
Card({ property: 'value' });
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 3. Use .nd.attach() for Event Handlers
|
|
702
|
+
```javascript
|
|
703
|
+
import { useCache } from 'native-document';
|
|
704
|
+
import { Button } from 'native-document/src/elements';
|
|
705
|
+
|
|
706
|
+
// ✅ Good: Use binder.attach with .nd.attach()
|
|
707
|
+
const ActionButton = useCache(($binder) => {
|
|
708
|
+
const label = $binder.value('label');
|
|
709
|
+
|
|
710
|
+
// Handler receives (event, data)
|
|
711
|
+
const handleClick = $binder.attach((event, data) => {
|
|
712
|
+
console.log('Clicked:', data);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
return Button(label).nd.attach('onClick', handleClick);
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
// ❌ Bad: Direct event handler (won't receive data)
|
|
719
|
+
const ActionButton = useCache(($binder) => {
|
|
720
|
+
const label = $binder.value('label');
|
|
721
|
+
|
|
722
|
+
return Button(label).nd.onClick((event) => {
|
|
723
|
+
// No access to data here
|
|
724
|
+
});
|
|
725
|
+
});
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### 4. Combine with Observable.when()
|
|
729
|
+
```javascript
|
|
730
|
+
import { useCache } from 'native-document';
|
|
731
|
+
import { Observable } from 'native-document';
|
|
732
|
+
import { Li } from 'native-document/src/elements';
|
|
733
|
+
|
|
734
|
+
const activeId = Observable(null);
|
|
735
|
+
|
|
736
|
+
// ✅ Good: Use Observable.when() for reactive class binding
|
|
737
|
+
const ListItem = useCache(($binder) => {
|
|
738
|
+
const label = $binder.value('label');
|
|
739
|
+
|
|
740
|
+
// Class updates automatically when activeId changes
|
|
741
|
+
const isActive = $binder.class((item) => {
|
|
742
|
+
return activeId.when(item.id);
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
return Li({ class: { 'active': isActive } }, label);
|
|
746
|
+
});
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### 5. Keep Transform Functions Simple
|
|
750
|
+
```javascript
|
|
751
|
+
import { useCache } from 'native-document';
|
|
752
|
+
|
|
753
|
+
// ✅ Good: Simple, focused transforms
|
|
754
|
+
const Card = useCache(($binder) => {
|
|
755
|
+
const price = $binder.value((product) => {
|
|
756
|
+
return `$${product.price.toFixed(2)}`;
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
const inStock = $binder.class((product) => {
|
|
760
|
+
return product.stock > 0;
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
return Div({ class: { 'in-stock': inStock } }, price);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// ❌ Bad: Complex logic
|
|
767
|
+
const Card = useCache(($binder) => {
|
|
768
|
+
const value = $binder.value((product) => {
|
|
769
|
+
// Multiple API calls
|
|
770
|
+
// Complex calculations
|
|
771
|
+
// Side effects
|
|
772
|
+
return result;
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
## Binder API Reference
|
|
778
|
+
|
|
779
|
+
| Method | Parameters | Returns | Description |
|
|
780
|
+
|--------|------------|---------|-------------|
|
|
781
|
+
| `$binder.value(key)` | `key: string` | `TemplateBinding` | Bind to property by name |
|
|
782
|
+
| `$binder.value(fn)` | `fn: (...args) => any` | `TemplateBinding` | Bind with transform function |
|
|
783
|
+
| `$binder.property(key)` | `key: string` | `TemplateBinding` | Alias for value() |
|
|
784
|
+
| `$binder.class(fn)` | `fn: (...args) => boolean` | `TemplateBinding` | Bind CSS class |
|
|
785
|
+
| `$binder.style(fn)` | `fn: (...args) => string` | `TemplateBinding` | Bind inline style value |
|
|
786
|
+
| `$binder.attr(fn)` | `fn: (...args) => string` | `TemplateBinding` | Bind attribute value |
|
|
787
|
+
| `$binder.attach(fn)` | `fn: (event, ...args) => void` | `TemplateBinding` | Bind event handler |
|
|
788
|
+
|
|
789
|
+
**Note:** All binder methods (except `attach`) receive arguments exactly as passed by the user. The `attach` method receives the DOM event as the first parameter, followed by user arguments.
|
|
790
|
+
|
|
791
|
+
## Next Steps
|
|
792
|
+
|
|
793
|
+
Explore related concepts and utilities:
|
|
794
|
+
|
|
795
|
+
## Next Steps
|
|
796
|
+
|
|
797
|
+
- **[Getting Started](getting-started.md)** - Installation and first steps
|
|
798
|
+
- **[Core Concepts](core-concepts.md)** - Understanding the fundamentals
|
|
799
|
+
- **[Observables](observables.md)** - Reactive state management
|
|
800
|
+
- **[Elements](elements.md)** - Creating and composing UI
|
|
801
|
+
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
802
|
+
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
803
|
+
- **[Routing](routing.md)** - Navigation and URL management
|
|
804
|
+
- **[State Management](state-management.md)** - Global state patterns
|
|
805
|
+
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
806
|
+
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
807
|
+
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
808
|
+
- **[Memory Management](memory-management.md)** - Memory management
|
|
809
|
+
|
|
810
|
+
## Utilities
|
|
811
|
+
|
|
812
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
813
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
814
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|