native-document 1.0.95 → 1.0.98
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/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 +1922 -1277
- package/dist/native-document.dev.js +1985 -1401
- 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 +310 -306
- package/docs/state-management.md +198 -193
- package/package.json +1 -1
- package/readme.md +1 -1
- package/src/core/data/ObservableChecker.js +2 -0
- package/src/core/data/ObservableItem.js +97 -0
- package/src/core/data/ObservableObject.js +182 -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/devtools.js +9 -0
- package/src/fetch/NativeFetch.js +5 -2
- package/types/observable.d.ts +71 -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/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.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:**
|
|
@@ -207,62 +313,71 @@ Observable.object(data) === Observable.json(data) === Observable.init(data)
|
|
|
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,84 +600,57 @@ 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
606
|
// Updates when greeting or user changes
|
|
596
607
|
```
|
|
597
608
|
|
|
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
|
-
```
|
|
611
|
-
|
|
612
609
|
## Memory Management
|
|
613
610
|
|
|
614
611
|
```javascript
|
|
615
612
|
const data = Observable("test");
|
|
616
613
|
|
|
617
|
-
// Create a subscription
|
|
618
614
|
const handler = value => console.log(value);
|
|
619
615
|
data.subscribe(handler);
|
|
620
616
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
data.unsubscribe('test', handler);
|
|
617
|
+
// Remove specific subscription
|
|
618
|
+
data.unsubscribe(handler);
|
|
624
619
|
|
|
625
620
|
// Complete observable cleanup
|
|
626
621
|
data.cleanup(); // Removes all listeners and prevents new subscriptions
|
|
627
622
|
|
|
628
|
-
// Manual trigger
|
|
629
|
-
data.trigger();
|
|
623
|
+
// Manual trigger — forces update without changing the value
|
|
624
|
+
data.trigger();
|
|
630
625
|
|
|
631
626
|
// 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
|
|
627
|
+
const complexData = Observable.object({ user: "John", items: [1, 2, 3] });
|
|
628
|
+
console.log(complexData.val()); // Plain object with extracted values
|
|
637
629
|
```
|
|
638
630
|
|
|
639
631
|
## Utility Methods
|
|
640
632
|
|
|
641
|
-
### `off(value, callback?)`
|
|
633
|
+
### `off(value, callback?)` — Remove Watchers
|
|
642
634
|
|
|
643
|
-
Remove specific value watchers created with `.on()`:
|
|
644
635
|
```javascript
|
|
645
636
|
const status = Observable("idle");
|
|
646
637
|
|
|
647
638
|
const loadingHandler = (isActive) => console.log("Loading:", isActive);
|
|
648
639
|
status.on("loading", loadingHandler);
|
|
649
640
|
|
|
650
|
-
// Remove specific callback
|
|
651
|
-
status.off("loading"
|
|
652
|
-
|
|
653
|
-
// Remove all watchers for a value
|
|
654
|
-
status.off("loading");
|
|
641
|
+
status.off("loading", loadingHandler); // Remove specific callback
|
|
642
|
+
status.off("loading"); // Remove all watchers for this value
|
|
655
643
|
```
|
|
656
644
|
|
|
657
|
-
### `once(predicate, callback)`
|
|
645
|
+
### `once(predicate, callback)` — Single-Time Listener
|
|
658
646
|
|
|
659
|
-
Execute callback only once when condition is met:
|
|
660
647
|
```javascript
|
|
661
648
|
const count = Observable(0);
|
|
662
649
|
|
|
663
|
-
// Wait for specific value
|
|
664
650
|
count.once(5, (value) => {
|
|
665
651
|
console.log("Reached 5!"); // Only called once
|
|
666
652
|
});
|
|
667
653
|
|
|
668
|
-
// With predicate function
|
|
669
654
|
count.once(val => val > 10, (value) => {
|
|
670
655
|
console.log("Greater than 10!"); // Only called once
|
|
671
656
|
});
|
|
@@ -674,92 +659,113 @@ count.set(5); // Callback fires and unsubscribes
|
|
|
674
659
|
count.set(5); // Callback doesn't fire again
|
|
675
660
|
```
|
|
676
661
|
|
|
677
|
-
### `toggle()`
|
|
662
|
+
### `toggle()` — Boolean Toggle
|
|
678
663
|
|
|
679
|
-
Toggle boolean observables:
|
|
680
664
|
```javascript
|
|
681
665
|
const isVisible = Observable(false);
|
|
682
666
|
|
|
683
667
|
isVisible.toggle(); // true
|
|
684
668
|
isVisible.toggle(); // false
|
|
685
|
-
isVisible.toggle(); // true
|
|
686
669
|
|
|
687
|
-
// Useful with buttons
|
|
688
670
|
Button("Toggle").nd.onClick(() => isVisible.toggle());
|
|
689
671
|
```
|
|
690
672
|
|
|
691
|
-
### `reset()`
|
|
673
|
+
### `reset()` — Reset to Initial Value
|
|
692
674
|
|
|
693
|
-
Reset observable to its initial value (requires `reset: true` config):
|
|
694
675
|
```javascript
|
|
695
676
|
const name = Observable("Alice", { reset: true });
|
|
696
677
|
|
|
697
678
|
name.set("Bob");
|
|
698
|
-
console.log(name.val()); // "Bob"
|
|
699
|
-
|
|
700
679
|
name.reset();
|
|
701
|
-
console.log(name.val()); // "Alice"
|
|
680
|
+
console.log(name.val()); // "Alice"
|
|
702
681
|
|
|
703
|
-
// With objects
|
|
704
682
|
const user = Observable({ name: "Alice", age: 25 }, { reset: true });
|
|
705
683
|
user.set({ name: "Bob", age: 30 });
|
|
706
684
|
user.reset(); // Back to { name: "Alice", age: 25 }
|
|
707
685
|
```
|
|
708
686
|
|
|
709
|
-
### `equals(other)`
|
|
687
|
+
### `equals(other)` — Value Comparison
|
|
710
688
|
|
|
711
|
-
Compare observable values:
|
|
712
689
|
```javascript
|
|
713
690
|
const num1 = Observable(5);
|
|
714
691
|
const num2 = Observable(5);
|
|
715
692
|
const num3 = Observable(10);
|
|
716
693
|
|
|
717
|
-
console.log(num1.equals(num2)); // true
|
|
718
|
-
console.log(num1.equals(5)); // true
|
|
694
|
+
console.log(num1.equals(num2)); // true
|
|
695
|
+
console.log(num1.equals(5)); // true
|
|
719
696
|
console.log(num1.equals(num3)); // false
|
|
720
697
|
```
|
|
721
698
|
|
|
722
|
-
### `toBool()`
|
|
699
|
+
### `toBool()` — Boolean Conversion
|
|
723
700
|
|
|
724
|
-
Convert observable value to boolean:
|
|
725
701
|
```javascript
|
|
726
702
|
const text = Observable("");
|
|
727
703
|
console.log(text.toBool()); // false
|
|
728
704
|
|
|
729
705
|
text.set("Hello");
|
|
730
706
|
console.log(text.toBool()); // true
|
|
731
|
-
|
|
732
|
-
// Useful for conditions
|
|
733
|
-
const hasContent = text.toBool();
|
|
734
707
|
```
|
|
735
708
|
|
|
736
|
-
### `intercept(callback)`
|
|
709
|
+
### `intercept(callback)` — Value Interception
|
|
737
710
|
|
|
738
|
-
Intercept and modify values before they're set:
|
|
739
711
|
```javascript
|
|
740
712
|
const age = Observable(0);
|
|
741
713
|
|
|
742
|
-
// Intercept sets to enforce constraints
|
|
743
714
|
age.intercept((newValue, oldValue) => {
|
|
744
|
-
if (newValue < 0)
|
|
715
|
+
if (newValue < 0) return 0;
|
|
745
716
|
if (newValue > 120) return 120;
|
|
746
717
|
return newValue;
|
|
747
718
|
});
|
|
748
719
|
|
|
749
|
-
age.set(-5); //
|
|
750
|
-
age.set(150); //
|
|
720
|
+
age.set(-5); // Sets 0
|
|
721
|
+
age.set(150); // Sets 120
|
|
751
722
|
age.set(25); // Sets 25
|
|
752
723
|
|
|
753
|
-
// Practical example: sanitize input
|
|
754
724
|
const username = Observable("");
|
|
755
|
-
username.intercept((value) =>
|
|
756
|
-
return value.toLowerCase().trim();
|
|
757
|
-
});
|
|
725
|
+
username.intercept((value) => value.toLowerCase().trim());
|
|
758
726
|
|
|
759
|
-
username.set(" JohnDoe ");
|
|
727
|
+
username.set(" JohnDoe ");
|
|
760
728
|
console.log(username.val()); // "johndoe"
|
|
761
729
|
```
|
|
762
730
|
|
|
731
|
+
### `persist(key, options?)` — Persist to localStorage
|
|
732
|
+
|
|
733
|
+
Binds an observable to localStorage. The value is automatically restored on load and saved on every change.
|
|
734
|
+
```javascript
|
|
735
|
+
// Simple persistence — key defaults to the variable name
|
|
736
|
+
const theme = Observable('light').persist('theme');
|
|
737
|
+
theme.set('dark'); // saved to localStorage automatically
|
|
738
|
+
|
|
739
|
+
// On next page load
|
|
740
|
+
const theme = Observable('light').persist('theme');
|
|
741
|
+
theme.val(); // "dark" — restored from localStorage
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
With transform options — useful when the stored format differs from the runtime format:
|
|
745
|
+
```javascript
|
|
746
|
+
// Store as ISO string, restore as Date object
|
|
747
|
+
const selectedDate = Observable(new Date()).persist('event:date', {
|
|
748
|
+
get: value => new Date(value), // transform on load
|
|
749
|
+
set: value => value.toISOString() // transform on save
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
// Store only the user id, restore the full object later
|
|
753
|
+
const user = Observable(null).persist('session:user', {
|
|
754
|
+
get: value => value ? JSON.parse(value) : null,
|
|
755
|
+
set: value => value ? JSON.stringify(value) : null
|
|
756
|
+
});
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
**Without Store** — use `.persist()` directly on any observable for component-level persistence without polluting the global store:
|
|
760
|
+
```javascript
|
|
761
|
+
const FiltersPanel = () => {
|
|
762
|
+
const expanded = Observable(false).persist('filters:expanded');
|
|
763
|
+
const columns = Observable(['name', 'date']).persist('table:columns');
|
|
764
|
+
|
|
765
|
+
return Div([/* ... */]);
|
|
766
|
+
};
|
|
767
|
+
```
|
|
768
|
+
|
|
763
769
|
## Best Practices
|
|
764
770
|
|
|
765
771
|
1. **Use descriptive names** for your observables
|
|
@@ -774,8 +780,6 @@ console.log(username.val()); // "johndoe"
|
|
|
774
780
|
|
|
775
781
|
## Next Steps
|
|
776
782
|
|
|
777
|
-
Now that you understand NativeDocument's observable, explore these advanced topics:
|
|
778
|
-
|
|
779
783
|
- **[Elements](elements.md)** - Creating and composing UI
|
|
780
784
|
- **[Conditional Rendering](conditional-rendering.md)** - Dynamic content
|
|
781
785
|
- **[List Rendering](list-rendering.md)** - (ForEach | ForEachArray) and dynamic lists
|
|
@@ -792,4 +796,4 @@ Now that you understand NativeDocument's observable, explore these advanced topi
|
|
|
792
796
|
|
|
793
797
|
- **[Cache](docs/utils/cache.md)** - Lazy initialization and singleton patterns
|
|
794
798
|
- **[NativeFetch](docs/utils/native-fetch.md)** - HTTP client with interceptors
|
|
795
|
-
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|
|
799
|
+
- **[Filters](docs/utils/filters.md)** - Data filtering helpers
|