native-document 1.0.14 → 1.0.15
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.dev.js +1262 -839
- package/dist/native-document.min.js +1 -1
- package/docs/anchor.md +216 -53
- package/docs/conditional-rendering.md +25 -24
- package/docs/core-concepts.md +20 -19
- package/docs/elements.md +21 -20
- package/docs/getting-started.md +28 -27
- package/docs/lifecycle-events.md +2 -2
- package/docs/list-rendering.md +607 -0
- package/docs/memory-management.md +1 -1
- package/docs/observables.md +15 -14
- package/docs/routing.md +22 -22
- package/docs/state-management.md +8 -8
- package/docs/validation.md +0 -2
- package/index.js +6 -1
- package/package.json +1 -1
- package/readme.md +5 -4
- package/src/data/MemoryManager.js +8 -20
- package/src/data/Observable.js +2 -180
- package/src/data/ObservableChecker.js +25 -24
- package/src/data/ObservableItem.js +158 -79
- package/src/data/observable-helpers/array.js +74 -0
- package/src/data/observable-helpers/batch.js +22 -0
- package/src/data/observable-helpers/computed.js +28 -0
- package/src/data/observable-helpers/object.js +111 -0
- package/src/elements/anchor.js +54 -9
- package/src/elements/control/for-each-array.js +280 -0
- package/src/elements/control/for-each.js +87 -110
- package/src/elements/index.js +1 -0
- package/src/elements/list.js +4 -0
- package/src/utils/helpers.js +44 -21
- package/src/wrappers/AttributesWrapper.js +5 -18
- package/src/wrappers/DocumentObserver.js +58 -29
- package/src/wrappers/ElementCreator.js +114 -0
- package/src/wrappers/HtmlElementEventsWrapper.js +52 -65
- package/src/wrappers/HtmlElementWrapper.js +11 -167
- package/src/wrappers/NdPrototype.js +109 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
# List Rendering
|
|
2
|
+
|
|
3
|
+
List rendering in NativeDocument provides powerful utilities for efficiently displaying dynamic collections of data. The framework offers two specialized functions: `ForEach` for generic iteration over objects and arrays, and `ForEachArray` for high-performance array-specific operations with advanced optimization features.
|
|
4
|
+
|
|
5
|
+
## Understanding List Rendering
|
|
6
|
+
|
|
7
|
+
List rendering automatically manages DOM updates when your data changes. Instead of manually manipulating the DOM, you define how each item should be rendered, and NativeDocument handles creation, updates, reordering, and cleanup efficiently.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { ForEach, Observable, Li, Ul } from 'native-document';
|
|
11
|
+
|
|
12
|
+
const items = Observable.array(['Apple', 'Banana', 'Cherry']);
|
|
13
|
+
|
|
14
|
+
// Automatically updates when items change
|
|
15
|
+
const itemList = Ul([
|
|
16
|
+
ForEach(items, item => Li(item))
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
// Add items - DOM updates automatically
|
|
20
|
+
items.push('Orange', 'Grape');
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## ForEach - Generic Collection Rendering
|
|
24
|
+
|
|
25
|
+
`ForEach` is the versatile option that works with both arrays and objects. It's perfect when you need flexibility or are working with mixed data types.
|
|
26
|
+
|
|
27
|
+
### Basic Array Iteration
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const fruits = Observable.array(['Apple', 'Banana', 'Cherry']);
|
|
31
|
+
|
|
32
|
+
const FruitList = Ul([
|
|
33
|
+
ForEach(fruits, fruit =>
|
|
34
|
+
Li({ class: 'fruit-item' }, fruit)
|
|
35
|
+
)
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
// All array operations trigger updates
|
|
39
|
+
fruits.push('Orange'); // Adds new item
|
|
40
|
+
fruits.splice(1, 1); // Removes 'Banana'
|
|
41
|
+
fruits.sort(); // Reorders items
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Object Iteration
|
|
45
|
+
|
|
46
|
+
```javascript
|
|
47
|
+
const userRoles = Observable({
|
|
48
|
+
admin: 'Administrator',
|
|
49
|
+
editor: 'Content Editor',
|
|
50
|
+
viewer: 'Read Only'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const RolesList = Ul([
|
|
54
|
+
ForEach(userRoles, (roleName, roleKey) =>
|
|
55
|
+
Li([
|
|
56
|
+
Strong(roleKey), ': ', roleName
|
|
57
|
+
])
|
|
58
|
+
)
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// Update object - DOM reflects changes
|
|
62
|
+
userRoles.set({
|
|
63
|
+
...userRoles.val(),
|
|
64
|
+
moderator: 'Community Moderator'
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Using Index Parameter
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
const tasks = Observable.array([
|
|
72
|
+
'Review pull requests',
|
|
73
|
+
'Update documentation',
|
|
74
|
+
'Fix bug reports'
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const TaskList = Ol([
|
|
78
|
+
ForEach(tasks, (task, indexObservable) =>
|
|
79
|
+
Li([
|
|
80
|
+
Strong(indexObservable.get(val => val + 1)),
|
|
81
|
+
' ',
|
|
82
|
+
task,
|
|
83
|
+
Button('Remove').nd.onClick(() =>
|
|
84
|
+
tasks.remove(indexObservable.val())
|
|
85
|
+
)
|
|
86
|
+
])
|
|
87
|
+
)
|
|
88
|
+
]);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Custom Key Functions
|
|
92
|
+
|
|
93
|
+
Use custom key functions
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const users = Observable.array([
|
|
97
|
+
{ id: 1, name: 'Alice', role: 'admin' },
|
|
98
|
+
{ id: 2, name: 'Bob', role: 'user' },
|
|
99
|
+
{ id: 3, name: 'Carol', role: 'editor' }
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// Use 'id' field as the key for efficient updates
|
|
103
|
+
const UserList = Div([
|
|
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
|
+
]);
|
|
113
|
+
|
|
114
|
+
// When users reorder, DOM nodes are moved, not recreated
|
|
115
|
+
users.set((items) => items.sort((a, b) => a.name.localeCompare(b.name)));
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## ForEachArray - High-Performance Array Rendering
|
|
119
|
+
|
|
120
|
+
`ForEachArray` is specifically optimized for **arrays of complex objects** and provides superior performance. **Use ForEachArray for object arrays** - it's designed for array-specific operations.
|
|
121
|
+
|
|
122
|
+
### Why ForEachArray for Complex Arrays?
|
|
123
|
+
|
|
124
|
+
`ForEachArray` includes optimizations that generic `ForEach` not provide:
|
|
125
|
+
|
|
126
|
+
- **Specialized diffing algorithm** optimized for array operations
|
|
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.)
|
|
130
|
+
|
|
131
|
+
### Basic Usage
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
const messages = Observable.array([
|
|
135
|
+
{ id: 1, text: 'Hello world!', timestamp: Date.now() },
|
|
136
|
+
{ id: 2, text: 'How are you?', timestamp: Date.now() + 1000 }
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
const ChatMessages = Div({ class: 'chat-container' }, [
|
|
140
|
+
ForEachArray(messages, message =>
|
|
141
|
+
Div({ class: 'message' }, [
|
|
142
|
+
Div({ class: 'message-text' }, message.text),
|
|
143
|
+
Div({ class: 'timestamp' }, new Date(message.timestamp).toLocaleTimeString())
|
|
144
|
+
])
|
|
145
|
+
)
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// Optimized array operations
|
|
149
|
+
messages.push({ id: 3, text: 'New message!', timestamp: Date.now() });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Advanced Array Operations
|
|
153
|
+
|
|
154
|
+
`ForEachArray` efficiently handles all array mutations:
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const playlist = Observable.array([
|
|
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,
|
|
193
|
+
Div([
|
|
194
|
+
Button('Push ').nd.onClick(() => {
|
|
195
|
+
playlist.push({ id: 4, title: 'New Song', artist: 'New Artist' });
|
|
196
|
+
}),
|
|
197
|
+
Button('Unshift').nd.onClick(() => {
|
|
198
|
+
playlist.unshift({ id: 0, title: 'First Song', artist: 'First' })
|
|
199
|
+
}),
|
|
200
|
+
Button('Reverse').nd.onClick(() => {
|
|
201
|
+
playlist.reverse()
|
|
202
|
+
}),
|
|
203
|
+
Button('Sort').nd.onClick(() => {
|
|
204
|
+
playlist.sort((a, b) => a.title.localeCompare(b.title))
|
|
205
|
+
})
|
|
206
|
+
])
|
|
207
|
+
]);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Custom Key Functions with ForEachArray
|
|
211
|
+
|
|
212
|
+
Key functions are crucial for optimal performance with complex objects:
|
|
213
|
+
|
|
214
|
+
```javascript
|
|
215
|
+
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 }
|
|
219
|
+
]);
|
|
220
|
+
|
|
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
|
+
)
|
|
230
|
+
]);
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Performance Configuration
|
|
235
|
+
|
|
236
|
+
`ForEachArray` supports performance tuning for large datasets:
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
const bigDataset = Observable.array([...Array(10000)].map((_, i) => ({
|
|
240
|
+
id: i,
|
|
241
|
+
value: `Item ${i}`,
|
|
242
|
+
category: Math.floor(i / 100)
|
|
243
|
+
})));
|
|
244
|
+
|
|
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
|
+
)
|
|
255
|
+
]);
|
|
256
|
+
|
|
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
|
+
})));
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Choosing Between ForEach and ForEachArray
|
|
266
|
+
|
|
267
|
+
### Use ForEachArray When:
|
|
268
|
+
|
|
269
|
+
✅ **Working with arrays of complex objects**
|
|
270
|
+
✅ **Performance is critical** - Large lists, frequent updates
|
|
271
|
+
✅ **Using array methods** - push, pop, splice, sort, reverse, etc.
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
// Perfect for ForEachArray
|
|
275
|
+
const comments = Observable.array([...]);
|
|
276
|
+
const CommentList = ForEachArray(comments, comment => CommentComponent(comment));
|
|
277
|
+
|
|
278
|
+
// Array operations work optimally
|
|
279
|
+
comments.push(newComment);
|
|
280
|
+
|
|
281
|
+
comments.splice(index, 1);
|
|
282
|
+
|
|
283
|
+
comments.sort((a, b) => b.timestamp - a.timestamp);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Use ForEach When:
|
|
287
|
+
|
|
288
|
+
✅ **Working with objects** - ForEach is required for object iteration
|
|
289
|
+
✅ **Mixed data types** - When data might be array or object
|
|
290
|
+
✅ **Arrays of primitive values** - string, number, boolean
|
|
291
|
+
✅ **Simple use cases** - Small lists with infrequent updates
|
|
292
|
+
|
|
293
|
+
## Real-World Examples
|
|
294
|
+
|
|
295
|
+
### Nested Lists with Mixed Rendering
|
|
296
|
+
|
|
297
|
+
```javascript
|
|
298
|
+
const categories = Observable.array([
|
|
299
|
+
{
|
|
300
|
+
id: 1,
|
|
301
|
+
name: 'Electronics',
|
|
302
|
+
items: Observable.array([
|
|
303
|
+
{ id: 101, name: 'Smartphone', price: 599 },
|
|
304
|
+
{ id: 102, name: 'Laptop', price: 999 }
|
|
305
|
+
])
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: 2,
|
|
309
|
+
name: 'Books',
|
|
310
|
+
items: Observable.array([
|
|
311
|
+
{ id: 201, name: 'JavaScript Guide', price: 29.99 },
|
|
312
|
+
{ id: 202, name: 'Design Patterns', price: 39.99 }
|
|
313
|
+
])
|
|
314
|
+
}
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const CategorizedProducts = Div({ class: 'product-categories' }, [
|
|
318
|
+
// Categories use ForEachArray (it's an array)
|
|
319
|
+
ForEachArray(categories, category =>
|
|
320
|
+
Div({ class: 'category' }, [
|
|
321
|
+
H3({ class: 'category-title' }, category.name),
|
|
322
|
+
|
|
323
|
+
// Items within each category also use ForEachArray
|
|
324
|
+
ForEachArray(category.items,
|
|
325
|
+
item => Div({ class: 'product-item' }, [
|
|
326
|
+
Span({ class: 'product-name' }, item.name),
|
|
327
|
+
Span({ class: 'product-price' }, `$${item.price}`),
|
|
328
|
+
Button('Add to Cart').nd.onClick(() => addToCart(item))
|
|
329
|
+
]),
|
|
330
|
+
'id'
|
|
331
|
+
),
|
|
332
|
+
|
|
333
|
+
Button('Add Item').nd.onClick(() => {
|
|
334
|
+
const newItem = {
|
|
335
|
+
id: Date.now(),
|
|
336
|
+
name: `New ${category.name} Item`,
|
|
337
|
+
price: Math.floor(Math.random() * 100) + 10
|
|
338
|
+
};
|
|
339
|
+
category.items.push(newItem);
|
|
340
|
+
})
|
|
341
|
+
]),
|
|
342
|
+
'id'
|
|
343
|
+
)
|
|
344
|
+
]);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Performance Best Practices
|
|
348
|
+
|
|
349
|
+
### 1. Always Use Keys for Complex Objects
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
// ✅ 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
|
+
ForEachArray(users, user => UserCard(user))
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 2. Choose the Right Function
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
// ✅ Perfect: ForEachArray for complex objects
|
|
364
|
+
const users = Observable.array([{id: 1, name: 'Alice'}]);
|
|
365
|
+
ForEachArray(users, renderUser, 'id')
|
|
366
|
+
|
|
367
|
+
// ✅ Correct: ForEach for primitives
|
|
368
|
+
const tags = Observable.array(['js', 'css']);
|
|
369
|
+
ForEach(tags, renderTag)
|
|
370
|
+
|
|
371
|
+
// ✅ Correct: ForEach for objects
|
|
372
|
+
const config = Observable({theme: 'dark'});
|
|
373
|
+
ForEach(config, renderSetting, (item, key) => key)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### 3. Use Computed Values for Derived Lists
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
// ✅ Efficient: Computed filtered list
|
|
380
|
+
const searchTerm = Observable('');
|
|
381
|
+
const filteredItems = Observable.computed(() =>
|
|
382
|
+
allItems.val().filter(item =>
|
|
383
|
+
item.name.toLowerCase().includes(searchTerm.val().toLowerCase())
|
|
384
|
+
),
|
|
385
|
+
[allItems, searchTerm]
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
ForEachArray(filteredItems, renderItem, 'id')
|
|
389
|
+
|
|
390
|
+
// ❌ Inefficient: Filtering in render
|
|
391
|
+
ForEachArray(allItems, item => {
|
|
392
|
+
if (item.name.includes(searchTerm.val())) {
|
|
393
|
+
return renderItem(item);
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
})
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Memory Management
|
|
400
|
+
|
|
401
|
+
Both `ForEach` and `ForEachArray` automatically manage memory:
|
|
402
|
+
|
|
403
|
+
```javascript
|
|
404
|
+
// Cleanup is automatic when observables are garbage collected
|
|
405
|
+
let myList = Observable.array([1, 2, 3]);
|
|
406
|
+
let listComponent = ForEachArray(myList, item => Div(item));
|
|
407
|
+
|
|
408
|
+
// When references are lost, cleanup happens automatically
|
|
409
|
+
myList = null;
|
|
410
|
+
listComponent = null; // Memory will be freed
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
For explicit cleanup:
|
|
414
|
+
|
|
415
|
+
```javascript
|
|
416
|
+
const items = Observable.array([...]);
|
|
417
|
+
const listComponent = ForEachArray(items, renderItem);
|
|
418
|
+
|
|
419
|
+
// Manual cleanup when needed
|
|
420
|
+
items.cleanup();
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Common Pitfalls and Solutions
|
|
424
|
+
|
|
425
|
+
### 1. Missing Keys with Complex Objects
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
// ❌ Problem: No key, inefficient updates
|
|
429
|
+
ForEachArray(users, user => UserProfile(user))
|
|
430
|
+
|
|
431
|
+
// ✅ Solution: Use unique key
|
|
432
|
+
ForEachArray(users, user => UserProfile(user), 'id')
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### 2. Using ForEach for Complex Arrays
|
|
436
|
+
|
|
437
|
+
```javascript
|
|
438
|
+
// ❌ Suboptimal: Generic ForEach for arrays
|
|
439
|
+
ForEach(genericObject, renderItem)
|
|
440
|
+
|
|
441
|
+
// ✅ Optimal: Specialized ForEachArray for arrays
|
|
442
|
+
ForEachArray(arrayData, renderItem)
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### 3. Modifying Arrays Directly
|
|
446
|
+
|
|
447
|
+
```javascript
|
|
448
|
+
// ❌ Wrong: Direct mutation doesn't trigger updates
|
|
449
|
+
items.val().push(newItem);
|
|
450
|
+
|
|
451
|
+
// ✅ Correct: Use Observable array methods
|
|
452
|
+
items.push(newItem);
|
|
453
|
+
|
|
454
|
+
// ✅ Also correct: Set new array
|
|
455
|
+
items.set([...items.val(), newItem]);
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Integration with Other Features
|
|
459
|
+
|
|
460
|
+
### With Conditional Rendering
|
|
461
|
+
|
|
462
|
+
```javascript
|
|
463
|
+
const items = Observable.array([]);
|
|
464
|
+
const showEmptyState = items.check(arr => arr.length === 0);
|
|
465
|
+
|
|
466
|
+
const ItemList = Div([
|
|
467
|
+
ShowIf(showEmptyState,
|
|
468
|
+
Div({ class: 'empty-state' }, 'No items found')
|
|
469
|
+
),
|
|
470
|
+
HideIf(showEmptyState,
|
|
471
|
+
ForEachArray(items, item => ItemComponent(item), 'id')
|
|
472
|
+
)
|
|
473
|
+
]);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### With Forms and Validation
|
|
477
|
+
|
|
478
|
+
```javascript
|
|
479
|
+
const formFields = Observable.array([
|
|
480
|
+
{ name: 'firstName', label: 'First Name', value: '', required: true },
|
|
481
|
+
{ name: 'lastName', label: 'Last Name', value: '', required: true },
|
|
482
|
+
{ name: 'email', label: 'Email', value: '', required: true }
|
|
483
|
+
]);
|
|
484
|
+
|
|
485
|
+
const DynamicForm = Form([
|
|
486
|
+
ForEachArray(formFields, field =>
|
|
487
|
+
Div({ class: 'form-group' }, [
|
|
488
|
+
Label(field.label + (field.required ? ' *' : '')),
|
|
489
|
+
Input({
|
|
490
|
+
name: field.name,
|
|
491
|
+
value: field.value,
|
|
492
|
+
required: field.required
|
|
493
|
+
}),
|
|
494
|
+
ShowIf(field.error,
|
|
495
|
+
Div({ class: 'error' }, field.error)
|
|
496
|
+
)
|
|
497
|
+
]),
|
|
498
|
+
'name'
|
|
499
|
+
),
|
|
500
|
+
Button({ type: 'submit' }, 'Submit')
|
|
501
|
+
]);
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Advanced Patterns
|
|
505
|
+
|
|
506
|
+
### Infinite Scrolling
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
const items = Observable.array([]);
|
|
510
|
+
const isLoading = Observable(false);
|
|
511
|
+
const hasMore = Observable(true);
|
|
512
|
+
|
|
513
|
+
const loadMoreItems = async () => {
|
|
514
|
+
if (isLoading.val()) return;
|
|
515
|
+
|
|
516
|
+
isLoading.set(true);
|
|
517
|
+
try {
|
|
518
|
+
const newItems = await fetchItems(items.val().length);
|
|
519
|
+
if (newItems.length === 0) {
|
|
520
|
+
hasMore.set(false);
|
|
521
|
+
} else {
|
|
522
|
+
items.push(...newItems);
|
|
523
|
+
}
|
|
524
|
+
} finally {
|
|
525
|
+
isLoading.set(false);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const InfiniteList = Div([
|
|
530
|
+
ForEachArray(items, item => ItemComponent(item), 'id'),
|
|
531
|
+
ShowIf(isLoading, LoadingSpinner()),
|
|
532
|
+
ShowIf(hasMore.check(more => more && !isLoading.val()),
|
|
533
|
+
Button('Load More').nd.onClick(loadMoreItems)
|
|
534
|
+
)
|
|
535
|
+
]);
|
|
536
|
+
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Drag and Drop Reordering
|
|
540
|
+
|
|
541
|
+
```javascript
|
|
542
|
+
const draggableItems = Observable.array([
|
|
543
|
+
{ id: 1, text: 'Item 1' },
|
|
544
|
+
{ id: 2, text: 'Item 2' },
|
|
545
|
+
{ id: 3, text: 'Item 3' }
|
|
546
|
+
]);
|
|
547
|
+
|
|
548
|
+
let draggedIndex = null;
|
|
549
|
+
|
|
550
|
+
const DraggableList = Div([
|
|
551
|
+
ForEachArray(draggableItems, (item, indexObservable) =>
|
|
552
|
+
Div({
|
|
553
|
+
class: 'draggable-item',
|
|
554
|
+
draggable: true
|
|
555
|
+
}, item.text)
|
|
556
|
+
.nd.onDragStart((e) => {
|
|
557
|
+
draggedIndex = indexObservable.val();
|
|
558
|
+
})
|
|
559
|
+
.nd.onDragOver((e) => e.preventDefault())
|
|
560
|
+
.nd.onDrop((e) => {
|
|
561
|
+
e.preventDefault();
|
|
562
|
+
const dropIndex = indexObservable.val();
|
|
563
|
+
if (draggedIndex !== null && draggedIndex !== dropIndex) {
|
|
564
|
+
const items = draggableItems.val();
|
|
565
|
+
const draggedItem = items[draggedIndex];
|
|
566
|
+
|
|
567
|
+
// Remove from old position
|
|
568
|
+
items.splice(draggedIndex, 1);
|
|
569
|
+
// Insert at new position
|
|
570
|
+
items.splice(dropIndex, 0, draggedItem);
|
|
571
|
+
|
|
572
|
+
draggableItems.set([...items]);
|
|
573
|
+
}
|
|
574
|
+
draggedIndex = null;
|
|
575
|
+
}),
|
|
576
|
+
'id'
|
|
577
|
+
)
|
|
578
|
+
]);
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
## Debugging List Rendering
|
|
582
|
+
|
|
583
|
+
### Logging Updates
|
|
584
|
+
|
|
585
|
+
```javascript
|
|
586
|
+
const items = Observable.array([]);
|
|
587
|
+
|
|
588
|
+
// Log all array operations
|
|
589
|
+
items.subscribe((newItems, oldItems, operations) => {
|
|
590
|
+
console.log('Array operation:', operations);
|
|
591
|
+
console.log('Old items:', oldItems);
|
|
592
|
+
console.log('New items:', newItems);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const DebugList = ForEachArray(items, (item, index) => {
|
|
596
|
+
console.log('Rendering item:', item, 'at index:', index?.val());
|
|
597
|
+
return ItemComponent(item);
|
|
598
|
+
}, 'id');
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
## Next Steps
|
|
602
|
+
|
|
603
|
+
Now that you understand list rendering, explore these related topics:
|
|
604
|
+
|
|
605
|
+
- **[Conditional Rendering](conditional-rendering.md)** - Show/hide content dynamically
|
|
606
|
+
- **[State Management](state-management.md)** - Managing application state
|
|
607
|
+
- **[Memory Management](memory-management.md)** - Understanding cleanup and memory
|
package/docs/observables.md
CHANGED
|
@@ -66,7 +66,7 @@ console.log(userProxy.name.val()); // "Alice"
|
|
|
66
66
|
userProxy.name.set("Bob");
|
|
67
67
|
|
|
68
68
|
// Get all values as plain object
|
|
69
|
-
console.log(userProxy.$
|
|
69
|
+
console.log(userProxy.$value); // { name: "Bob", age: 25 }
|
|
70
70
|
console.log(Observable.value(userProxy)); // { name: "Bob", age: 25 }
|
|
71
71
|
|
|
72
72
|
// Observable(object) creates a SINGLE observable containing the whole object
|
|
@@ -104,7 +104,7 @@ user.name.set("Bob");
|
|
|
104
104
|
user.age.$value = 30; // Using proxy syntax
|
|
105
105
|
|
|
106
106
|
// Get the complete object value
|
|
107
|
-
console.log(user.$
|
|
107
|
+
console.log(user.$value); // { name: "Bob", age: 30, email: "alice@example.com" }
|
|
108
108
|
console.log(Observable.value(user)); // Same as above
|
|
109
109
|
|
|
110
110
|
// Listen to individual property changes
|
|
@@ -164,9 +164,9 @@ const decrement = () => (count.$value--);
|
|
|
164
164
|
|
|
165
165
|
// Reactive interface
|
|
166
166
|
const app = Div({ class: "counter" }, [
|
|
167
|
-
Button("-").nd.
|
|
167
|
+
Button("-").nd.onClick(decrement),
|
|
168
168
|
Span({ class: "count" }, count), // Automatic display
|
|
169
|
-
Button("+").nd.
|
|
169
|
+
Button("+").nd.onClick(increment)
|
|
170
170
|
]);
|
|
171
171
|
```
|
|
172
172
|
|
|
@@ -356,8 +356,8 @@ const updateProfile = Observable.batch((profileData) => {
|
|
|
356
356
|
// ✅ Correct: Single batch dependency
|
|
357
357
|
const profileSummary = Observable.computed(() => {
|
|
358
358
|
return {
|
|
359
|
-
user: user.$
|
|
360
|
-
settings: settings.$
|
|
359
|
+
user: user.$value,
|
|
360
|
+
settings: settings.$value,
|
|
361
361
|
lastUpdated: Date.now()
|
|
362
362
|
};
|
|
363
363
|
}, updateProfile); // ← Single batch function
|
|
@@ -436,7 +436,7 @@ const levelUp = Observable.batch(() => {
|
|
|
436
436
|
|
|
437
437
|
// Complex computed that should only run after complete transitions
|
|
438
438
|
const gameStatusMessage = Observable.computed(() => {
|
|
439
|
-
const state = gameState.$
|
|
439
|
+
const state = gameState.$value;
|
|
440
440
|
return `Level ${state.level}: ${state.score} points, ${state.lives} lives remaining`;
|
|
441
441
|
}, levelUp); // ← Only updates when levelUp() is called
|
|
442
442
|
```
|
|
@@ -536,10 +536,11 @@ console.log(Observable.value(complexData)); // Plain object with extracted value
|
|
|
536
536
|
|
|
537
537
|
Now that you understand NativeDocument's observable, explore these advanced topics:
|
|
538
538
|
|
|
539
|
-
- **[Elements](
|
|
540
|
-
- **[Conditional Rendering](
|
|
541
|
-
- **[
|
|
542
|
-
- **[
|
|
543
|
-
- **[
|
|
544
|
-
- **[
|
|
545
|
-
- **[
|
|
539
|
+
- **[Elements](elements.md)** - Creating and composing UI
|
|
540
|
+
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
541
|
+
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
542
|
+
- **[Routing](routing.md)** - Navigation and URL management
|
|
543
|
+
- **[State Management](state-management.md)** - Global state patterns
|
|
544
|
+
- **[Lifecycle Events](lifecycle-events.md)** - Lifecycle events
|
|
545
|
+
- **[Memory Management](memory-management.md)** - Memory management
|
|
546
|
+
- **[Anchor](anchor.md)** - Anchor
|