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/list-rendering.md
CHANGED
|
@@ -1,1012 +1,387 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
---
|
|
2
|
+
title: List Rendering
|
|
3
|
+
description: Efficiently render dynamic collections with ForEach and ForEachArray - automatic DOM updates, keyed diffing, and reactive filtering
|
|
4
|
+
---
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
# List Rendering
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
NativeDocument provides two functions for rendering dynamic collections: `ForEach` for generic iteration over arrays and objects, and `ForEachArray` for high-performance array-specific operations.
|
|
8
9
|
|
|
9
10
|
```javascript
|
|
10
|
-
import { ForEach,
|
|
11
|
+
import { ForEach, ForEachArray } from 'native-document/elements';
|
|
12
|
+
```
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
---
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
const itemList = Ul([
|
|
16
|
-
ForEach(items, item => Li(item))
|
|
17
|
-
]);
|
|
16
|
+
## `ForEach` - Generic Collection Rendering
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
items.push('Orange', 'Grape');
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
## ForEach - Generic Collection Rendering
|
|
18
|
+
`ForEach` works with both observable arrays and observable objects.
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
```javascript
|
|
21
|
+
ForEach(data, callback, key?, options?)
|
|
22
|
+
```
|
|
26
23
|
|
|
27
|
-
###
|
|
24
|
+
### Array iteration
|
|
28
25
|
|
|
29
26
|
```javascript
|
|
30
27
|
const fruits = Observable.array(['Apple', 'Banana', 'Cherry']);
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
ForEach(fruits, fruit =>
|
|
34
|
-
|
|
35
|
-
)
|
|
36
|
-
]);
|
|
29
|
+
Ul(
|
|
30
|
+
ForEach(fruits, fruit => Li(fruit))
|
|
31
|
+
)
|
|
37
32
|
|
|
38
|
-
// All array operations trigger updates
|
|
39
|
-
fruits.push('Orange');
|
|
40
|
-
fruits.splice(1, 1);
|
|
41
|
-
fruits.sort();
|
|
33
|
+
// All array operations trigger DOM updates
|
|
34
|
+
fruits.push('Orange');
|
|
35
|
+
fruits.splice(1, 1);
|
|
36
|
+
fruits.sort();
|
|
42
37
|
```
|
|
43
38
|
|
|
44
|
-
### Object
|
|
39
|
+
### Object iteration
|
|
45
40
|
|
|
46
41
|
```javascript
|
|
47
|
-
const
|
|
48
|
-
admin:
|
|
49
|
-
editor: 'Content Editor',
|
|
42
|
+
const roles = Observable({
|
|
43
|
+
admin: 'Administrator',
|
|
44
|
+
editor: 'Content Editor',
|
|
50
45
|
viewer: 'Read Only'
|
|
51
46
|
});
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
ForEach(
|
|
55
|
-
Li([
|
|
56
|
-
Strong(roleKey), ': ', roleName
|
|
57
|
-
])
|
|
48
|
+
Ul(
|
|
49
|
+
ForEach(roles, (roleName, roleKey) =>
|
|
50
|
+
Li([Strong(roleKey), ': ', roleName])
|
|
58
51
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Update object - DOM reflects changes
|
|
62
|
-
userRoles.set({
|
|
63
|
-
...userRoles.val(),
|
|
64
|
-
moderator: 'Community Moderator'
|
|
65
|
-
});
|
|
52
|
+
)
|
|
66
53
|
```
|
|
67
54
|
|
|
68
|
-
###
|
|
55
|
+
### Index parameter
|
|
56
|
+
|
|
57
|
+
The second callback argument is an **observable** tracking the item's current index:
|
|
69
58
|
|
|
70
59
|
```javascript
|
|
71
|
-
const tasks = Observable.array([
|
|
72
|
-
'Review pull requests',
|
|
73
|
-
'Update documentation',
|
|
74
|
-
'Fix bug reports'
|
|
75
|
-
]);
|
|
60
|
+
const tasks = Observable.array(['Review PRs', 'Update docs', 'Fix bugs']);
|
|
76
61
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
tasks.remove(indexObservable.val())
|
|
85
|
-
)
|
|
86
|
-
])
|
|
87
|
-
)
|
|
88
|
-
]);
|
|
62
|
+
Ol(ForEach(tasks, (task, index) =>
|
|
63
|
+
Li([
|
|
64
|
+
Strong(index.transform(i => i + 1)), '. ',
|
|
65
|
+
task,
|
|
66
|
+
Button('Remove').nd.onClick(() => tasks.remove(index.val()))
|
|
67
|
+
])
|
|
68
|
+
))
|
|
89
69
|
```
|
|
90
70
|
|
|
91
|
-
###
|
|
71
|
+
### Key function
|
|
92
72
|
|
|
93
|
-
Use
|
|
73
|
+
Use a key to help NativeDocument identify items for efficient reordering. Pass a property name string or a function:
|
|
94
74
|
|
|
95
75
|
```javascript
|
|
96
76
|
const users = Observable.array([
|
|
97
77
|
{ id: 1, name: 'Alice', role: 'admin' },
|
|
98
|
-
{ id: 2, name: 'Bob',
|
|
99
|
-
{ id: 3, name: 'Carol', role: 'editor' }
|
|
78
|
+
{ id: 2, name: 'Bob', role: 'user' }
|
|
100
79
|
]);
|
|
101
80
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
ForEach(users,
|
|
105
|
-
user => Div({ class: 'user-card' }, [
|
|
106
|
-
H3(user.name),
|
|
107
|
-
Span({ class: 'role' }, user.role)
|
|
108
|
-
]),
|
|
109
|
-
'id' // Key function - uses user.id
|
|
110
|
-
// Or (item) => item.id
|
|
111
|
-
)
|
|
112
|
-
]);
|
|
81
|
+
// Property name shorthand
|
|
82
|
+
ForEach(users, user => Div(user.name), 'id')
|
|
113
83
|
|
|
114
|
-
//
|
|
115
|
-
users
|
|
84
|
+
// Function
|
|
85
|
+
ForEach(users, user => Div(user.name), item => item.id)
|
|
116
86
|
```
|
|
117
87
|
|
|
118
|
-
|
|
88
|
+
### Options
|
|
119
89
|
|
|
120
|
-
|
|
90
|
+
```javascript
|
|
91
|
+
ForEach(data, callback, key, { shouldKeepItemsInCache: true })
|
|
92
|
+
```
|
|
121
93
|
|
|
122
|
-
|
|
94
|
+
`shouldKeepItemsInCache` - when `true`, rendered items stay in cache even when removed. Useful when items toggle frequently:
|
|
123
95
|
|
|
124
|
-
|
|
96
|
+
```javascript
|
|
97
|
+
// Items are cached - re-adding won't re-render them
|
|
98
|
+
ForEach(items, renderItem, 'id', { shouldKeepItemsInCache: true })
|
|
99
|
+
```
|
|
125
100
|
|
|
126
|
-
|
|
127
|
-
- **Batch DOM updates** for better performance
|
|
128
|
-
- **Memory-efficient caching** with WeakMap references
|
|
129
|
-
- **Array method detection** for targeted updates (push, splice, sort, etc.)
|
|
101
|
+
---
|
|
130
102
|
|
|
131
|
-
|
|
103
|
+
## `ForEachArray` - High-Performance Array Rendering
|
|
104
|
+
|
|
105
|
+
`ForEachArray` is specifically optimized for arrays of objects. Use it when performance matters.
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
ForEachArray(data, callback, configs?)
|
|
109
|
+
```
|
|
132
110
|
|
|
133
111
|
```javascript
|
|
134
112
|
const messages = Observable.array([
|
|
135
|
-
{ id: 1, text: 'Hello
|
|
113
|
+
{ id: 1, text: 'Hello!', timestamp: Date.now() },
|
|
136
114
|
{ id: 2, text: 'How are you?', timestamp: Date.now() + 1000 }
|
|
137
115
|
]);
|
|
138
116
|
|
|
139
|
-
|
|
140
|
-
ForEachArray(messages, message =>
|
|
117
|
+
Div({ class: 'chat' },
|
|
118
|
+
ForEachArray(messages, message =>
|
|
141
119
|
Div({ class: 'message' }, [
|
|
142
|
-
Div(
|
|
143
|
-
Div(
|
|
120
|
+
Div(message.text),
|
|
121
|
+
Div(new Date(message.timestamp).toLocaleTimeString())
|
|
144
122
|
])
|
|
145
123
|
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// Optimized array operations
|
|
149
|
-
messages.push({ id: 3, text: 'New message!', timestamp: Date.now() });
|
|
124
|
+
)
|
|
150
125
|
```
|
|
151
126
|
|
|
152
|
-
###
|
|
127
|
+
### Index in `ForEachArray`
|
|
153
128
|
|
|
154
|
-
|
|
129
|
+
The index is a **computed observable** derived from the array - always reactive, no need to call `.val()` to use it in the DOM:
|
|
155
130
|
|
|
156
131
|
```javascript
|
|
157
|
-
|
|
158
|
-
{ id: 1, title: 'Song One', artist: 'Artist A' },
|
|
159
|
-
{ id: 2, title: 'Song Two', artist: 'Artist B' },
|
|
160
|
-
{ id: 3, title: 'Song Three', artist: 'Artist C' }
|
|
161
|
-
]);
|
|
162
|
-
|
|
163
|
-
const PlaylistView = Div({ class: 'playlist' }, [
|
|
164
|
-
ForEachArray(playlist, (song, indexObservable) => {
|
|
165
|
-
|
|
166
|
-
return Div({ class: 'song-item', style: 'display: flex; align-items: center; column-gap: 10px;' }, [
|
|
167
|
-
Div({ class: 'song-info' }, [
|
|
168
|
-
Strong(indexObservable.get((value) => value + 1)),
|
|
169
|
-
' - ',
|
|
170
|
-
Strong(song.title),
|
|
171
|
-
Span({ class: 'artist' }, ` by ${song.artist}`)
|
|
172
|
-
]),
|
|
173
|
-
Div({ class: 'song-controls' }, [
|
|
174
|
-
Button('↑').nd.onClick(() => {
|
|
175
|
-
const index = indexObservable.$value;
|
|
176
|
-
if(index > 0) {
|
|
177
|
-
playlist.swap(index, index-1);
|
|
178
|
-
}
|
|
179
|
-
}),
|
|
180
|
-
Button('↓').nd.onClick(() => {
|
|
181
|
-
const index = indexObservable.$value;
|
|
182
|
-
if(index < playlist.length -1) {
|
|
183
|
-
playlist.swap(index, index+1);
|
|
184
|
-
}
|
|
185
|
-
}),
|
|
186
|
-
Button('Remove').nd.onClick(() =>{
|
|
187
|
-
playlist.remove(indexObservable.$value);
|
|
188
|
-
})
|
|
189
|
-
])
|
|
190
|
-
])
|
|
191
|
-
}, 'id'),
|
|
192
|
-
Br,
|
|
132
|
+
ForEachArray(playlist, (song, index) =>
|
|
193
133
|
Div([
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
134
|
+
index.transform(i => i + 1), '. ',
|
|
135
|
+
song.title,
|
|
136
|
+
Button('Up').nd.onClick(() => {
|
|
137
|
+
const i = index.$value;
|
|
138
|
+
if (i > 0) {
|
|
139
|
+
playlist.swap(i, i - 1);
|
|
140
|
+
}
|
|
199
141
|
}),
|
|
200
|
-
Button('
|
|
201
|
-
|
|
142
|
+
Button('Down').nd.onClick(() => {
|
|
143
|
+
const i = index.$value;
|
|
144
|
+
if (i < playlist.val().length - 1) {
|
|
145
|
+
playlist.swap(i, i + 1);
|
|
146
|
+
}
|
|
202
147
|
}),
|
|
203
|
-
Button('
|
|
204
|
-
playlist.sort((a, b) => a.title.localeCompare(b.title))
|
|
205
|
-
})
|
|
148
|
+
Button('Remove').nd.onClick(() => playlist.remove(index.$value))
|
|
206
149
|
])
|
|
207
|
-
|
|
150
|
+
)
|
|
208
151
|
```
|
|
209
152
|
|
|
210
|
-
###
|
|
211
|
-
```javascript
|
|
212
|
-
import {
|
|
213
|
-
dateEquals, dateBefore, dateAfter, dateBetween,
|
|
214
|
-
timeEquals, timeBefore, timeAfter, timeBetween,
|
|
215
|
-
dateTimeEquals, dateTimeBefore, dateTimeAfter, dateTimeBetween
|
|
216
|
-
} from 'native-document/utils/filters';
|
|
217
|
-
|
|
218
|
-
const events = Observable.array([
|
|
219
|
-
{ name: 'Meeting', date: '2024-03-15' },
|
|
220
|
-
{ name: 'Conference', date: '2024-06-20' },
|
|
221
|
-
{ name: 'Workshop', date: '2024-09-10' }
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
// Date filters
|
|
225
|
-
const today = Observable(new Date());
|
|
226
|
-
const todayEvents = events.where({
|
|
227
|
-
date: dateEquals(today)
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
const futureEvents = events.where({
|
|
231
|
-
date: dateAfter(new Date())
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
const summerEvents = events.where({
|
|
235
|
-
date: dateBetween('2024-06-01', '2024-08-31')
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
// Time filters (ignores date, only checks time)
|
|
239
|
-
const morningEvents = events.where({
|
|
240
|
-
date: timeBefore('2024-01-01 12:00:00')
|
|
241
|
-
});
|
|
153
|
+
### Configs
|
|
242
154
|
|
|
243
|
-
const afternoonEvents = events.where({
|
|
244
|
-
date: timeBetween(
|
|
245
|
-
new Date('2024-01-01 13:00:00'),
|
|
246
|
-
new Date('2024-01-01 17:00:00')
|
|
247
|
-
)
|
|
248
|
-
});
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### Custom Filters
|
|
252
|
-
|
|
253
|
-
Create custom filter logic:
|
|
254
155
|
```javascript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const highRatedProducts = products.where({
|
|
260
|
-
_: custom((product, minRatingValue) => {
|
|
261
|
-
return product.rating >= minRatingValue && product.reviews > 10;
|
|
262
|
-
}, minRating) // Pass observables as dependencies
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// Multiple observable dependencies
|
|
266
|
-
const searchTerm = Observable('');
|
|
267
|
-
const minPrice = Observable(0);
|
|
268
|
-
|
|
269
|
-
const advancedFilter = products.where({
|
|
270
|
-
_: custom((product, search, price) => {
|
|
271
|
-
const matchesSearch = product.name.toLowerCase().includes(search.toLowerCase());
|
|
272
|
-
const matchesPrice = product.price >= price;
|
|
273
|
-
const hasDiscount = product.discount > 0;
|
|
274
|
-
|
|
275
|
-
return matchesSearch && matchesPrice && hasDiscount;
|
|
276
|
-
}, searchTerm, minPrice)
|
|
277
|
-
});
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Observable Array Utility Methods
|
|
281
|
-
|
|
282
|
-
### swap() - Reorder Items
|
|
283
|
-
|
|
284
|
-
Swap two items by their indices:
|
|
285
|
-
```javascript
|
|
286
|
-
const items = Observable.array(['A', 'B', 'C', 'D']);
|
|
287
|
-
|
|
288
|
-
// Swap items at index 0 and 2
|
|
289
|
-
items.swap(0, 2); // Result: ['C', 'B', 'A', 'D']
|
|
290
|
-
|
|
291
|
-
// Practical example: Move item up/down
|
|
292
|
-
const moveUp = (index) => {
|
|
293
|
-
if (index > 0) {
|
|
294
|
-
items.swap(index, index - 1);
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
const moveDown = (index) => {
|
|
299
|
-
if (index < items.length - 1) {
|
|
300
|
-
items.swap(index, index + 1);
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### removeItem() - Remove by Value
|
|
306
|
-
|
|
307
|
-
Remove an item by its value (not index):
|
|
308
|
-
```javascript
|
|
309
|
-
const tags = Observable.array(['javascript', 'react', 'vue', 'angular']);
|
|
310
|
-
|
|
311
|
-
// Remove by value
|
|
312
|
-
tags.removeItem('react'); // Result: ['javascript', 'vue', 'angular']
|
|
313
|
-
|
|
314
|
-
// Practical example: Remove tag
|
|
315
|
-
const TagList = ForEachArray(tags, tag =>
|
|
316
|
-
Span({ class: 'tag' }, [
|
|
317
|
-
tag,
|
|
318
|
-
Button('×').nd.onClick(() => tags.removeItem(tag))
|
|
319
|
-
]),
|
|
320
|
-
(item) => item
|
|
321
|
-
);
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
### isEmpty() - Check if Empty
|
|
325
|
-
|
|
326
|
-
Check if array is empty:
|
|
327
|
-
```javascript
|
|
328
|
-
const todos = Observable.array([]);
|
|
329
|
-
|
|
330
|
-
// Check if empty
|
|
331
|
-
console.log(todos.isEmpty()); // true
|
|
332
|
-
|
|
333
|
-
todos.push({ text: 'New task' });
|
|
334
|
-
console.log(todos.isEmpty()); // false
|
|
335
|
-
|
|
336
|
-
// Practical example: Show empty state
|
|
337
|
-
const TodoList = Div([
|
|
338
|
-
ShowIf(todos.check(list => list.isEmpty()),
|
|
339
|
-
Div({ class: 'empty-state' }, 'No todos yet!')
|
|
340
|
-
),
|
|
341
|
-
ForEachArray(todos, renderTodo, 'id')
|
|
342
|
-
]);
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### clear() - Remove All Items
|
|
346
|
-
|
|
347
|
-
Clear all items from array:
|
|
348
|
-
```javascript
|
|
349
|
-
const notifications = Observable.array([...]);
|
|
350
|
-
|
|
351
|
-
// Clear all notifications
|
|
352
|
-
notifications.clear();
|
|
353
|
-
|
|
354
|
-
// Practical example: Clear all button
|
|
355
|
-
Button('Clear All').nd.onClick(() => notifications.clear())
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
### at() - Access Item by Index
|
|
359
|
-
|
|
360
|
-
Get item at specific index (supports negative indices):
|
|
361
|
-
```javascript
|
|
362
|
-
const items = Observable.array(['A', 'B', 'C', 'D']);
|
|
363
|
-
|
|
364
|
-
console.log(items.at(0)); // 'A'
|
|
365
|
-
console.log(items.at(-1)); // 'D' (last item)
|
|
366
|
-
console.log(items.at(-2)); // 'C' (second to last)
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### count() - Conditional Count
|
|
370
|
-
|
|
371
|
-
Count items that match a condition:
|
|
372
|
-
```javascript
|
|
373
|
-
const tasks = Observable.array([
|
|
374
|
-
{ text: 'Task 1', done: true },
|
|
375
|
-
{ text: 'Task 2', done: false },
|
|
376
|
-
{ text: 'Task 3', done: true }
|
|
377
|
-
]);
|
|
378
|
-
|
|
379
|
-
// Count completed tasks
|
|
380
|
-
const completedCount = tasks.count(task => task.done); // 2
|
|
381
|
-
|
|
382
|
-
// Practical example: Display count
|
|
383
|
-
const Stats = Div([
|
|
384
|
-
'Completed: ',
|
|
385
|
-
Observable.computed(() => tasks.count(t => t.done), [tasks]),
|
|
386
|
-
' / ',
|
|
387
|
-
tasks.check(list => list.length)
|
|
388
|
-
]);
|
|
156
|
+
ForEachArray(data, callback, {
|
|
157
|
+
shouldKeepItemsInCache: false, // same as ForEach
|
|
158
|
+
pushDelay: (items) => items.length > 100 ? 50 : 0 // throttle large batch inserts
|
|
159
|
+
})
|
|
389
160
|
```
|
|
390
161
|
|
|
391
|
-
|
|
162
|
+
---
|
|
392
163
|
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
const items = Observable.array([1, 2, 3]);
|
|
164
|
+
## Choosing Between `ForEach` and `ForEachArray`
|
|
396
165
|
|
|
397
|
-
|
|
398
|
-
|
|
166
|
+
| | `ForEach` | `ForEachArray` |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Arrays of objects | yes | yes (optimized) |
|
|
169
|
+
| Arrays of primitives | yes | yes |
|
|
170
|
+
| Object (non-array) iteration | yes | no |
|
|
171
|
+
| Index is observable | yes | yes (computed) |
|
|
172
|
+
| Key argument | third arg (string/fn) | inside configs |
|
|
173
|
+
| Best for | objects, primitives, small lists | large or frequently updated arrays |
|
|
399
174
|
|
|
400
|
-
|
|
401
|
-
// items.push(4); items.push(5); items.push(6); // Less efficient
|
|
402
|
-
````
|
|
175
|
+
---
|
|
403
176
|
|
|
404
|
-
##
|
|
177
|
+
## Filtering with `.where()`
|
|
405
178
|
|
|
406
|
-
|
|
179
|
+
`.where()` is available on `ObservableArray` only. It returns a new live `ObservableArray` that re-filters automatically. See [Observables](./observables.md) for the full `.where()` reference.
|
|
407
180
|
|
|
408
|
-
### Import Filter Helpers
|
|
409
181
|
```javascript
|
|
410
|
-
|
|
411
|
-
|
|
182
|
+
import { equals, greaterThan, lessThan, between, includes, match,
|
|
183
|
+
startsWith, endsWith, inArray, notIn, custom,
|
|
184
|
+
and, or, not } from 'native-document/filters';
|
|
412
185
|
|
|
413
|
-
// Or import all filters
|
|
414
|
-
import * as Filters from 'native-document/utils/filters';
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Basic Filtering with Helpers
|
|
418
|
-
```javascript
|
|
419
186
|
const products = Observable.array([
|
|
420
|
-
{ id: 1, name: 'Phone',
|
|
187
|
+
{ id: 1, name: 'Phone', price: 599, inStock: true, category: 'electronics' },
|
|
421
188
|
{ id: 2, name: 'Laptop', price: 999, inStock: false, category: 'electronics' },
|
|
422
|
-
{ id: 3, name: '
|
|
423
|
-
{ id: 4, name: 'Book', price: 29, inStock: true, category: 'books' }
|
|
189
|
+
{ id: 3, name: 'Book', price: 29, inStock: true, category: 'books' }
|
|
424
190
|
]);
|
|
425
|
-
|
|
426
|
-
// Filter by exact value
|
|
427
|
-
const inStockProducts = products.where({
|
|
428
|
-
inStock: equals(true)
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Filter by category
|
|
432
|
-
const electronics = products.where({
|
|
433
|
-
category: equals('electronics')
|
|
434
|
-
});
|
|
435
191
|
```
|
|
436
192
|
|
|
437
|
-
###
|
|
193
|
+
### Comparison
|
|
438
194
|
|
|
439
|
-
Filter helpers work seamlessly with observables - filters update automatically when observables change:
|
|
440
195
|
```javascript
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const filteredProducts = products.where({
|
|
447
|
-
price: between(minPrice, maxPrice), // Updates when minPrice or maxPrice change
|
|
448
|
-
name: includes(searchTerm) // Updates when searchTerm changes
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
// UI controls
|
|
452
|
-
const FiltersUI = Div([
|
|
453
|
-
Input({
|
|
454
|
-
type: 'number',
|
|
455
|
-
placeholder: 'Min price',
|
|
456
|
-
value: minPrice
|
|
457
|
-
}),
|
|
458
|
-
Input({
|
|
459
|
-
type: 'number',
|
|
460
|
-
placeholder: 'Max price',
|
|
461
|
-
value: maxPrice
|
|
462
|
-
}),
|
|
463
|
-
Input({
|
|
464
|
-
placeholder: 'Search products...',
|
|
465
|
-
value: searchTerm
|
|
466
|
-
})
|
|
467
|
-
]);
|
|
468
|
-
|
|
469
|
-
// Product list updates automatically
|
|
470
|
-
const ProductList = ForEachArray(filteredProducts, product =>
|
|
471
|
-
ProductCard(product)
|
|
472
|
-
);
|
|
196
|
+
products.where({ price: greaterThan(500) })
|
|
197
|
+
products.where({ price: lessThan(100) })
|
|
198
|
+
products.where({ price: between(200, 800) })
|
|
199
|
+
products.where({ inStock: equals(true) })
|
|
200
|
+
products.where({ name: notEquals('Phone') })
|
|
473
201
|
```
|
|
474
202
|
|
|
475
|
-
###
|
|
203
|
+
### String
|
|
476
204
|
|
|
477
|
-
#### Comparison Filters
|
|
478
205
|
```javascript
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const expensiveProducts = products.where({
|
|
485
|
-
price: gt(500) // price > 500
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
const affordableProducts = products.where({
|
|
489
|
-
price: lte(100) // price <= 100
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const notPhones = products.where({
|
|
493
|
-
name: neq('Phone') // name !== 'Phone'
|
|
494
|
-
});
|
|
495
|
-
```
|
|
496
|
-
|
|
497
|
-
#### Range Filters
|
|
498
|
-
```javascript
|
|
499
|
-
import { between } from 'native-document/utils/filters';
|
|
500
|
-
|
|
501
|
-
const midRangeProducts = products.where({
|
|
502
|
-
price: between(200, 800) // price >= 200 AND price <= 800
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// With reactive observables
|
|
506
|
-
const minPrice = Observable(100);
|
|
507
|
-
const maxPrice = Observable(500);
|
|
508
|
-
|
|
509
|
-
const rangeFiltered = products.where({
|
|
510
|
-
price: between(minPrice, maxPrice) // Updates when either observable changes
|
|
511
|
-
});
|
|
206
|
+
products.where({ name: includes('phone') }) // case-insensitive by default
|
|
207
|
+
products.where({ name: startsWith('P') })
|
|
208
|
+
products.where({ name: endsWith('book') })
|
|
209
|
+
products.where({ name: match(/^[A-Z]/) }) // regex
|
|
210
|
+
products.where({ name: match('lap', false) }) // plain string, no regex
|
|
512
211
|
```
|
|
513
212
|
|
|
514
|
-
|
|
515
|
-
```javascript
|
|
516
|
-
import { includes, startsWith, endsWith, match } from 'native-document/utils/filters';
|
|
517
|
-
|
|
518
|
-
// Contains (case-insensitive by default)
|
|
519
|
-
const searchResults = products.where({
|
|
520
|
-
name: includes('phone') // Matches 'Phone', 'PHONE', 'phone', 'Smartphone'
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
// Starts with
|
|
524
|
-
const pProducts = products.where({
|
|
525
|
-
name: startsWith('P') // Phone, Pencil, etc.
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
// Ends with
|
|
529
|
-
const bookProducts = products.where({
|
|
530
|
-
name: endsWith('book') // Textbook, Handbook, etc.
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
// Regex pattern matching
|
|
534
|
-
const alphaNumeric = products.where({
|
|
535
|
-
sku: match(/^[A-Z]{3}-\d{3}$/) // Matches 'ABC-123' pattern
|
|
536
|
-
});
|
|
213
|
+
### Array membership
|
|
537
214
|
|
|
538
|
-
// Simple text search (no regex)
|
|
539
|
-
const simpleSearch = products.where({
|
|
540
|
-
name: match('lap', false) // false = not regex, just contains
|
|
541
|
-
});
|
|
542
|
-
```
|
|
543
|
-
|
|
544
|
-
#### Array Filters
|
|
545
215
|
```javascript
|
|
546
|
-
|
|
216
|
+
const allowed = Observable.array(['electronics', 'books']);
|
|
547
217
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const filteredByCategory = products.where({
|
|
551
|
-
category: inArray(allowedCategories) // category in ['electronics', 'books']
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
const excludedCategories = ['clothing', 'food'];
|
|
555
|
-
const nonExcluded = products.where({
|
|
556
|
-
category: notIn(excludedCategories)
|
|
557
|
-
});
|
|
218
|
+
products.where({ category: inArray(allowed) }) // reactive
|
|
219
|
+
products.where({ category: notIn(['clothing']) })
|
|
558
220
|
```
|
|
559
221
|
|
|
560
|
-
|
|
561
|
-
```javascript
|
|
562
|
-
import { isEmpty, isNotEmpty } from 'native-document/utils/filters';
|
|
222
|
+
### Reactive filters
|
|
563
223
|
|
|
564
|
-
|
|
565
|
-
{ title: 'Task 1', description: '' },
|
|
566
|
-
{ title: 'Task 2', description: 'Details here' }
|
|
567
|
-
]);
|
|
224
|
+
Pass an observable as the filter value - re-filters automatically when it changes:
|
|
568
225
|
|
|
569
|
-
const tasksWithDescription = tasks.where({
|
|
570
|
-
description: isNotEmpty() // Has a description
|
|
571
|
-
});
|
|
572
|
-
|
|
573
|
-
const tasksWithoutDescription = tasks.where({
|
|
574
|
-
description: isEmpty() // Empty or null description
|
|
575
|
-
});
|
|
576
|
-
```
|
|
577
|
-
|
|
578
|
-
### Combining Filters with Logic
|
|
579
226
|
```javascript
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
const premiumElectronics = products.where({
|
|
584
|
-
category: equals('electronics'),
|
|
585
|
-
price: gt(500)
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// OR: Any condition can be true
|
|
589
|
-
const dealsOrPopular = products.where({
|
|
590
|
-
price: or(
|
|
591
|
-
lt(50), // Price < 50
|
|
592
|
-
gte(1000) // OR rating >= 1000
|
|
593
|
-
)
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// NOT: Invert condition
|
|
597
|
-
const notInStock = products.where({
|
|
598
|
-
inStock: not(equals(true)) // NOT in stock
|
|
599
|
-
});
|
|
227
|
+
const search = Observable('');
|
|
228
|
+
const minPrice = Observable(0);
|
|
229
|
+
const maxPrice = Observable(1000);
|
|
600
230
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
const complexFilter = products.where({
|
|
606
|
-
category: and(
|
|
607
|
-
or(
|
|
608
|
-
equals('all'), // Show all categories
|
|
609
|
-
equals(selectedCategory) // OR match selected category
|
|
610
|
-
),
|
|
611
|
-
includes(searchQuery) // AND name includes search term
|
|
612
|
-
)
|
|
231
|
+
const filtered = products.where({
|
|
232
|
+
name: includes(search),
|
|
233
|
+
price: between(minPrice, maxPrice)
|
|
613
234
|
});
|
|
614
235
|
```
|
|
615
236
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
### Use ForEachArray When:
|
|
619
|
-
|
|
620
|
-
✅ **Working with arrays of complex objects**
|
|
621
|
-
✅ **Performance is critical** - Large lists, frequent updates
|
|
622
|
-
✅ **Using array methods** - push, pop, splice, sort, reverse, etc.
|
|
623
|
-
|
|
624
|
-
```javascript
|
|
625
|
-
// Perfect for ForEachArray
|
|
626
|
-
const comments = Observable.array([...]);
|
|
627
|
-
const CommentList = ForEachArray(comments, comment => CommentComponent(comment));
|
|
628
|
-
|
|
629
|
-
// Array operations work optimally
|
|
630
|
-
comments.push(newComment);
|
|
631
|
-
|
|
632
|
-
comments.splice(index, 1);
|
|
633
|
-
|
|
634
|
-
comments.sort((a, b) => b.timestamp - a.timestamp);
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
### Use ForEach When:
|
|
638
|
-
|
|
639
|
-
✅ **Working with objects** - ForEach is required for object iteration
|
|
640
|
-
✅ **Mixed data types** - When data might be array or object
|
|
641
|
-
✅ **Arrays of primitive values** - string, number, boolean
|
|
642
|
-
✅ **Simple use cases** - Small lists with infrequent updates
|
|
643
|
-
|
|
644
|
-
## Configuration Options
|
|
237
|
+
### Custom filter
|
|
645
238
|
|
|
646
|
-
### ForEach Configuration
|
|
647
|
-
|
|
648
|
-
`ForEach` accepts an optional configuration object:
|
|
649
239
|
```javascript
|
|
650
|
-
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
**shouldKeepItemsInCache**: When `true`, keeps rendered items in cache even when removed from the list. Useful for frequently toggling items visibility.
|
|
654
|
-
```javascript
|
|
655
|
-
const items = Observable.array(['A', 'B', 'C']);
|
|
656
|
-
|
|
657
|
-
// Items stay in cache when removed
|
|
658
|
-
const list = ForEach(items, item => Div(item), null, {
|
|
659
|
-
shouldKeepItemsInCache: true
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
items.splice(1, 1); // Removes 'B' from DOM but keeps it cached
|
|
663
|
-
items.push('B'); // Re-adds 'B' without re-rendering
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
### ForEachArray Configuration
|
|
240
|
+
const minRating = Observable(4);
|
|
667
241
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
242
|
+
products.where({
|
|
243
|
+
_: custom((product, min) => {
|
|
244
|
+
return product.rating >= min && product.reviews > 10;
|
|
245
|
+
}, minRating) // observables passed as extra args
|
|
672
246
|
})
|
|
673
247
|
```
|
|
674
248
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
**pushDelay**: Function that returns delay in milliseconds for batch operations. Useful for large datasets.
|
|
678
|
-
```javascript
|
|
679
|
-
const bigList = Observable.array([]);
|
|
680
|
-
|
|
681
|
-
const list = ForEachArray(bigList, item => Div(item), {
|
|
682
|
-
pushDelay: (items) => {
|
|
683
|
-
// Add delay for large batches
|
|
684
|
-
return items.length > 100 ? 50 : 0;
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// Adding 500 items will be throttled
|
|
689
|
-
bigList.push(...Array(500).fill().map((_, i) => ({ id: i, text: `Item ${i}` })));
|
|
690
|
-
```
|
|
691
|
-
## Real-World Examples
|
|
692
|
-
|
|
693
|
-
### Nested Lists with Mixed Rendering
|
|
249
|
+
### Combining
|
|
694
250
|
|
|
695
251
|
```javascript
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
id: 1,
|
|
699
|
-
name: 'Electronics',
|
|
700
|
-
items: Observable.array([
|
|
701
|
-
{ id: 101, name: 'Smartphone', price: 599 },
|
|
702
|
-
{ id: 102, name: 'Laptop', price: 999 }
|
|
703
|
-
])
|
|
704
|
-
},
|
|
705
|
-
{
|
|
706
|
-
id: 2,
|
|
707
|
-
name: 'Books',
|
|
708
|
-
items: Observable.array([
|
|
709
|
-
{ id: 201, name: 'JavaScript Guide', price: 29.99 },
|
|
710
|
-
{ id: 202, name: 'Design Patterns', price: 39.99 }
|
|
711
|
-
])
|
|
712
|
-
}
|
|
713
|
-
]);
|
|
714
|
-
|
|
715
|
-
const CategorizedProducts = Div({ class: 'product-categories' }, [
|
|
716
|
-
// Categories use ForEachArray (it's an array)
|
|
717
|
-
ForEachArray(categories, category =>
|
|
718
|
-
Div({ class: 'category' }, [
|
|
719
|
-
H3({ class: 'category-title' }, category.name),
|
|
720
|
-
|
|
721
|
-
// Items within each category also use ForEachArray
|
|
722
|
-
ForEachArray(category.items,
|
|
723
|
-
item => Div({ class: 'product-item' }, [
|
|
724
|
-
Span({ class: 'product-name' }, item.name),
|
|
725
|
-
Span({ class: 'product-price' }, `$${item.price}`),
|
|
726
|
-
Button('Add to Cart').nd.onClick(() => addToCart(item))
|
|
727
|
-
]),
|
|
728
|
-
),
|
|
729
|
-
|
|
730
|
-
Button('Add Item').nd.onClick(() => {
|
|
731
|
-
const newItem = {
|
|
732
|
-
id: Date.now(),
|
|
733
|
-
name: `New ${category.name} Item`,
|
|
734
|
-
price: Math.floor(Math.random() * 100) + 10
|
|
735
|
-
};
|
|
736
|
-
category.items.push(newItem);
|
|
737
|
-
})
|
|
738
|
-
])
|
|
739
|
-
)
|
|
740
|
-
]);
|
|
741
|
-
```
|
|
252
|
+
// and - field must pass ALL conditions
|
|
253
|
+
products.where({ price: and(greaterThan(100), lessThan(500)) })
|
|
742
254
|
|
|
743
|
-
|
|
255
|
+
// or - field must pass AT LEAST ONE
|
|
256
|
+
products.where({ category: or(equals('electronics'), equals('books')) })
|
|
744
257
|
|
|
745
|
-
|
|
258
|
+
// not - invert
|
|
259
|
+
products.where({ inStock: not(equals(true)) })
|
|
746
260
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
ForEach(tags, tag => TagComponent(tag), 'index')
|
|
751
|
-
```
|
|
752
|
-
|
|
753
|
-
### 2. Choose the Right Function
|
|
754
|
-
|
|
755
|
-
```javascript
|
|
756
|
-
// ✅ Perfect: ForEachArray for complex objects
|
|
757
|
-
const users = Observable.array([{id: 1, name: 'Alice'}]);
|
|
758
|
-
ForEachArray(users, renderUser, 'id')
|
|
759
|
-
|
|
760
|
-
// ✅ Correct: ForEach for primitives
|
|
761
|
-
const tags = Observable.array(['js', 'css']);
|
|
762
|
-
ForEach(tags, renderTag)
|
|
763
|
-
|
|
764
|
-
// ✅ Correct: ForEach for objects
|
|
765
|
-
const config = Observable({theme: 'dark'});
|
|
766
|
-
ForEach(config, renderSetting, (item, key) => key)
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
### 3. Use Computed Values for Derived Lists
|
|
770
|
-
|
|
771
|
-
```javascript
|
|
772
|
-
// ✅ Efficient: Computed filtered list
|
|
773
|
-
const searchTerm = Observable('');
|
|
774
|
-
const filteredItems = Observable.computed(() =>
|
|
775
|
-
allItems.val().filter(item =>
|
|
776
|
-
item.name.toLowerCase().includes(searchTerm.val().toLowerCase())
|
|
777
|
-
),
|
|
778
|
-
[allItems, searchTerm]
|
|
779
|
-
);
|
|
780
|
-
|
|
781
|
-
ForEachArray(filteredItems, renderItem);
|
|
782
|
-
|
|
783
|
-
// ❌ Inefficient: Filtering in render
|
|
784
|
-
ForEachArray(allItems, item => {
|
|
785
|
-
if (item.name.includes(searchTerm.val())) {
|
|
786
|
-
return renderItem(item);
|
|
787
|
-
}
|
|
788
|
-
return null;
|
|
261
|
+
// cross-field - use _ key with plain function
|
|
262
|
+
products.where({
|
|
263
|
+
_: item => item.inStock && item.price < 500
|
|
789
264
|
})
|
|
790
265
|
```
|
|
791
266
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
Both `ForEach` and `ForEachArray` automatically manage memory:
|
|
267
|
+
### Date and time filters
|
|
795
268
|
|
|
796
269
|
```javascript
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
let listComponent = ForEachArray(myList, item => Div(item));
|
|
800
|
-
|
|
801
|
-
// When references are lost, cleanup happens automatically
|
|
802
|
-
myList = null;
|
|
803
|
-
listComponent = null; // Memory will be freed
|
|
804
|
-
```
|
|
270
|
+
import { dateEquals, dateBefore, dateAfter, dateBetween,
|
|
271
|
+
timeBefore, timeBetween } from 'native-document/filters';
|
|
805
272
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
const listComponent = ForEachArray(items, renderItem);
|
|
273
|
+
const events = Observable.array([
|
|
274
|
+
{ name: 'Meeting', date: '2024-03-15' },
|
|
275
|
+
{ name: 'Conference', date: '2024-06-20' }
|
|
276
|
+
]);
|
|
811
277
|
|
|
812
|
-
|
|
813
|
-
|
|
278
|
+
events.where({ date: dateAfter(new Date()) })
|
|
279
|
+
events.where({ date: dateBetween('2024-06-01', '2024-08-31') })
|
|
280
|
+
events.where({ date: timeBetween(
|
|
281
|
+
new Date('2024-01-01 09:00:00'),
|
|
282
|
+
new Date('2024-01-01 17:00:00')
|
|
283
|
+
)})
|
|
814
284
|
```
|
|
815
285
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
### 1. Missing Keys with Complex Objects
|
|
286
|
+
---
|
|
819
287
|
|
|
820
|
-
|
|
821
|
-
// ❌ Problem: No key, inefficient updates
|
|
822
|
-
ForEachArray(users, user => UserProfile(user))
|
|
823
|
-
|
|
824
|
-
// ✅ Solution: Use unique key
|
|
825
|
-
ForEachArray(users, user => UserProfile(user))
|
|
826
|
-
```
|
|
288
|
+
## Common Patterns
|
|
827
289
|
|
|
828
|
-
###
|
|
290
|
+
### Empty state
|
|
829
291
|
|
|
830
292
|
```javascript
|
|
831
|
-
|
|
832
|
-
ForEach(genericObject, renderItem)
|
|
293
|
+
const items = Observable.array([]);
|
|
833
294
|
|
|
834
|
-
|
|
835
|
-
|
|
295
|
+
Div([
|
|
296
|
+
ShowIf(items.isEmpty(), Div({ class: 'empty' }, 'No items yet')),
|
|
297
|
+
ForEachArray(items, item => ItemComponent(item))
|
|
298
|
+
])
|
|
836
299
|
```
|
|
837
300
|
|
|
838
|
-
###
|
|
301
|
+
### Search + filter
|
|
839
302
|
|
|
840
303
|
```javascript
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
// ✅ Correct: Use Observable array methods
|
|
845
|
-
items.push(newItem);
|
|
846
|
-
|
|
847
|
-
// ✅ Also correct: Set new array
|
|
848
|
-
items.set([...items.val(), newItem]);
|
|
849
|
-
```
|
|
304
|
+
const search = Observable('');
|
|
305
|
+
const category = Observable('all');
|
|
850
306
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
307
|
+
const filtered = products.where({
|
|
308
|
+
name: includes(search),
|
|
309
|
+
category: or(equals('all'), equals(category))
|
|
310
|
+
});
|
|
854
311
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const ItemList = Div([
|
|
860
|
-
ShowIf(showEmptyState,
|
|
861
|
-
Div({ class: 'empty-state' }, 'No items found')
|
|
862
|
-
),
|
|
863
|
-
HideIf(showEmptyState,
|
|
864
|
-
ForEachArray(items, item => ItemComponent(item))
|
|
865
|
-
)
|
|
866
|
-
]);
|
|
312
|
+
Div([
|
|
313
|
+
Input({ placeholder: 'Search...', value: search }),
|
|
314
|
+
ForEachArray(filtered, product => ProductCard(product))
|
|
315
|
+
])
|
|
867
316
|
```
|
|
868
317
|
|
|
869
|
-
###
|
|
318
|
+
### Drag-and-drop reordering
|
|
870
319
|
|
|
871
320
|
```javascript
|
|
872
|
-
|
|
873
|
-
{ name: 'firstName', label: 'First Name', value: '', required: true },
|
|
874
|
-
{ name: 'lastName', label: 'Last Name', value: '', required: true },
|
|
875
|
-
{ name: 'email', label: 'Email', value: '', required: true }
|
|
876
|
-
]);
|
|
321
|
+
let draggedIndex = null;
|
|
877
322
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
Button({ type: 'submit' }, 'Submit')
|
|
893
|
-
]);
|
|
323
|
+
ForEachArray(items, (item, index) =>
|
|
324
|
+
Div({ class: 'item', draggable: true }, item.text)
|
|
325
|
+
.nd
|
|
326
|
+
.onDragStart(() => { draggedIndex = index.$value; })
|
|
327
|
+
.onDragOver(e => e.preventDefault())
|
|
328
|
+
.onDrop(e => {
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
const dropIndex = index.$value;
|
|
331
|
+
if (draggedIndex !== null && draggedIndex !== dropIndex) {
|
|
332
|
+
items.swap(draggedIndex, dropIndex);
|
|
333
|
+
}
|
|
334
|
+
draggedIndex = null;
|
|
335
|
+
})
|
|
336
|
+
)
|
|
894
337
|
```
|
|
895
338
|
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
### Infinite Scrolling
|
|
339
|
+
### Infinite scroll
|
|
899
340
|
|
|
900
341
|
```javascript
|
|
901
|
-
const items
|
|
342
|
+
const items = Observable.array([]);
|
|
902
343
|
const isLoading = Observable(false);
|
|
903
|
-
const hasMore
|
|
344
|
+
const hasMore = Observable(true);
|
|
904
345
|
|
|
905
|
-
const
|
|
346
|
+
const loadMore = async () => {
|
|
906
347
|
if (isLoading.val()) return;
|
|
907
|
-
|
|
908
348
|
isLoading.set(true);
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
hasMore.set(false);
|
|
913
|
-
} else {
|
|
914
|
-
items.push(...newItems);
|
|
915
|
-
}
|
|
916
|
-
} finally {
|
|
917
|
-
isLoading.set(false);
|
|
918
|
-
}
|
|
349
|
+
const next = await fetchItems(items.val().length);
|
|
350
|
+
next.length ? items.merge(next) : hasMore.set(false);
|
|
351
|
+
isLoading.set(false);
|
|
919
352
|
};
|
|
920
353
|
|
|
921
|
-
|
|
354
|
+
Div([
|
|
922
355
|
ForEachArray(items, item => ItemComponent(item)),
|
|
923
|
-
ShowIf(isLoading,
|
|
924
|
-
ShowIf(hasMore.
|
|
925
|
-
Button('Load
|
|
926
|
-
)
|
|
927
|
-
]);
|
|
928
|
-
|
|
929
|
-
```
|
|
930
|
-
|
|
931
|
-
### Drag and Drop Reordering
|
|
932
|
-
|
|
933
|
-
```javascript
|
|
934
|
-
const draggableItems = Observable.array([
|
|
935
|
-
{ id: 1, text: 'Item 1' },
|
|
936
|
-
{ id: 2, text: 'Item 2' },
|
|
937
|
-
{ id: 3, text: 'Item 3' }
|
|
938
|
-
]);
|
|
939
|
-
|
|
940
|
-
let draggedIndex = null;
|
|
941
|
-
|
|
942
|
-
const DraggableList = Div([
|
|
943
|
-
ForEachArray(draggableItems, (item, indexObservable) =>
|
|
944
|
-
Div({
|
|
945
|
-
class: 'draggable-item',
|
|
946
|
-
draggable: true
|
|
947
|
-
}, item.text)
|
|
948
|
-
.nd.onDragStart((e) => {
|
|
949
|
-
draggedIndex = indexObservable.val();
|
|
950
|
-
})
|
|
951
|
-
.nd.onDragOver((e) => e.preventDefault())
|
|
952
|
-
.nd.onDrop((e) => {
|
|
953
|
-
e.preventDefault();
|
|
954
|
-
const dropIndex = indexObservable.val();
|
|
955
|
-
if (draggedIndex !== null && draggedIndex !== dropIndex) {
|
|
956
|
-
const items = draggableItems.val();
|
|
957
|
-
const draggedItem = items[draggedIndex];
|
|
958
|
-
|
|
959
|
-
// Remove from old position
|
|
960
|
-
items.splice(draggedIndex, 1);
|
|
961
|
-
// Insert at new position
|
|
962
|
-
items.splice(dropIndex, 0, draggedItem);
|
|
963
|
-
|
|
964
|
-
draggableItems.set([...items]);
|
|
965
|
-
}
|
|
966
|
-
draggedIndex = null;
|
|
967
|
-
}),
|
|
968
|
-
'id'
|
|
356
|
+
ShowIf(isLoading.isTruthy(), Div('Loading...')),
|
|
357
|
+
ShowIf(hasMore.isTruthy(),
|
|
358
|
+
Button('Load more').nd.onClick(loadMore)
|
|
969
359
|
)
|
|
970
|
-
])
|
|
360
|
+
])
|
|
971
361
|
```
|
|
972
362
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
### Logging Updates
|
|
363
|
+
---
|
|
976
364
|
|
|
977
|
-
|
|
978
|
-
const items = Observable.array([]);
|
|
365
|
+
## Best Practices
|
|
979
366
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
367
|
+
1. Use `ForEachArray` for arrays of objects - it's optimized for that case
|
|
368
|
+
2. Use `ForEach` for objects (non-array) and arrays of primitives
|
|
369
|
+
3. Always pass a key when items can be reordered - avoids unnecessary re-rendering
|
|
370
|
+
4. Use `.where()` for filtering instead of filtering inside the callback
|
|
371
|
+
5. Use `shouldKeepItemsInCache: true` when items toggle in and out frequently
|
|
372
|
+
6. Never mutate `.val()` directly - use observable array methods (`push`, `splice`, `swap`, etc.)
|
|
986
373
|
|
|
987
|
-
|
|
988
|
-
console.log('Rendering item:', item, 'at index:', index?.val());
|
|
989
|
-
return ItemComponent(item);
|
|
990
|
-
});
|
|
991
|
-
```
|
|
374
|
+
---
|
|
992
375
|
|
|
993
376
|
## Next Steps
|
|
994
377
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
- **[
|
|
998
|
-
- **[
|
|
999
|
-
- **[State Management](state-management.md)** - Global state patterns
|
|
1000
|
-
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
1001
|
-
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
1002
|
-
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
1003
|
-
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
1004
|
-
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
1005
|
-
- **[State Management](state-management.md)** - Managing application state
|
|
1006
|
-
- **[Memory Management](memory-management.md)** - Understanding cleanup and memory
|
|
378
|
+
- **[Conditional Rendering](./conditional-rendering.md)** - ShowIf, Match, Switch
|
|
379
|
+
- **[Observables](./observables.md)** - Observable arrays and `.where()` in depth
|
|
380
|
+
- **[Advanced Components](./advanced-components.md)** - `ForEachArray` with `useCache`
|
|
381
|
+
- **[Filters](./filters.md)** - Full filter reference
|
|
1007
382
|
|
|
1008
383
|
## Utilities
|
|
1009
384
|
|
|
1010
|
-
- **[Cache](
|
|
1011
|
-
- **[NativeFetch](
|
|
1012
|
-
- **[Filters](
|
|
385
|
+
- **[Cache](./cache.md)** - Lazy initialization and singleton patterns
|
|
386
|
+
- **[NativeFetch](./native-fetch.md)** - HTTP client with interceptors
|
|
387
|
+
- **[Filters](./filters.md)** - Data filtering helpers
|