native-document 1.0.95 → 1.0.99
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/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
- package/devtools/index.js +8 -0
- package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
- package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
- package/devtools/transformers/src/transformComponentForHrm.js +74 -0
- package/devtools/transformers/src/transformJsFile.js +9 -0
- package/devtools/transformers/src/utils.js +79 -0
- package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
- package/devtools/widget/Widget.js +48 -0
- package/devtools/widget/widget.css +81 -0
- package/devtools/widget.js +23 -0
- package/dist/native-document.components.min.js +1953 -1245
- package/dist/native-document.dev.js +2022 -1375
- 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/cache.md +1 -1
- package/docs/core-concepts.md +1 -1
- package/docs/native-document-element.md +51 -15
- package/docs/observables.md +333 -315
- package/docs/state-management.md +198 -193
- package/package.json +1 -1
- package/readme.md +1 -1
- package/rollup.config.js +1 -1
- package/src/core/data/ObservableArray.js +67 -0
- package/src/core/data/ObservableChecker.js +2 -0
- package/src/core/data/ObservableItem.js +97 -0
- package/src/core/data/ObservableObject.js +183 -0
- package/src/core/data/Store.js +364 -34
- package/src/core/data/observable-helpers/object.js +2 -166
- package/src/core/utils/formatters.js +91 -0
- package/src/core/utils/localstorage.js +57 -0
- package/src/core/utils/validator.js +0 -2
- package/src/fetch/NativeFetch.js +5 -2
- package/types/observable.d.ts +73 -15
- package/types/plugins-manager.d.ts +1 -1
- package/types/store.d.ts +33 -6
- package/hrm.js +0 -7
- package/src/devtools/app/App.js +0 -66
- package/src/devtools/app/app.css +0 -0
- package/src/devtools/hrm/transformComponent.js +0 -129
- package/src/devtools/index.js +0 -18
- package/src/devtools/widget/DevToolsWidget.js +0 -26
- /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
package/docs/observables.md
CHANGED
|
@@ -5,7 +5,6 @@ Observables are the reactive core of NativeDocument. They allow you to create va
|
|
|
5
5
|
## Creating Simple Observables
|
|
6
6
|
|
|
7
7
|
```javascript
|
|
8
|
-
// Create an observable with an initial value
|
|
9
8
|
const count = Observable(0);
|
|
10
9
|
const message = Observable("Hello World");
|
|
11
10
|
const isVisible = Observable(true);
|
|
@@ -36,17 +35,16 @@ console.log(name.val()); // "BOB"
|
|
|
36
35
|
```
|
|
37
36
|
|
|
38
37
|
## Listening to Changes
|
|
38
|
+
|
|
39
39
|
The `.subscribe()` method allows you to listen to every change in an observable. The callback receives both the new value and the previous value.
|
|
40
40
|
|
|
41
41
|
```javascript
|
|
42
42
|
const counter = Observable(0);
|
|
43
43
|
|
|
44
|
-
// Subscribe to changes
|
|
45
44
|
counter.subscribe(newValue => {
|
|
46
|
-
|
|
45
|
+
console.log("Counter is now:", newValue);
|
|
47
46
|
});
|
|
48
47
|
|
|
49
|
-
// Function will be called on every change
|
|
50
48
|
counter.set(1); // Logs: "Counter is now: 1"
|
|
51
49
|
counter.set(2); // Logs: "Counter is now: 2"
|
|
52
50
|
```
|
|
@@ -58,18 +56,14 @@ The `.on()` method allows you to watch for specific values in an observable. The
|
|
|
58
56
|
```javascript
|
|
59
57
|
const status = Observable("idle");
|
|
60
58
|
|
|
61
|
-
// Watch for specific value - callback called twice:
|
|
62
|
-
// - once with true when value is reached
|
|
63
|
-
// - once with false when value changes away
|
|
64
59
|
status.on("loading", (isActive) => {
|
|
65
|
-
|
|
60
|
+
console.log(`Loading state: ${isActive}`);
|
|
66
61
|
});
|
|
67
62
|
|
|
68
63
|
status.on("success", (isActive) => {
|
|
69
|
-
|
|
64
|
+
console.log(`Success state: ${isActive}`);
|
|
70
65
|
});
|
|
71
66
|
|
|
72
|
-
// Test the watchers
|
|
73
67
|
status.set("loading"); // Logs: "Loading state: true"
|
|
74
68
|
status.set("success"); // Logs: "Loading state: false", "Success state: true"
|
|
75
69
|
status.set("idle"); // Logs: "Success state: false"
|
|
@@ -100,51 +94,43 @@ status.set("idle"); // Logs: "Success state: false"
|
|
|
100
94
|
```javascript
|
|
101
95
|
const status = Observable("idle");
|
|
102
96
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
// ❌ .subscribe() - ALL 1000 callbacks run on EVERY change
|
|
97
|
+
// ❌ .subscribe() — ALL 1000 callbacks run on EVERY change
|
|
106
98
|
for (let i = 0; i < 1000; i++) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
status.subscribe(value => {
|
|
100
|
+
if (value === `state-${i}`) updateComponent(i);
|
|
101
|
+
});
|
|
110
102
|
}
|
|
111
103
|
|
|
112
|
-
// ✅ .on()
|
|
104
|
+
// ✅ .on() — Only relevant callback runs
|
|
113
105
|
for (let i = 0; i < 1000; i++) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
status.on(`state-${i}`, (isActive) => {
|
|
107
|
+
if (isActive) updateComponent(i);
|
|
108
|
+
});
|
|
117
109
|
}
|
|
118
110
|
```
|
|
111
|
+
|
|
119
112
|
## Observable .when() Method
|
|
120
113
|
|
|
121
114
|
The `.when()` method creates a transitive object that passes the observable and target value without creating additional observables. It's memory-efficient for conditional operations like CSS class binding.
|
|
122
115
|
|
|
123
|
-
## Syntax
|
|
124
|
-
|
|
125
116
|
```javascript
|
|
126
117
|
observable.when(targetValue)
|
|
127
118
|
```
|
|
128
119
|
|
|
129
120
|
Returns an object with `{$target: targetValue, $observer: observable}` that can be used with conditional operations.
|
|
121
|
+
|
|
130
122
|
### Primary Use Case: CSS Class Binding
|
|
131
123
|
|
|
132
124
|
```javascript
|
|
133
125
|
const status = Observable("loading");
|
|
134
126
|
|
|
135
|
-
// Memory-efficient conditional class binding
|
|
136
127
|
const element = Div({
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
class: {
|
|
129
|
+
"spinner": status.when("loading"),
|
|
130
|
+
"success": status.when("success"),
|
|
131
|
+
"error": status.when("error")
|
|
132
|
+
}
|
|
142
133
|
});
|
|
143
|
-
|
|
144
|
-
// The class binding system recognizes the .when() pattern:
|
|
145
|
-
// - Checks if status.val() === "loading"
|
|
146
|
-
// - Toggles 'spinner' class accordingly
|
|
147
|
-
// - Uses status.on("loading", callback) internally for efficiency
|
|
148
134
|
```
|
|
149
135
|
|
|
150
136
|
### Benefits
|
|
@@ -153,29 +139,150 @@ const element = Div({
|
|
|
153
139
|
- **Optimized for class binding**: Works seamlessly with NativeDocument's class system
|
|
154
140
|
- **Simple API**: Just pass the value to watch for
|
|
155
141
|
|
|
156
|
-
|
|
142
|
+
## Method Comparison
|
|
157
143
|
|
|
158
|
-
|
|
144
|
+
| Method | Memory Impact | Use Case | Return Value |
|
|
145
|
+
|--------|---------------|----------|----------------------------|
|
|
146
|
+
| `.when(value)` | ✅ Zero — transitive object | CSS classes, conditional checks | `{$target, $observer}` |
|
|
147
|
+
| `.on(value, callback)` | ✅ Minimal — single listener per value | Specific value watching | void |
|
|
148
|
+
| `.check(callback)` | ❌ Creates new ObservableChecker | Complex conditions | ObservableChecker instance |
|
|
149
|
+
| `.subscribe(callback)` | ❌ Creates listener for all changes | General change detection | void |
|
|
150
|
+
| `.persist(key, options?)` | ✅ No new observable | localStorage binding | `this` (chainable) |
|
|
159
151
|
|
|
160
|
-
##
|
|
152
|
+
## Observable Checkers
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|--------|---------------------------------------|----------|-----------------------------|
|
|
164
|
-
| `.when(value)` | ✅ Zero - transitive object | CSS classes, conditional checks | `{$target, $observer}` |
|
|
165
|
-
| `.on(value, callback)` | ✅ Minimal - single listener per value | Specific value watching | Unsubscribe function |
|
|
166
|
-
| `.check(callback)` | ❌ Creates new Observable Checker | Complex conditions | Observable Checker instance |
|
|
167
|
-
| `.subscribe(callback)` | ❌ Creates listener for all changes | General change detection | Unsubscribe function |
|
|
154
|
+
Create derived observables with conditions or transformations. All aliases point to the same underlying `ObservableChecker` — use whichever reads most naturally for your use case.
|
|
168
155
|
|
|
156
|
+
```javascript
|
|
157
|
+
const age = Observable(17);
|
|
169
158
|
|
|
170
|
-
|
|
159
|
+
// check — canonical name
|
|
160
|
+
const isAdult = age.check(value => value >= 18);
|
|
161
|
+
console.log(isAdult.val()); // false
|
|
171
162
|
|
|
172
|
-
|
|
163
|
+
age.set(20);
|
|
164
|
+
console.log(isAdult.val()); // true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Available Aliases
|
|
168
|
+
|
|
169
|
+
| Alias | Best suited for |
|
|
170
|
+
|-------|-----------------|
|
|
171
|
+
| `.check(fn)` | General conditions |
|
|
172
|
+
| `.is(fn)` | Boolean / state checks |
|
|
173
|
+
| `.select(fn)` | Extracting a field |
|
|
174
|
+
| `.pluck(fn)` | Extracting a field (Lodash style) |
|
|
175
|
+
| `.transform(fn)` | Explicit value transformation |
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// All return an ObservableChecker — pick what reads best
|
|
179
|
+
ShowIf(user.is(u => u.isAdmin), AdminPanel())
|
|
180
|
+
|
|
181
|
+
const email = user.select(u => u.email);
|
|
182
|
+
const label = status.transform(s => s.toUpperCase());
|
|
183
|
+
const name = user.pluck(u => u.name);
|
|
184
|
+
```
|
|
185
|
+
## Observable.format()
|
|
186
|
+
|
|
187
|
+
Creates a derived observable that formats the current value using `Intl`.
|
|
188
|
+
Automatically reacts to both value changes and locale changes via `Store.setLocale()`.
|
|
189
|
+
```javascript
|
|
190
|
+
const price = Observable(15000);
|
|
191
|
+
const date = Observable(new Date());
|
|
192
|
+
const count = Observable(3);
|
|
193
|
+
|
|
194
|
+
// Currency
|
|
195
|
+
price.format('currency') // "15 000 FCFA"
|
|
196
|
+
price.format('currency', { currency: 'EUR' }) // "15 000,00 €"
|
|
197
|
+
price.format('currency', { notation: 'compact' }) // "15 K FCFA"
|
|
198
|
+
|
|
199
|
+
// Number
|
|
200
|
+
price.format('number') // "15 000"
|
|
201
|
+
|
|
202
|
+
// Percent
|
|
203
|
+
Observable(0.15).format('percent') // "15,0 %"
|
|
204
|
+
Observable(0.15).format('percent', { decimals: 2}) // "15,00 %"
|
|
205
|
+
|
|
206
|
+
// Date
|
|
207
|
+
date.format('date') // "3 mars 2026"
|
|
208
|
+
date.format('date', { dateStyle: 'full' }) // "mardi 3 mars 2026"
|
|
209
|
+
date.format('date', { format: 'DD/MM/YYYY' }) // "03/03/2026"
|
|
210
|
+
date.format('date', { format: 'DD MMM YYYY' }) // "03 mar 2026"
|
|
211
|
+
date.format('date', { format: 'DD MMMM YYYY' }) // "03 mars 2026"
|
|
212
|
+
|
|
213
|
+
// Time
|
|
214
|
+
date.format('time') // "20:30"
|
|
215
|
+
date.format('time', { second: '2-digit' }) // "20:30:00"
|
|
216
|
+
date.format('time', { format: 'HH:mm:ss' }) // "20:30:00"
|
|
217
|
+
|
|
218
|
+
// Datetime
|
|
219
|
+
date.format('datetime') // "3 mars 2026, 20:30"
|
|
220
|
+
date.format('datetime', { dateStyle: 'full' }) // "mardi 3 mars 2026, 20:30"
|
|
221
|
+
date.format('datetime', { format: 'DD/MM/YYYY HH:mm' }) // "03/03/2026 20:30"
|
|
222
|
+
|
|
223
|
+
// Relative
|
|
224
|
+
date.format('relative') // "dans 11 jours"
|
|
225
|
+
date.format('relative', { unit: 'month' }) // "dans 1 mois"
|
|
226
|
+
|
|
227
|
+
// Plural
|
|
228
|
+
count.format('plural', { singular: 'billet', plural: 'billets' }) // "3 billets"
|
|
229
|
+
|
|
230
|
+
// Custom formatter — works like transform()
|
|
231
|
+
price.format(value => `${value.toLocaleString()} FCFA`)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Locale Reactivity
|
|
235
|
+
|
|
236
|
+
All formatted observables automatically recalculate when the locale changes:
|
|
237
|
+
```javascript
|
|
238
|
+
const price = Observable(15000);
|
|
239
|
+
const label = price.format('currency', { currency: 'XOF' });
|
|
240
|
+
|
|
241
|
+
label.val(); // "15 000 FCFA"
|
|
242
|
+
|
|
243
|
+
Store.get('locale').set('en-US');
|
|
244
|
+
label.val(); // "$15,000.00"
|
|
245
|
+
|
|
246
|
+
Store.get('locale').set('fr-TG');
|
|
247
|
+
label.val(); // "15 000 FCFA"
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Extending Formatters
|
|
251
|
+
|
|
252
|
+
Add custom format types via `Formatters`:
|
|
253
|
+
```javascript
|
|
254
|
+
import { Formatters } from 'native-document';
|
|
255
|
+
|
|
256
|
+
Formatters.duration = (value, locale) => {
|
|
257
|
+
const hours = Math.floor(value / 3600);
|
|
258
|
+
const minutes = Math.floor((value % 3600) / 60);
|
|
259
|
+
return `${hours}h${minutes < 10 ? '0' : ''}${minutes}`;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const duration = Observable(3661);
|
|
263
|
+
duration.format('duration'); // "1h01"
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Available Format Types
|
|
267
|
+
|
|
268
|
+
| Type | Input | Options |
|
|
269
|
+
|------|-------|---------|
|
|
270
|
+
| `currency` | `number` | `currency`, `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
|
|
271
|
+
| `number` | `number` | `notation`, `minimumFractionDigits`, `maximumFractionDigits` |
|
|
272
|
+
| `percent` | `number` | `decimals` |
|
|
273
|
+
| `date` | `Date \| number` | `dateStyle`, `format` |
|
|
274
|
+
| `time` | `Date \| number` | `hour`, `minute`, `second`, `format` |
|
|
275
|
+
| `datetime` | `Date \| number` | `dateStyle`, `hour`, `minute`, `second`, `format` |
|
|
276
|
+
| `relative` | `Date \| number` | `unit`, `numeric` |
|
|
277
|
+
| `plural` | `number` | `singular`, `plural` |
|
|
278
|
+
|
|
279
|
+
## Observable Objects vs Simple Objects
|
|
173
280
|
|
|
174
281
|
```javascript
|
|
175
282
|
// Observable.object() creates a PROXY with reactive properties
|
|
176
283
|
const userProxy = Observable.object({
|
|
177
|
-
|
|
178
|
-
|
|
284
|
+
name: "Alice",
|
|
285
|
+
age: 25
|
|
179
286
|
});
|
|
180
287
|
|
|
181
288
|
// Each property is an individual observable
|
|
@@ -183,18 +290,17 @@ console.log(userProxy.name.val()); // "Alice"
|
|
|
183
290
|
userProxy.name.set("Bob");
|
|
184
291
|
|
|
185
292
|
// Get all values as plain object
|
|
186
|
-
console.log(userProxy.$value);
|
|
187
|
-
console.log(
|
|
293
|
+
console.log(userProxy.$value); // { name: "Bob", age: 25 }
|
|
294
|
+
console.log(userProxy.val()); // { name: "Bob", age: 25 }
|
|
188
295
|
|
|
189
296
|
// Observable(object) creates a SINGLE observable containing the whole object
|
|
190
297
|
const userSingle = Observable({
|
|
191
|
-
|
|
192
|
-
|
|
298
|
+
name: "Alice",
|
|
299
|
+
age: 25
|
|
193
300
|
});
|
|
194
301
|
|
|
195
|
-
// The entire object is the observable value
|
|
196
302
|
console.log(userSingle.val()); // { name: "Alice", age: 25 }
|
|
197
|
-
userSingle.set({ name: "Bob", age: 30 });
|
|
303
|
+
userSingle.set({ name: "Bob", age: 30 });
|
|
198
304
|
```
|
|
199
305
|
|
|
200
306
|
**Observable.object is an alias:**
|
|
@@ -203,66 +309,75 @@ userSingle.set({ name: "Bob", age: 30 }); // Replace entire object
|
|
|
203
309
|
Observable.object(data) === Observable.json(data) === Observable.init(data)
|
|
204
310
|
```
|
|
205
311
|
|
|
206
|
-
## Working with Observable
|
|
312
|
+
## Working with Object Observable
|
|
207
313
|
|
|
208
314
|
```javascript
|
|
209
315
|
const user = Observable.object({
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
316
|
+
name: "Alice",
|
|
317
|
+
age: 25,
|
|
318
|
+
email: "alice@example.com"
|
|
213
319
|
});
|
|
214
320
|
|
|
215
|
-
// Access individual properties
|
|
216
|
-
console.log(user.name.val());
|
|
217
|
-
console.log(user.name.$value);
|
|
321
|
+
// Access individual properties
|
|
322
|
+
console.log(user.name.val()); // "Alice"
|
|
323
|
+
console.log(user.name.$value); // "Alice"
|
|
218
324
|
|
|
219
325
|
// Update individual properties
|
|
220
326
|
user.name.set("Bob");
|
|
221
|
-
user.age.$value = 30;
|
|
327
|
+
user.age.$value = 30;
|
|
222
328
|
|
|
223
329
|
// Get the complete object value
|
|
224
|
-
console.log(user.$value);
|
|
225
|
-
console.log(
|
|
330
|
+
console.log(user.$value); // { name: "Bob", age: 30, email: "alice@example.com" }
|
|
331
|
+
console.log(user.val()); // Same as above
|
|
226
332
|
|
|
227
333
|
// Listen to individual property changes
|
|
228
334
|
user.name.subscribe(newName => {
|
|
229
|
-
|
|
335
|
+
console.log("New name:", newName);
|
|
230
336
|
});
|
|
231
337
|
|
|
232
|
-
// Update multiple properties
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
338
|
+
// Update multiple properties at once
|
|
339
|
+
user.set({
|
|
340
|
+
name: "Charlie",
|
|
341
|
+
age: 35
|
|
236
342
|
});
|
|
237
343
|
```
|
|
238
344
|
|
|
239
345
|
```javascript
|
|
240
346
|
const todos = Observable.array([
|
|
241
|
-
|
|
242
|
-
|
|
347
|
+
"Buy groceries",
|
|
348
|
+
"Call doctor"
|
|
243
349
|
]);
|
|
244
350
|
|
|
245
|
-
// Add elements
|
|
246
351
|
todos.push("Clean house");
|
|
247
|
-
|
|
248
|
-
// Remove last element
|
|
249
352
|
todos.pop();
|
|
250
353
|
|
|
251
|
-
// Use array methods
|
|
252
354
|
const completed = todos.filter(todo => todo.includes("✓"));
|
|
253
355
|
```
|
|
254
356
|
|
|
357
|
+
### Subscribing to an Observable Object
|
|
358
|
+
|
|
359
|
+
Subscribing to an `Observable.object()` reacts to changes on any individual property — you don't need to subscribe to each property separately.
|
|
360
|
+
```javascript
|
|
361
|
+
const user = Observable.object({ name: 'Alice', age: 25 });
|
|
362
|
+
|
|
363
|
+
user.subscribe(value => {
|
|
364
|
+
console.log('User changed:', value); // { name: 'Bob', age: 25 }
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
user.name.set('Bob'); // triggers the parent subscribe
|
|
368
|
+
user.age.set(30); // triggers the parent subscribe
|
|
369
|
+
```
|
|
370
|
+
|
|
255
371
|
## Computed Observables
|
|
256
372
|
|
|
257
373
|
Computed observables automatically recalculate when their dependencies change.
|
|
258
374
|
|
|
259
375
|
```javascript
|
|
260
376
|
const firstName = Observable("John");
|
|
261
|
-
const lastName
|
|
377
|
+
const lastName = Observable("Doe");
|
|
262
378
|
|
|
263
|
-
// Updates automatically
|
|
264
379
|
const fullName = Observable.computed(() => {
|
|
265
|
-
|
|
380
|
+
return `${firstName.val()} ${lastName.val()}`;
|
|
266
381
|
}, [firstName, lastName]);
|
|
267
382
|
|
|
268
383
|
console.log(fullName.val()); // "John Doe"
|
|
@@ -279,31 +394,29 @@ const count = Observable(0);
|
|
|
279
394
|
const increment = () => count.set(count.val() + 1);
|
|
280
395
|
const decrement = () => (count.$value--);
|
|
281
396
|
|
|
282
|
-
// Reactive interface
|
|
283
397
|
const app = Div({ class: "counter" }, [
|
|
284
398
|
Button("-").nd.onClick(decrement),
|
|
285
|
-
Span({ class: "count" }, count),
|
|
399
|
+
Span({ class: "count" }, count),
|
|
286
400
|
Button("+").nd.onClick(increment)
|
|
287
401
|
]);
|
|
288
402
|
```
|
|
289
403
|
|
|
290
|
-
|
|
404
|
+
## Batching Operations
|
|
291
405
|
|
|
292
|
-
Batching is a performance optimization technique that delays notifications to **dependent
|
|
406
|
+
Batching is a performance optimization technique that delays notifications to **dependent computed observables** until the end of a batch operation. Individual observable subscribers still receive their notifications immediately.
|
|
293
407
|
|
|
294
408
|
### Understanding Batch Behavior
|
|
295
409
|
|
|
296
410
|
```javascript
|
|
297
411
|
const name = Observable("John");
|
|
298
|
-
const age
|
|
412
|
+
const age = Observable(25);
|
|
299
413
|
|
|
300
|
-
// Direct subscribers always get immediate notifications
|
|
301
414
|
name.subscribe(value => console.log("Name changed to:", value));
|
|
302
|
-
age.subscribe(value
|
|
415
|
+
age.subscribe(value => console.log("Age changed to:", value));
|
|
303
416
|
|
|
304
417
|
const updateProfile = Observable.batch(() => {
|
|
305
|
-
name.set("Alice");
|
|
306
|
-
age.set(30);
|
|
418
|
+
name.set("Alice"); // Logs: "Name changed to: Alice"
|
|
419
|
+
age.set(30); // Logs: "Age changed to: 30"
|
|
307
420
|
});
|
|
308
421
|
|
|
309
422
|
updateProfile(); // Individual subscribers are notified immediately
|
|
@@ -311,34 +424,28 @@ updateProfile(); // Individual subscribers are notified immediately
|
|
|
311
424
|
|
|
312
425
|
### Batching with Computed Dependencies
|
|
313
426
|
|
|
314
|
-
The real power of batching shows when computed observables depend on the batch function:
|
|
315
|
-
|
|
316
427
|
```javascript
|
|
317
428
|
const firstName = Observable("John");
|
|
318
|
-
const lastName
|
|
429
|
+
const lastName = Observable("Doe");
|
|
319
430
|
|
|
320
|
-
// Direct subscribers get immediate notifications
|
|
321
431
|
firstName.subscribe(name => console.log("First name:", name));
|
|
322
|
-
lastName.subscribe(name
|
|
432
|
+
lastName.subscribe(name => console.log("Last name:", name));
|
|
323
433
|
|
|
324
|
-
// Batch function for name updates
|
|
325
434
|
const updateName = Observable.batch((first, last) => {
|
|
326
435
|
firstName.set(first); // Logs: "First name: Alice"
|
|
327
436
|
lastName.set(last); // Logs: "Last name: Smith"
|
|
328
437
|
});
|
|
329
438
|
|
|
330
|
-
// Computed that depends on the BATCH FUNCTION (not individual observables)
|
|
331
439
|
const fullName = Observable.computed(() => {
|
|
332
440
|
return `${firstName.val()} ${lastName.val()}`;
|
|
333
441
|
}, updateName); // ← Depends on the batch function
|
|
334
442
|
|
|
335
443
|
fullName.subscribe(name => console.log("Full name:", name));
|
|
336
444
|
|
|
337
|
-
// When we call the batch:
|
|
338
445
|
updateName("Alice", "Smith");
|
|
339
446
|
// Logs:
|
|
340
|
-
// "First name: Alice"
|
|
341
|
-
// "Last name: Smith"
|
|
447
|
+
// "First name: Alice" ← immediate
|
|
448
|
+
// "Last name: Smith" ← immediate
|
|
342
449
|
// "Full name: Alice Smith" ← single notification at the end
|
|
343
450
|
```
|
|
344
451
|
|
|
@@ -348,12 +455,12 @@ updateName("Alice", "Smith");
|
|
|
348
455
|
const score = Observable(0);
|
|
349
456
|
const lives = Observable(3);
|
|
350
457
|
|
|
351
|
-
// Method 1:
|
|
458
|
+
// Method 1: depends on individual observables — recalculates twice
|
|
352
459
|
const gameStatus1 = Observable.computed(() => {
|
|
353
460
|
return `Score: ${score.val()}, Lives: ${lives.val()}`;
|
|
354
|
-
}, [score, lives]);
|
|
461
|
+
}, [score, lives]);
|
|
355
462
|
|
|
356
|
-
// Method 2:
|
|
463
|
+
// Method 2: depends on batch function — recalculates once
|
|
357
464
|
const updateGame = Observable.batch(() => {
|
|
358
465
|
score.set(score.val() + 100);
|
|
359
466
|
lives.set(lives.val() - 1);
|
|
@@ -361,48 +468,37 @@ const updateGame = Observable.batch(() => {
|
|
|
361
468
|
|
|
362
469
|
const gameStatus2 = Observable.computed(() => {
|
|
363
470
|
return `Score: ${score.val()}, Lives: ${lives.val()}`;
|
|
364
|
-
}, updateGame);
|
|
471
|
+
}, updateGame);
|
|
365
472
|
|
|
366
|
-
//
|
|
367
|
-
|
|
368
|
-
lives.set(2); // gameStatus1 recalculates again
|
|
473
|
+
score.set(100); // gameStatus1 recalculates
|
|
474
|
+
lives.set(2); // gameStatus1 recalculates again
|
|
369
475
|
|
|
370
|
-
//
|
|
371
|
-
updateGame(); // gameStatus2 recalculates only at the end
|
|
476
|
+
updateGame(); // gameStatus2 recalculates only once
|
|
372
477
|
```
|
|
373
478
|
|
|
374
479
|
### Practical Example: Shopping Cart
|
|
375
480
|
|
|
376
481
|
```javascript
|
|
377
|
-
const items
|
|
378
|
-
const discount
|
|
482
|
+
const items = Observable.array([]);
|
|
483
|
+
const discount = Observable(0);
|
|
379
484
|
const shippingCost = Observable(0);
|
|
380
485
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
console.log('Items count : '+items.length);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
discount.subscribe(discount => {
|
|
387
|
-
console.log(`Discount: ${discount}%`);
|
|
388
|
-
});
|
|
486
|
+
items.subscribe(items => console.log('Items count: ' + items.length));
|
|
487
|
+
discount.subscribe(discount => console.log(`Discount: ${discount}%`));
|
|
389
488
|
|
|
390
|
-
// Batch function for cart operations
|
|
391
489
|
const updateCart = Observable.batch((cartData) => {
|
|
392
|
-
items.splice(0);
|
|
490
|
+
items.splice(0);
|
|
393
491
|
cartData.items.forEach(item => items.push(item));
|
|
394
492
|
discount.set(cartData.discount);
|
|
395
493
|
shippingCost.set(cartData.shipping);
|
|
396
494
|
});
|
|
397
495
|
|
|
398
|
-
// Expensive calculation that should only run after complete cart updates
|
|
399
496
|
const cartTotal = Observable.computed(() => {
|
|
400
|
-
const itemsTotal
|
|
401
|
-
const discountAmount
|
|
497
|
+
const itemsTotal = items.val().reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
|
498
|
+
const discountAmount = itemsTotal * (discount.val() / 100);
|
|
402
499
|
return itemsTotal - discountAmount + shippingCost.val();
|
|
403
|
-
}, updateCart);
|
|
500
|
+
}, updateCart);
|
|
404
501
|
|
|
405
|
-
// Example usage
|
|
406
502
|
updateCart({
|
|
407
503
|
items: [
|
|
408
504
|
{ name: "Product A", price: 29.99, quantity: 2 },
|
|
@@ -411,161 +507,77 @@ updateCart({
|
|
|
411
507
|
discount: 10,
|
|
412
508
|
shipping: 5.99
|
|
413
509
|
});
|
|
414
|
-
// Individual subscribers fire immediately, cartTotal calculates once at the end
|
|
415
510
|
```
|
|
416
511
|
|
|
417
512
|
### Async Batching
|
|
418
513
|
|
|
419
|
-
Batch functions handle asynchronous operations, delaying dependent notifications until the promise resolves:
|
|
420
|
-
|
|
421
514
|
```javascript
|
|
422
515
|
const isLoading = Observable(false);
|
|
423
|
-
const userData
|
|
424
|
-
const error
|
|
516
|
+
const userData = Observable(null);
|
|
517
|
+
const error = Observable(null);
|
|
425
518
|
|
|
426
|
-
|
|
427
|
-
isLoading.subscribe(loading => {
|
|
428
|
-
console.log('Loading.....');
|
|
429
|
-
});
|
|
519
|
+
isLoading.subscribe(loading => console.log('Loading.....'));
|
|
430
520
|
|
|
431
521
|
const fetchUser = Observable.batch(async (userId) => {
|
|
432
|
-
isLoading.set(true);
|
|
433
|
-
error.set(null);
|
|
434
|
-
|
|
522
|
+
isLoading.set(true);
|
|
523
|
+
error.set(null);
|
|
524
|
+
|
|
435
525
|
try {
|
|
436
526
|
const response = await fetch(`/api/users/${userId}`);
|
|
437
|
-
const data
|
|
438
|
-
userData.set(data);
|
|
527
|
+
const data = await response.json();
|
|
528
|
+
userData.set(data);
|
|
439
529
|
} catch (err) {
|
|
440
|
-
error.set(err.message);
|
|
530
|
+
error.set(err.message);
|
|
441
531
|
} finally {
|
|
442
|
-
isLoading.set(false);
|
|
532
|
+
isLoading.set(false);
|
|
443
533
|
}
|
|
444
|
-
// Dependent computed observables are notified HERE
|
|
445
534
|
});
|
|
446
535
|
|
|
447
|
-
// This computed depends on the batch function
|
|
448
536
|
const userDisplay = Observable.computed(() => {
|
|
449
537
|
if (isLoading.val()) return "Loading...";
|
|
450
|
-
if (error.val())
|
|
451
|
-
if (userData.val())
|
|
538
|
+
if (error.val()) return `Error: ${error.val()}`;
|
|
539
|
+
if (userData.val()) return `Hello ${userData.val().name}`;
|
|
452
540
|
return "No user";
|
|
453
|
-
}, fetchUser);
|
|
541
|
+
}, fetchUser);
|
|
454
542
|
|
|
455
543
|
await fetchUser(123);
|
|
456
544
|
```
|
|
457
545
|
|
|
458
546
|
### Single Batch Dependency Only
|
|
459
547
|
|
|
460
|
-
|
|
548
|
+
Computed observables can only depend on **one batch function**, not multiple:
|
|
461
549
|
|
|
462
550
|
```javascript
|
|
463
|
-
const user = Observable.object({ name: "", email: "" });
|
|
464
|
-
const settings = Observable.object({ theme: "light", lang: "en" });
|
|
465
|
-
|
|
466
551
|
const updateProfile = Observable.batch((profileData) => {
|
|
467
552
|
user.name.set(profileData.name);
|
|
468
553
|
user.email.set(profileData.email);
|
|
469
554
|
settings.theme.set(profileData.theme);
|
|
470
|
-
settings.lang.set(profileData.lang);
|
|
471
555
|
});
|
|
472
556
|
|
|
473
|
-
// ✅
|
|
474
|
-
const profileSummary = Observable.computed(() => {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
};
|
|
480
|
-
}, updateProfile); // ← Single batch function
|
|
557
|
+
// ✅ Single batch dependency
|
|
558
|
+
const profileSummary = Observable.computed(() => ({
|
|
559
|
+
user: user.$value,
|
|
560
|
+
settings: settings.$value,
|
|
561
|
+
lastUpdated: Date.now()
|
|
562
|
+
}), updateProfile);
|
|
481
563
|
|
|
482
|
-
// ❌
|
|
564
|
+
// ❌ Not supported
|
|
483
565
|
// Observable.computed(callback, [batch1, batch2])
|
|
484
566
|
```
|
|
485
567
|
|
|
486
|
-
### Performance Benefits
|
|
487
|
-
|
|
488
|
-
```javascript
|
|
489
|
-
const items = Observable.array([]);
|
|
490
|
-
|
|
491
|
-
// Expensive computed operation
|
|
492
|
-
const expensiveCalculation = Observable.computed(() => {
|
|
493
|
-
console.log("🔄 Recalculating..."); // This helps visualize when it runs
|
|
494
|
-
return items.val()
|
|
495
|
-
.filter(item => item.active)
|
|
496
|
-
.map(item => item.price * item.quantity)
|
|
497
|
-
.reduce((sum, total) => sum + total, 0);
|
|
498
|
-
}, [items]); // ← Depends on individual observable
|
|
499
|
-
|
|
500
|
-
const batchUpdateItems = Observable.batch(() => {
|
|
501
|
-
items.push({ active: true, price: 10, quantity: 2 });
|
|
502
|
-
items.push({ active: true, price: 15, quantity: 1 });
|
|
503
|
-
items.push({ active: false, price: 20, quantity: 3 });
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
const optimizedCalculation = Observable.computed(() => {
|
|
507
|
-
console.log("✅ Optimized recalculation");
|
|
508
|
-
return items.val()
|
|
509
|
-
.filter(item => item.active)
|
|
510
|
-
.map(item => item.price * item.quantity)
|
|
511
|
-
.reduce((sum, total) => sum + total, 0);
|
|
512
|
-
}, batchUpdateItems); // ← Depends on batch function
|
|
513
|
-
|
|
514
|
-
// Without batching:
|
|
515
|
-
items.push({ active: true, price: 10, quantity: 2 }); // 🔄 Recalculating...
|
|
516
|
-
items.push({ active: true, price: 15, quantity: 1 }); // 🔄 Recalculating...
|
|
517
|
-
items.push({ active: false, price: 20, quantity: 3 }); // 🔄 Recalculating...
|
|
518
|
-
|
|
519
|
-
// With batching:
|
|
520
|
-
batchUpdateItems(); // ✅ Optimized recalculation (only once!)
|
|
521
|
-
```
|
|
522
|
-
|
|
523
568
|
### Best Practices
|
|
524
569
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
3. **Batch related operations**: Group logically connected updates that should trigger dependent computations together
|
|
530
|
-
|
|
531
|
-
4. **Don't over-batch**: Only use batching when you have computed observables that benefit from delayed updates
|
|
532
|
-
|
|
533
|
-
### Common Patterns
|
|
534
|
-
|
|
535
|
-
### State Machine with Batched Transitions
|
|
536
|
-
```javascript
|
|
537
|
-
const gameState = Observable.object({
|
|
538
|
-
level: 1,
|
|
539
|
-
score: 0,
|
|
540
|
-
lives: 3
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Individual subscribers for immediate UI updates
|
|
544
|
-
gameState.score.subscribe(score => updateScoreDisplay(score));
|
|
545
|
-
gameState.lives.subscribe(lives => updateLivesDisplay(lives));
|
|
546
|
-
|
|
547
|
-
// Batch function for state transitions
|
|
548
|
-
const levelUp = Observable.batch(() => {
|
|
549
|
-
gameState.level.set(gameState.level.val() + 1);
|
|
550
|
-
gameState.score.set(gameState.score.val() + 1000);
|
|
551
|
-
gameState.lives.set(gameState.lives.val() + 1);
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
// Complex computed that should only run after complete transitions
|
|
555
|
-
const gameStatusMessage = Observable.computed(() => {
|
|
556
|
-
const state = gameState.$value;
|
|
557
|
-
return `Level ${state.level}: ${state.score} points, ${state.lives} lives remaining`;
|
|
558
|
-
}, levelUp); // ← Only updates when levelUp() is called
|
|
559
|
-
```
|
|
570
|
+
- Use batch dependencies for expensive computations that shouldn't recalculate on every individual change
|
|
571
|
+
- Keep individual subscribers for immediate feedback (input validation, UI updates)
|
|
572
|
+
- Group logically connected updates that should trigger dependent computations together
|
|
573
|
+
- Don't over-batch — only use when computed observables benefit from delayed updates
|
|
560
574
|
|
|
561
575
|
### When NOT to Use Batch Dependencies
|
|
562
576
|
|
|
563
|
-
-
|
|
564
|
-
-
|
|
565
|
-
-
|
|
566
|
-
-
|
|
567
|
-
|
|
568
|
-
The key insight is that batching in NativeDocument is about **controlling when dependent computed observables recalculate**, not about suppressing individual observable notifications.
|
|
577
|
+
- Real-time updates where computed observables need to update immediately
|
|
578
|
+
- Simple computations where the cost is minimal
|
|
579
|
+
- Debugging contexts — batching can make the flow harder to trace
|
|
580
|
+
- Single observable changes — no benefit when only one observable changes
|
|
569
581
|
|
|
570
582
|
## String Templates with Observables
|
|
571
583
|
|
|
@@ -573,10 +585,10 @@ The key insight is that batching in NativeDocument is about **controlling when d
|
|
|
573
585
|
|
|
574
586
|
```javascript
|
|
575
587
|
const name = Observable("Alice");
|
|
576
|
-
const age
|
|
588
|
+
const age = Observable(25);
|
|
577
589
|
|
|
578
590
|
const template = "Hello ${name}, you are ${age} years old";
|
|
579
|
-
const message
|
|
591
|
+
const message = template.use({ name, age });
|
|
580
592
|
|
|
581
593
|
console.log(message.val()); // "Hello Alice, you are 25 years old"
|
|
582
594
|
|
|
@@ -588,25 +600,9 @@ console.log(message.val()); // "Hello Bob, you are 25 years old"
|
|
|
588
600
|
|
|
589
601
|
```javascript
|
|
590
602
|
const greeting = Observable("Hello");
|
|
591
|
-
const user
|
|
603
|
+
const user = Observable("Marie");
|
|
592
604
|
|
|
593
|
-
// Observables are automatically integrated
|
|
594
605
|
const element = Div(null, `${greeting} ${user}!`);
|
|
595
|
-
// Updates when greeting or user changes
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
## Observable Checkers
|
|
599
|
-
|
|
600
|
-
Create derived observables with conditions:
|
|
601
|
-
|
|
602
|
-
```javascript
|
|
603
|
-
const age = Observable(17);
|
|
604
|
-
const isAdult = age.check(value => value >= 18);
|
|
605
|
-
|
|
606
|
-
console.log(isAdult.val()); // false
|
|
607
|
-
|
|
608
|
-
age.set(20);
|
|
609
|
-
console.log(isAdult.val()); // true
|
|
610
606
|
```
|
|
611
607
|
|
|
612
608
|
## Memory Management
|
|
@@ -614,58 +610,46 @@ console.log(isAdult.val()); // true
|
|
|
614
610
|
```javascript
|
|
615
611
|
const data = Observable("test");
|
|
616
612
|
|
|
617
|
-
// Create a subscription
|
|
618
613
|
const handler = value => console.log(value);
|
|
619
614
|
data.subscribe(handler);
|
|
620
615
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
data.unsubscribe('test', handler);
|
|
616
|
+
// Remove specific subscription
|
|
617
|
+
data.unsubscribe(handler);
|
|
624
618
|
|
|
625
619
|
// Complete observable cleanup
|
|
626
620
|
data.cleanup(); // Removes all listeners and prevents new subscriptions
|
|
627
621
|
|
|
628
|
-
// Manual trigger
|
|
629
|
-
data.trigger();
|
|
622
|
+
// Manual trigger — forces update without changing the value
|
|
623
|
+
data.trigger();
|
|
630
624
|
|
|
631
625
|
// Extract values from any observable structure
|
|
632
|
-
const complexData = Observable.object({
|
|
633
|
-
|
|
634
|
-
items: [1, 2, 3]
|
|
635
|
-
});
|
|
636
|
-
console.log(Observable.value(complexData)); // Plain object with extracted values
|
|
626
|
+
const complexData = Observable.object({ user: "John", items: [1, 2, 3] });
|
|
627
|
+
console.log(complexData.val()); // Plain object with extracted values
|
|
637
628
|
```
|
|
638
629
|
|
|
639
630
|
## Utility Methods
|
|
640
631
|
|
|
641
|
-
### `off(value, callback?)`
|
|
632
|
+
### `off(value, callback?)` — Remove Watchers
|
|
642
633
|
|
|
643
|
-
Remove specific value watchers created with `.on()`:
|
|
644
634
|
```javascript
|
|
645
635
|
const status = Observable("idle");
|
|
646
636
|
|
|
647
637
|
const loadingHandler = (isActive) => console.log("Loading:", isActive);
|
|
648
638
|
status.on("loading", loadingHandler);
|
|
649
639
|
|
|
650
|
-
// Remove specific callback
|
|
651
|
-
status.off("loading"
|
|
652
|
-
|
|
653
|
-
// Remove all watchers for a value
|
|
654
|
-
status.off("loading");
|
|
640
|
+
status.off("loading", loadingHandler); // Remove specific callback
|
|
641
|
+
status.off("loading"); // Remove all watchers for this value
|
|
655
642
|
```
|
|
656
643
|
|
|
657
|
-
### `once(predicate, callback)`
|
|
644
|
+
### `once(predicate, callback)` — Single-Time Listener
|
|
658
645
|
|
|
659
|
-
Execute callback only once when condition is met:
|
|
660
646
|
```javascript
|
|
661
647
|
const count = Observable(0);
|
|
662
648
|
|
|
663
|
-
// Wait for specific value
|
|
664
649
|
count.once(5, (value) => {
|
|
665
650
|
console.log("Reached 5!"); // Only called once
|
|
666
651
|
});
|
|
667
652
|
|
|
668
|
-
// With predicate function
|
|
669
653
|
count.once(val => val > 10, (value) => {
|
|
670
654
|
console.log("Greater than 10!"); // Only called once
|
|
671
655
|
});
|
|
@@ -674,108 +658,142 @@ count.set(5); // Callback fires and unsubscribes
|
|
|
674
658
|
count.set(5); // Callback doesn't fire again
|
|
675
659
|
```
|
|
676
660
|
|
|
677
|
-
### `toggle()`
|
|
661
|
+
### `toggle()` — Boolean Toggle
|
|
678
662
|
|
|
679
|
-
Toggle boolean observables:
|
|
680
663
|
```javascript
|
|
681
664
|
const isVisible = Observable(false);
|
|
682
665
|
|
|
683
666
|
isVisible.toggle(); // true
|
|
684
667
|
isVisible.toggle(); // false
|
|
685
|
-
isVisible.toggle(); // true
|
|
686
668
|
|
|
687
|
-
// Useful with buttons
|
|
688
669
|
Button("Toggle").nd.onClick(() => isVisible.toggle());
|
|
689
670
|
```
|
|
690
671
|
|
|
691
|
-
### `reset()`
|
|
672
|
+
### `reset()` — Reset to Initial Value
|
|
692
673
|
|
|
693
|
-
Reset observable to its initial value (requires `reset: true` config):
|
|
694
674
|
```javascript
|
|
695
675
|
const name = Observable("Alice", { reset: true });
|
|
696
676
|
|
|
697
677
|
name.set("Bob");
|
|
698
|
-
console.log(name.val()); // "Bob"
|
|
699
|
-
|
|
700
678
|
name.reset();
|
|
701
|
-
console.log(name.val()); // "Alice"
|
|
679
|
+
console.log(name.val()); // "Alice"
|
|
702
680
|
|
|
703
|
-
// With objects
|
|
704
681
|
const user = Observable({ name: "Alice", age: 25 }, { reset: true });
|
|
705
682
|
user.set({ name: "Bob", age: 30 });
|
|
706
683
|
user.reset(); // Back to { name: "Alice", age: 25 }
|
|
707
684
|
```
|
|
708
685
|
|
|
709
|
-
### `equals(other)`
|
|
686
|
+
### `equals(other)` — Value Comparison
|
|
710
687
|
|
|
711
|
-
Compare observable values:
|
|
712
688
|
```javascript
|
|
713
689
|
const num1 = Observable(5);
|
|
714
690
|
const num2 = Observable(5);
|
|
715
691
|
const num3 = Observable(10);
|
|
716
692
|
|
|
717
|
-
console.log(num1.equals(num2)); // true
|
|
718
|
-
console.log(num1.equals(5)); // true
|
|
693
|
+
console.log(num1.equals(num2)); // true
|
|
694
|
+
console.log(num1.equals(5)); // true
|
|
719
695
|
console.log(num1.equals(num3)); // false
|
|
720
696
|
```
|
|
721
697
|
|
|
722
|
-
### `toBool()`
|
|
698
|
+
### `toBool()` — Boolean Conversion
|
|
723
699
|
|
|
724
|
-
Convert observable value to boolean:
|
|
725
700
|
```javascript
|
|
726
701
|
const text = Observable("");
|
|
727
702
|
console.log(text.toBool()); // false
|
|
728
703
|
|
|
729
704
|
text.set("Hello");
|
|
730
705
|
console.log(text.toBool()); // true
|
|
731
|
-
|
|
732
|
-
// Useful for conditions
|
|
733
|
-
const hasContent = text.toBool();
|
|
734
706
|
```
|
|
735
707
|
|
|
736
|
-
### `intercept(callback)`
|
|
708
|
+
### `intercept(callback)` — Value Interception
|
|
737
709
|
|
|
738
|
-
Intercept and modify values before they're set:
|
|
739
710
|
```javascript
|
|
740
711
|
const age = Observable(0);
|
|
741
712
|
|
|
742
|
-
// Intercept sets to enforce constraints
|
|
743
713
|
age.intercept((newValue, oldValue) => {
|
|
744
|
-
if (newValue < 0)
|
|
714
|
+
if (newValue < 0) return 0;
|
|
745
715
|
if (newValue > 120) return 120;
|
|
746
716
|
return newValue;
|
|
747
717
|
});
|
|
748
718
|
|
|
749
|
-
age.set(-5); //
|
|
750
|
-
age.set(150); //
|
|
719
|
+
age.set(-5); // Sets 0
|
|
720
|
+
age.set(150); // Sets 120
|
|
751
721
|
age.set(25); // Sets 25
|
|
752
722
|
|
|
753
|
-
// Practical example: sanitize input
|
|
754
723
|
const username = Observable("");
|
|
755
|
-
username.intercept((value) =>
|
|
756
|
-
return value.toLowerCase().trim();
|
|
757
|
-
});
|
|
724
|
+
username.intercept((value) => value.toLowerCase().trim());
|
|
758
725
|
|
|
759
|
-
username.set(" JohnDoe ");
|
|
726
|
+
username.set(" JohnDoe ");
|
|
760
727
|
console.log(username.val()); // "johndoe"
|
|
761
728
|
```
|
|
762
729
|
|
|
730
|
+
### `persist(key, options?)` — Persist to localStorage
|
|
731
|
+
|
|
732
|
+
Binds an observable to localStorage. The value is automatically restored on load and saved on every change.
|
|
733
|
+
```javascript
|
|
734
|
+
// Simple persistence — key defaults to the variable name
|
|
735
|
+
const theme = Observable('light').persist('theme');
|
|
736
|
+
theme.set('dark'); // saved to localStorage automatically
|
|
737
|
+
|
|
738
|
+
// On next page load
|
|
739
|
+
const theme = Observable('light').persist('theme');
|
|
740
|
+
theme.val(); // "dark" — restored from localStorage
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
With transform options — useful when the stored format differs from the runtime format:
|
|
744
|
+
```javascript
|
|
745
|
+
// Store as ISO string, restore as Date object
|
|
746
|
+
const selectedDate = Observable(new Date()).persist('event:date', {
|
|
747
|
+
get: value => new Date(value), // transform on load
|
|
748
|
+
set: value => value.toISOString() // transform on save
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Store only the user id, restore the full object later
|
|
752
|
+
const user = Observable(null).persist('session:user', {
|
|
753
|
+
get: value => value ? JSON.parse(value) : null,
|
|
754
|
+
set: value => value ? JSON.stringify(value) : null
|
|
755
|
+
});
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Without Store** — use `.persist()` directly on any observable for component-level persistence without polluting the global store:
|
|
759
|
+
```javascript
|
|
760
|
+
const FiltersPanel = () => {
|
|
761
|
+
const expanded = Observable(false).persist('filters:expanded');
|
|
762
|
+
const columns = Observable(['name', 'date']).persist('table:columns');
|
|
763
|
+
|
|
764
|
+
return Div([/* ... */]);
|
|
765
|
+
};
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
### `deepSubscribe(callback)` — Deep Change Detection
|
|
769
|
+
|
|
770
|
+
Reacts to changes at any depth — array mutations, and property changes on nested observables.
|
|
771
|
+
```javascript
|
|
772
|
+
const tags = Observable.array([{ label: 'admin' }]);
|
|
773
|
+
|
|
774
|
+
const unsub = tags.deepSubscribe(value => console.log('changed:', value));
|
|
775
|
+
|
|
776
|
+
tags.push({ label: 'editor' }); // ✅ déclenche
|
|
777
|
+
tags[0].label.set('superadmin'); // ✅ déclenche
|
|
778
|
+
tags.splice(0, 1); // ✅ déclenche + cleanup du listener
|
|
779
|
+
|
|
780
|
+
// Cleanup
|
|
781
|
+
unsub();
|
|
782
|
+
```
|
|
783
|
+
|
|
763
784
|
## Best Practices
|
|
764
785
|
|
|
765
786
|
1. **Use descriptive names** for your observables
|
|
766
787
|
2. **Understand the difference**: `Observable(object)` vs `Observable.object(object)`
|
|
767
|
-
3. **
|
|
768
|
-
4. **
|
|
769
|
-
5. **
|
|
770
|
-
6. **
|
|
771
|
-
7. **
|
|
772
|
-
8. **
|
|
773
|
-
9. **Avoid** direct modifications in subscription callbacks
|
|
788
|
+
3. **Group related data** with `Observable.object()` for individual property reactivity
|
|
789
|
+
4. **Use `Observable.value()`** to extract plain values from complex structures
|
|
790
|
+
5. **Prefer computed** for derived values
|
|
791
|
+
6. **Clean up** unused observables to prevent memory leaks
|
|
792
|
+
7. **Use `trigger()`** when you need to force updates without value changes
|
|
793
|
+
8. **Avoid** direct modifications in subscription callbacks
|
|
774
794
|
|
|
775
795
|
## Next Steps
|
|
776
796
|
|
|
777
|
-
Now that you understand NativeDocument's observable, explore these advanced topics:
|
|
778
|
-
|
|
779
797
|
- **[Elements](elements.md)** - Creating and composing UI
|
|
780
798
|
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
781
799
|
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
@@ -792,4 +810,4 @@ Now that you understand NativeDocument's observable, explore these advanced topi
|
|
|
792
810
|
|
|
793
811
|
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
794
812
|
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
795
|
-
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|
|
813
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|