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.
Files changed (85) hide show
  1. package/dist/native-document.components.min.js +1088 -65
  2. package/dist/native-document.dev.js +695 -142
  3. package/dist/native-document.dev.js.map +1 -1
  4. package/dist/native-document.devtools.min.js +1 -1
  5. package/dist/native-document.min.js +1 -1
  6. package/docs/advanced-components.md +814 -0
  7. package/docs/anchor.md +71 -11
  8. package/docs/cache.md +888 -0
  9. package/docs/conditional-rendering.md +91 -1
  10. package/docs/core-concepts.md +9 -2
  11. package/docs/elements.md +127 -2
  12. package/docs/extending-native-document-element.md +7 -1
  13. package/docs/filters.md +1216 -0
  14. package/docs/getting-started.md +12 -3
  15. package/docs/lifecycle-events.md +10 -2
  16. package/docs/list-rendering.md +453 -54
  17. package/docs/memory-management.md +9 -7
  18. package/docs/native-document-element.md +30 -9
  19. package/docs/native-fetch.md +744 -0
  20. package/docs/observables.md +135 -6
  21. package/docs/routing.md +7 -1
  22. package/docs/state-management.md +7 -1
  23. package/docs/validation.md +8 -1
  24. package/elements.js +1 -0
  25. package/eslint.config.js +3 -3
  26. package/index.def.js +350 -0
  27. package/package.json +3 -2
  28. package/readme.md +53 -14
  29. package/src/components/$traits/HasItems.js +42 -1
  30. package/src/components/BaseComponent.js +4 -1
  31. package/src/components/accordion/Accordion.js +112 -8
  32. package/src/components/accordion/AccordionItem.js +93 -4
  33. package/src/components/alert/Alert.js +164 -4
  34. package/src/components/avatar/Avatar.js +236 -22
  35. package/src/components/menu/index.js +1 -2
  36. package/src/core/data/ObservableArray.js +120 -2
  37. package/src/core/data/ObservableChecker.js +50 -0
  38. package/src/core/data/ObservableItem.js +124 -4
  39. package/src/core/data/ObservableWhen.js +36 -6
  40. package/src/core/data/observable-helpers/array.js +12 -3
  41. package/src/core/data/observable-helpers/computed.js +17 -4
  42. package/src/core/data/observable-helpers/object.js +19 -3
  43. package/src/core/elements/content-formatter.js +138 -1
  44. package/src/core/elements/control/for-each-array.js +20 -2
  45. package/src/core/elements/control/for-each.js +17 -5
  46. package/src/core/elements/control/show-if.js +31 -15
  47. package/src/core/elements/control/show-when.js +23 -0
  48. package/src/core/elements/control/switch.js +40 -10
  49. package/src/core/elements/description-list.js +14 -0
  50. package/src/core/elements/form.js +188 -4
  51. package/src/core/elements/html5-semantics.js +44 -1
  52. package/src/core/elements/img.js +22 -10
  53. package/src/core/elements/index.js +5 -0
  54. package/src/core/elements/interactive.js +19 -1
  55. package/src/core/elements/list.js +28 -1
  56. package/src/core/elements/medias.js +29 -0
  57. package/src/core/elements/meta-data.js +34 -0
  58. package/src/core/elements/table.js +59 -0
  59. package/src/core/utils/cache.js +5 -0
  60. package/src/core/utils/helpers.js +7 -2
  61. package/src/core/utils/memoize.js +25 -16
  62. package/src/core/utils/prototypes.js +3 -2
  63. package/src/core/wrappers/AttributesWrapper.js +1 -1
  64. package/src/core/wrappers/HtmlElementWrapper.js +2 -2
  65. package/src/core/wrappers/NDElement.js +42 -2
  66. package/src/core/wrappers/NdPrototype.js +4 -0
  67. package/src/core/wrappers/TemplateCloner.js +14 -11
  68. package/src/core/wrappers/prototypes/bind-class-extensions.js +1 -1
  69. package/src/core/wrappers/prototypes/nd-element-extensions.js +3 -0
  70. package/src/router/Route.js +9 -4
  71. package/src/router/Router.js +28 -9
  72. package/src/router/errors/RouterError.js +0 -1
  73. package/types/control-flow.d.ts +9 -6
  74. package/types/elements.d.ts +496 -111
  75. package/types/filters/index.d.ts +4 -0
  76. package/types/forms.d.ts +85 -48
  77. package/types/images.d.ts +16 -9
  78. package/types/nd-element.d.ts +5 -238
  79. package/types/observable.d.ts +9 -3
  80. package/types/router.d.ts +5 -1
  81. package/types/template-cloner.ts +1 -0
  82. package/types/validator.ts +11 -1
  83. package/utils.d.ts +2 -1
  84. package/utils.js +4 -4
  85. package/src/core/utils/service.js +0 -6
@@ -200,11 +200,12 @@ const addTodo = () => {
200
200
  if (!newTodo.val().trim()) {
201
201
  return;
202
202
  }
203
- todos.push({
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: Observable(todo.done)
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
@@ -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(input => {
58
- input.focus();
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
 
@@ -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.get(val => val + 1)),
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()-1) {
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
- ### Custom Key Functions with ForEachArray
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
- Key functions are crucial for optimal performance with complex objects:
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
- { sku: 'PHONE-001', name: 'Smartphone', price: 599 },
217
- { sku: 'LAPTOP-001', name: 'Laptop', price: 999 },
218
- { sku: 'TABLET-001', name: 'Tablet', price: 399 }
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
- const ProductCatalog = Div({ class: 'catalog' }, [
222
- ForEachArray(products,
223
- product => Div({ class: 'product-card' }, [
224
- H3(product.name),
225
- Div({ class: 'price' }, `$${product.price}`),
226
- Div({ class: 'sku' }, `SKU: ${product.sku}`)
227
- ]),
228
- 'sku' // Use SKU as key for efficient tracking
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
- ### Performance Configuration
475
+ ### Available Filter Helpers
235
476
 
236
- `ForEachArray` supports performance tuning for large datasets:
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
- const bigDataset = Observable.array([...Array(10000)].map((_, i) => ({
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 BigList = Div([
246
- ForEachArray(bigDataset,
247
- item => Div({ class: 'list-item' }, [
248
- Strong(`#${item.id}`), ' - ', item.value
249
- ]),
250
- 'id', // Key function
251
- {
252
- pushDelay: (items) => items.length > 100 ? 50 : 0 // Delay for large additions
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
- // Large additions are automatically throttled
258
- bigDataset.push(...[...Array(500)].map((_, i) => ({
259
- id: 10000 + i,
260
- value: `New Item ${i}`,
261
- category: 999
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, 'id')
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), 'id')
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), 'id')
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), 'id'),
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
- }, 'id');
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