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
package/docs/getting-started.md
CHANGED
|
@@ -200,11 +200,12 @@ const addTodo = () => {
|
|
|
200
200
|
if (!newTodo.val().trim()) {
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
|
-
|
|
203
|
+
const todo = Observable.object({
|
|
204
204
|
id: Date.now(),
|
|
205
205
|
text: newTodo.val().trim(),
|
|
206
206
|
done: false
|
|
207
207
|
});
|
|
208
|
+
todos.push(todo);
|
|
208
209
|
newTodo.set('');
|
|
209
210
|
};
|
|
210
211
|
|
|
@@ -229,14 +230,14 @@ const TodoApp = Div({ class: 'todo-app' }, [
|
|
|
229
230
|
Div({ class: 'todo-item' }, [
|
|
230
231
|
Input({
|
|
231
232
|
type: 'checkbox',
|
|
232
|
-
checked:
|
|
233
|
+
checked: todo.done
|
|
233
234
|
}).nd.onChange((e) => {
|
|
234
235
|
const todoList = todos.val();
|
|
235
236
|
todoList[index.val()].done = e.target.checked;
|
|
236
237
|
todos.set([...todoList]);
|
|
237
238
|
}),
|
|
238
239
|
|
|
239
|
-
Div(['Task: ', todo.text]),
|
|
240
|
+
Div({ class: todo.done.check(d => d ? 'completed' : '') }, ['Task: ', todo.text]),
|
|
240
241
|
|
|
241
242
|
Button('Delete').nd.onClick(() => {
|
|
242
243
|
todos.splice(index.val(), 1);
|
|
@@ -360,10 +361,18 @@ Now that you've built your first NativeDocument applications, explore these topi
|
|
|
360
361
|
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
361
362
|
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
362
363
|
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
364
|
+
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
363
365
|
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
364
366
|
- **[Memory Management](memory-management.md)** - Memory management
|
|
365
367
|
- **[Anchor](anchor.md)** - Anchor
|
|
366
368
|
|
|
369
|
+
## Utilities
|
|
370
|
+
|
|
371
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
372
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
373
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|
|
374
|
+
|
|
375
|
+
|
|
367
376
|
## Common Issues
|
|
368
377
|
|
|
369
378
|
### Import Errors
|
package/docs/lifecycle-events.md
CHANGED
|
@@ -54,8 +54,8 @@ const timer = Div("Timer: 0")
|
|
|
54
54
|
|
|
55
55
|
```javascript
|
|
56
56
|
const focusInput = Input({ placeholder: "Auto-focused" })
|
|
57
|
-
.nd.mounted(
|
|
58
|
-
|
|
57
|
+
.nd.mounted(element => {
|
|
58
|
+
Òelement.focus();
|
|
59
59
|
});
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -100,10 +100,18 @@ Now that you understand lifecycle events, explore these related topics:
|
|
|
100
100
|
|
|
101
101
|
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
102
102
|
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
103
|
+
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
103
104
|
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
104
105
|
- **[Memory Management](memory-management.md)** - Memory management
|
|
105
106
|
- **[Anchor](anchor.md)** - Anchor
|
|
106
107
|
|
|
108
|
+
## Utilities
|
|
109
|
+
|
|
110
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
111
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
112
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|
|
113
|
+
|
|
114
|
+
|
|
107
115
|
|
|
108
116
|
|
|
109
117
|
|
package/docs/list-rendering.md
CHANGED
|
@@ -77,7 +77,7 @@ const tasks = Observable.array([
|
|
|
77
77
|
const TaskList = Ol([
|
|
78
78
|
ForEach(tasks, (task, indexObservable) =>
|
|
79
79
|
Li([
|
|
80
|
-
Strong(indexObservable.
|
|
80
|
+
Strong(indexObservable.check(val => val + 1)),
|
|
81
81
|
' ',
|
|
82
82
|
task,
|
|
83
83
|
Button('Remove').nd.onClick(() =>
|
|
@@ -179,7 +179,7 @@ const PlaylistView = Div({ class: 'playlist' }, [
|
|
|
179
179
|
}),
|
|
180
180
|
Button('↓').nd.onClick(() => {
|
|
181
181
|
const index = indexObservable.$value;
|
|
182
|
-
if(index < playlist.length
|
|
182
|
+
if(index < playlist.length -1) {
|
|
183
183
|
playlist.swap(index, index+1);
|
|
184
184
|
}
|
|
185
185
|
}),
|
|
@@ -207,59 +207,410 @@ const PlaylistView = Div({ class: 'playlist' }, [
|
|
|
207
207
|
]);
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
###
|
|
210
|
+
### Date and Time Filters
|
|
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
|
+
});
|
|
242
|
+
|
|
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
|
+
```javascript
|
|
255
|
+
import { custom } from 'native-document/utils/filters';
|
|
256
|
+
|
|
257
|
+
const minRating = Observable(4);
|
|
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']);
|
|
211
363
|
|
|
212
|
-
|
|
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
|
+
]);
|
|
213
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
|
+
]);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### merge() - vBatch Add Items
|
|
392
|
+
|
|
393
|
+
Add multiple items efficiently:
|
|
394
|
+
```javascript
|
|
395
|
+
const items = Observable.array([1, 2, 3]);
|
|
396
|
+
|
|
397
|
+
// Add multiple items at once
|
|
398
|
+
items.merge([4, 5, 6]); // Result: [1, 2, 3, 4, 5, 6]
|
|
399
|
+
|
|
400
|
+
// More efficient than multiple push calls
|
|
401
|
+
// items.push(4); items.push(5); items.push(6); // Less efficient
|
|
402
|
+
````
|
|
403
|
+
|
|
404
|
+
## Advanced Filtering with where()
|
|
405
|
+
|
|
406
|
+
The `where()` method creates filtered observable arrays using powerful filter helpers that handle both static values and reactive observables.
|
|
407
|
+
|
|
408
|
+
### Import Filter Helpers
|
|
409
|
+
```javascript
|
|
410
|
+
// Direct import of specific helpers
|
|
411
|
+
import { equals, greaterThan, between, includes, and, or, not } from 'native-document/utils/filters';
|
|
412
|
+
|
|
413
|
+
// Or import all filters
|
|
414
|
+
import * as Filters from 'native-document/utils/filters';
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Basic Filtering with Helpers
|
|
214
418
|
```javascript
|
|
215
419
|
const products = Observable.array([
|
|
216
|
-
{
|
|
217
|
-
{
|
|
218
|
-
{
|
|
420
|
+
{ id: 1, name: 'Phone', price: 599, inStock: true, category: 'electronics' },
|
|
421
|
+
{ id: 2, name: 'Laptop', price: 999, inStock: false, category: 'electronics' },
|
|
422
|
+
{ id: 3, name: 'Tablet', price: 399, inStock: true, category: 'electronics' },
|
|
423
|
+
{ id: 4, name: 'Book', price: 29, inStock: true, category: 'books' }
|
|
219
424
|
]);
|
|
220
425
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
```
|
|
436
|
+
|
|
437
|
+
### Reactive Filtering with Observables
|
|
438
|
+
|
|
439
|
+
Filter helpers work seamlessly with observables - filters update automatically when observables change:
|
|
440
|
+
```javascript
|
|
441
|
+
const minPrice = Observable(0);
|
|
442
|
+
const maxPrice = Observable(1000);
|
|
443
|
+
const searchTerm = Observable('');
|
|
444
|
+
|
|
445
|
+
// Reactive filters using observables
|
|
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
|
+
})
|
|
230
467
|
]);
|
|
231
468
|
|
|
469
|
+
// Product list updates automatically
|
|
470
|
+
const ProductList = ForEachArray(filteredProducts, product =>
|
|
471
|
+
ProductCard(product)
|
|
472
|
+
);
|
|
232
473
|
```
|
|
233
474
|
|
|
234
|
-
###
|
|
475
|
+
### Available Filter Helpers
|
|
235
476
|
|
|
236
|
-
|
|
477
|
+
#### Comparison Filters
|
|
478
|
+
```javascript
|
|
479
|
+
import { equals, notEquals, greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual } from 'native-document/utils/filters';
|
|
480
|
+
|
|
481
|
+
// Or use shortcuts
|
|
482
|
+
import { eq, neq, gt, gte, lt, lte } from 'native-document/utils/filters';
|
|
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
|
+
});
|
|
237
491
|
|
|
492
|
+
const notPhones = products.where({
|
|
493
|
+
name: neq('Phone') // name !== 'Phone'
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
#### Range Filters
|
|
238
498
|
```javascript
|
|
239
|
-
|
|
240
|
-
id: i,
|
|
241
|
-
value: `Item ${i}`,
|
|
242
|
-
category: Math.floor(i / 100)
|
|
243
|
-
})));
|
|
499
|
+
import { between } from 'native-document/utils/filters';
|
|
244
500
|
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
)
|
|
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
|
+
});
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
#### String Filters
|
|
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
|
+
});
|
|
537
|
+
|
|
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
|
+
```javascript
|
|
546
|
+
import { inArray, notIn } from 'native-document/utils/filters';
|
|
547
|
+
|
|
548
|
+
const allowedCategories = Observable.array(['electronics', 'books']);
|
|
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
|
+
});
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### Empty/Existence Filters
|
|
561
|
+
```javascript
|
|
562
|
+
import { isEmpty, isNotEmpty } from 'native-document/utils/filters';
|
|
563
|
+
|
|
564
|
+
const tasks = Observable.array([
|
|
565
|
+
{ title: 'Task 1', description: '' },
|
|
566
|
+
{ title: 'Task 2', description: 'Details here' }
|
|
255
567
|
]);
|
|
256
568
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
+
```javascript
|
|
580
|
+
import { and, or, not, equals, gt, lt, gte } from 'native-document/utils/filters';
|
|
581
|
+
|
|
582
|
+
// AND: All conditions must be true
|
|
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
|
+
});
|
|
600
|
+
|
|
601
|
+
// Complex combinations
|
|
602
|
+
const searchQuery = Observable('');
|
|
603
|
+
const selectedCategory = Observable('all');
|
|
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
|
+
)
|
|
613
|
+
});
|
|
263
614
|
```
|
|
264
615
|
|
|
265
616
|
## Choosing Between ForEach and ForEachArray
|
|
@@ -290,6 +641,53 @@ comments.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
290
641
|
✅ **Arrays of primitive values** - string, number, boolean
|
|
291
642
|
✅ **Simple use cases** - Small lists with infrequent updates
|
|
292
643
|
|
|
644
|
+
## Configuration Options
|
|
645
|
+
|
|
646
|
+
### ForEach Configuration
|
|
647
|
+
|
|
648
|
+
`ForEach` accepts an optional configuration object:
|
|
649
|
+
```javascript
|
|
650
|
+
ForEach(data, callback, key, { shouldKeepItemsInCache: false })
|
|
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
|
|
667
|
+
|
|
668
|
+
`ForEachArray` accepts a configuration object:
|
|
669
|
+
```javascript
|
|
670
|
+
ForEachArray(data, callback, {
|
|
671
|
+
shouldKeepItemsInCache: false
|
|
672
|
+
})
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
**shouldKeepItemsInCache**: Same as ForEach - keeps items in cache when removed.
|
|
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
|
+
```
|
|
293
691
|
## Real-World Examples
|
|
294
692
|
|
|
295
693
|
### Nested Lists with Mixed Rendering
|
|
@@ -327,7 +725,6 @@ const CategorizedProducts = Div({ class: 'product-categories' }, [
|
|
|
327
725
|
Span({ class: 'product-price' }, `$${item.price}`),
|
|
328
726
|
Button('Add to Cart').nd.onClick(() => addToCart(item))
|
|
329
727
|
]),
|
|
330
|
-
'id'
|
|
331
728
|
),
|
|
332
729
|
|
|
333
730
|
Button('Add Item').nd.onClick(() => {
|
|
@@ -338,8 +735,7 @@ const CategorizedProducts = Div({ class: 'product-categories' }, [
|
|
|
338
735
|
};
|
|
339
736
|
category.items.push(newItem);
|
|
340
737
|
})
|
|
341
|
-
])
|
|
342
|
-
'id'
|
|
738
|
+
])
|
|
343
739
|
)
|
|
344
740
|
]);
|
|
345
741
|
```
|
|
@@ -350,11 +746,8 @@ const CategorizedProducts = Div({ class: 'product-categories' }, [
|
|
|
350
746
|
|
|
351
747
|
```javascript
|
|
352
748
|
// ✅ Good: Efficient updates and reordering
|
|
353
|
-
ForEachArray(users, user => UserCard(user), 'id')
|
|
354
|
-
ForEach(tags, tag => TagComponent(tag), 'index')
|
|
355
|
-
|
|
356
|
-
// ❌ Poor: Inefficient, may cause unnecessary re-renders
|
|
357
749
|
ForEachArray(users, user => UserCard(user))
|
|
750
|
+
ForEach(tags, tag => TagComponent(tag), 'index')
|
|
358
751
|
```
|
|
359
752
|
|
|
360
753
|
### 2. Choose the Right Function
|
|
@@ -385,7 +778,7 @@ const filteredItems = Observable.computed(() =>
|
|
|
385
778
|
[allItems, searchTerm]
|
|
386
779
|
);
|
|
387
780
|
|
|
388
|
-
ForEachArray(filteredItems, renderItem
|
|
781
|
+
ForEachArray(filteredItems, renderItem);
|
|
389
782
|
|
|
390
783
|
// ❌ Inefficient: Filtering in render
|
|
391
784
|
ForEachArray(allItems, item => {
|
|
@@ -429,7 +822,7 @@ items.cleanup();
|
|
|
429
822
|
ForEachArray(users, user => UserProfile(user))
|
|
430
823
|
|
|
431
824
|
// ✅ Solution: Use unique key
|
|
432
|
-
ForEachArray(users, user => UserProfile(user)
|
|
825
|
+
ForEachArray(users, user => UserProfile(user))
|
|
433
826
|
```
|
|
434
827
|
|
|
435
828
|
### 2. Using ForEach for Complex Arrays
|
|
@@ -468,7 +861,7 @@ const ItemList = Div([
|
|
|
468
861
|
Div({ class: 'empty-state' }, 'No items found')
|
|
469
862
|
),
|
|
470
863
|
HideIf(showEmptyState,
|
|
471
|
-
ForEachArray(items, item => ItemComponent(item)
|
|
864
|
+
ForEachArray(items, item => ItemComponent(item))
|
|
472
865
|
)
|
|
473
866
|
]);
|
|
474
867
|
```
|
|
@@ -494,8 +887,7 @@ const DynamicForm = Form([
|
|
|
494
887
|
ShowIf(field.error,
|
|
495
888
|
Div({ class: 'error' }, field.error)
|
|
496
889
|
)
|
|
497
|
-
])
|
|
498
|
-
'name'
|
|
890
|
+
])
|
|
499
891
|
),
|
|
500
892
|
Button({ type: 'submit' }, 'Submit')
|
|
501
893
|
]);
|
|
@@ -527,7 +919,7 @@ const loadMoreItems = async () => {
|
|
|
527
919
|
};
|
|
528
920
|
|
|
529
921
|
const InfiniteList = Div([
|
|
530
|
-
ForEachArray(items, item => ItemComponent(item)
|
|
922
|
+
ForEachArray(items, item => ItemComponent(item)),
|
|
531
923
|
ShowIf(isLoading, LoadingSpinner()),
|
|
532
924
|
ShowIf(hasMore.check(more => more && !isLoading.val()),
|
|
533
925
|
Button('Load More').nd.onClick(loadMoreItems)
|
|
@@ -595,7 +987,7 @@ items.subscribe((newItems, oldItems, operations) => {
|
|
|
595
987
|
const DebugList = ForEachArray(items, (item, index) => {
|
|
596
988
|
console.log('Rendering item:', item, 'at index:', index?.val());
|
|
597
989
|
return ItemComponent(item);
|
|
598
|
-
}
|
|
990
|
+
});
|
|
599
991
|
```
|
|
600
992
|
|
|
601
993
|
## Next Steps
|
|
@@ -608,6 +1000,13 @@ Now that you understand list rendering, explore these related topics:
|
|
|
608
1000
|
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
609
1001
|
- **[NDElement](native-document-element.md)** - Native Document Element
|
|
610
1002
|
- **[Extending NDElement](extending-native-document-element.md)** - Custom Methods Guide
|
|
1003
|
+
- **[Advanced Components](advanced-components.md)** - Template caching and singleton views
|
|
611
1004
|
- **[Args Validation](validation.md)** - Function Argument Validation
|
|
612
1005
|
- **[State Management](state-management.md)** - Managing application state
|
|
613
|
-
- **[Memory Management](memory-management.md)** - Understanding cleanup and memory
|
|
1006
|
+
- **[Memory Management](memory-management.md)** - Understanding cleanup and memory
|
|
1007
|
+
|
|
1008
|
+
## Utilities
|
|
1009
|
+
|
|
1010
|
+
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
1011
|
+
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
1012
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|