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.
Files changed (43) hide show
  1. package/{src/devtools/hrm → devtools}/ComponentRegistry.js +2 -2
  2. package/devtools/index.js +8 -0
  3. package/{src/devtools/plugin.js → devtools/plugin/dev-tools-plugin.js} +2 -2
  4. package/{src/devtools/hrm/nd-vite-hot-reload.js → devtools/transformers/nd-vite-devtools.js} +16 -6
  5. package/devtools/transformers/src/transformComponentForHrm.js +74 -0
  6. package/devtools/transformers/src/transformJsFile.js +9 -0
  7. package/devtools/transformers/src/utils.js +79 -0
  8. package/devtools/widget/Widget.js +48 -0
  9. package/devtools/widget/widget.css +81 -0
  10. package/devtools/widget.js +23 -0
  11. package/dist/native-document.components.min.js +1922 -1277
  12. package/dist/native-document.dev.js +1985 -1401
  13. package/dist/native-document.dev.js.map +1 -1
  14. package/dist/native-document.devtools.min.js +1 -1
  15. package/dist/native-document.min.js +1 -1
  16. package/docs/cache.md +1 -1
  17. package/docs/core-concepts.md +1 -1
  18. package/docs/native-document-element.md +51 -15
  19. package/docs/observables.md +310 -306
  20. package/docs/state-management.md +198 -193
  21. package/package.json +1 -1
  22. package/readme.md +1 -1
  23. package/src/core/data/ObservableChecker.js +2 -0
  24. package/src/core/data/ObservableItem.js +97 -0
  25. package/src/core/data/ObservableObject.js +182 -0
  26. package/src/core/data/Store.js +364 -34
  27. package/src/core/data/observable-helpers/object.js +2 -166
  28. package/src/core/utils/formatters.js +91 -0
  29. package/src/core/utils/localstorage.js +57 -0
  30. package/src/core/utils/validator.js +0 -2
  31. package/src/devtools.js +9 -0
  32. package/src/fetch/NativeFetch.js +5 -2
  33. package/types/observable.d.ts +71 -15
  34. package/types/plugins-manager.d.ts +1 -1
  35. package/types/store.d.ts +33 -6
  36. package/hrm.js +0 -7
  37. package/src/devtools/app/App.js +0 -66
  38. package/src/devtools/app/app.css +0 -0
  39. package/src/devtools/hrm/transformComponent.js +0 -129
  40. package/src/devtools/index.js +0 -18
  41. package/src/devtools/widget/DevToolsWidget.js +0 -26
  42. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.hook.template.js +0 -0
  43. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +0 -0
@@ -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
- console.log("Counter is now:", newValue);
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
- console.log(`Loading state: ${isActive}`);
60
+ console.log(`Loading state: ${isActive}`);
66
61
  });
67
62
 
68
63
  status.on("success", (isActive) => {
69
- console.log(`Success state: ${isActive}`);
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
- // Scenario: 1000 components, each watching different states
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
- status.subscribe(value => {
108
- if (value === `state-${i}`) updateComponent(i); // Only 1 cares, all 1000 run
109
- });
99
+ status.subscribe(value => {
100
+ if (value === `state-${i}`) updateComponent(i);
101
+ });
110
102
  }
111
103
 
112
- // ✅ .on() - Only relevant callback runs
104
+ // ✅ .on() Only relevant callback runs
113
105
  for (let i = 0; i < 1000; i++) {
114
- status.on(`state-${i}`, (isActive) => {
115
- if (isActive) updateComponent(i); // Only this one runs
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
- class: {
138
- "spinner": status.when("loading"),
139
- "success": status.when("success"),
140
- "error": status.when("error")
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
- ### Implementation Note
142
+ ## Method Comparison
157
143
 
158
- The `.when()` method simply returns a plain object that other parts of the framework (like class binding) can recognize and handle efficiently using the `.on()` method internally.
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
- ## Method Comparison
152
+ ## Observable Checkers
161
153
 
162
- | Method | Memory Impact | Use Case | Return Value |
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
- ## Observable Objects vs Simple Objects
159
+ // check canonical name
160
+ const isAdult = age.check(value => value >= 18);
161
+ console.log(isAdult.val()); // false
171
162
 
172
- **Important distinction:**
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
- name: "Alice",
178
- age: 25
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); // { name: "Bob", age: 25 }
187
- console.log(Observable.value(userProxy)); // { name: "Bob", age: 25 }
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
- name: "Alice",
192
- age: 25
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 }); // Replace entire object
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
- name: "Alice",
211
- age: 25,
212
- email: "alice@example.com"
316
+ name: "Alice",
317
+ age: 25,
318
+ email: "alice@example.com"
213
319
  });
214
320
 
215
- // Access individual properties (each is an observable)
216
- console.log(user.name.val()); // "Alice"
217
- console.log(user.name.$value); // "Alice" (proxy syntax)
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; // Using proxy syntax
327
+ user.age.$value = 30;
222
328
 
223
329
  // Get the complete object value
224
- console.log(user.$value); // { name: "Bob", age: 30, email: "alice@example.com" }
225
- console.log(Observable.value(user)); // Same as above
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
- console.log("New name:", newName);
335
+ console.log("New name:", newName);
230
336
  });
231
337
 
232
- // Update multiple properties
233
- Observable.update(user, {
234
- name: "Charlie",
235
- age: 35
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
- "Buy groceries",
242
- "Call doctor"
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 = Observable("Doe");
377
+ const lastName = Observable("Doe");
262
378
 
263
- // Updates automatically
264
379
  const fullName = Observable.computed(() => {
265
- return `${firstName.val()} ${lastName.val()}`;
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), // Automatic display
399
+ Span({ class: "count" }, count),
286
400
  Button("+").nd.onClick(increment)
287
401
  ]);
288
402
  ```
289
403
 
290
- # Batching Operations
404
+ ## Batching Operations
291
405
 
292
- Batching is a performance optimization technique that delays notifications to **dependent observers** (like computed observables) until the end of a batch operation. Individual observable subscribers still receive their notifications immediately, but computed observables that depend on the batch function are only triggered once at the end.
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 = Observable(25);
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 => console.log("Age changed to:", value));
415
+ age.subscribe(value => console.log("Age changed to:", value));
303
416
 
304
417
  const updateProfile = Observable.batch(() => {
305
- name.set("Alice"); // Logs: "Name changed to: Alice"
306
- age.set(30); // Logs: "Age changed to: 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 = Observable("Doe");
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 => console.log("Last name:", 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" ← immediate notification
341
- // "Last name: Smith" ← immediate notification
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: Computed depends on individual observables
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]); // ← Depends on individual observables
461
+ }, [score, lives]);
355
462
 
356
- // Method 2: Computed depends on batch function
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); // ← Depends on the batch function
471
+ }, updateGame);
365
472
 
366
- // Without batching - gameStatus1 recalculates twice:
367
- score.set(100); // gameStatus1 recalculates
368
- lives.set(2); // gameStatus1 recalculates again
473
+ score.set(100); // gameStatus1 recalculates
474
+ lives.set(2); // gameStatus1 recalculates again
369
475
 
370
- // With batching - gameStatus2 recalculates only once:
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 = Observable.array([]);
378
- const discount = Observable(0);
482
+ const items = Observable.array([]);
483
+ const discount = Observable(0);
379
484
  const shippingCost = Observable(0);
380
485
 
381
- // Individual subscribers for immediate UI updates
382
- items.subscribe(items => {
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); // Clear current items
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 = items.val().reduce((sum, item) => sum + (item.price * item.quantity), 0);
401
- const discountAmount = itemsTotal * (discount.val() / 100);
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); // ← Only recalculates when updateCart() is called
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 = Observable(null);
424
- const error = Observable(null);
516
+ const userData = Observable(null);
517
+ const error = Observable(null);
425
518
 
426
- // These subscribe immediately to loading states
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); // Immediate notification
433
- error.set(null); // Immediate notification
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 = await response.json();
438
- userData.set(data); // Immediate notification
527
+ const data = await response.json();
528
+ userData.set(data);
439
529
  } catch (err) {
440
- error.set(err.message); // Immediate notification
530
+ error.set(err.message);
441
531
  } finally {
442
- isLoading.set(false); // Immediate notification
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()) return `Error: ${error.val()}`;
451
- if (userData.val()) return `Hello ${userData.val().name}`;
538
+ if (error.val()) return `Error: ${error.val()}`;
539
+ if (userData.val()) return `Hello ${userData.val().name}`;
452
540
  return "No user";
453
- }, fetchUser); // ← Only updates when fetchUser() completes
541
+ }, fetchUser);
454
542
 
455
543
  await fetchUser(123);
456
544
  ```
457
545
 
458
546
  ### Single Batch Dependency Only
459
547
 
460
- **Important**: Computed observables can only depend on **one batch function**, not multiple:
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
- // ✅ Correct: Single batch dependency
474
- const profileSummary = Observable.computed(() => {
475
- return {
476
- user: user.$value,
477
- settings: settings.$value,
478
- lastUpdated: Date.now()
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
- // ❌ This is NOT supported:
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
- 1. **Use batch dependencies for expensive computations**: When you have costly computed observables that shouldn't recalculate on every individual change
526
-
527
- 2. **Keep individual subscribers for immediate feedback**: UI feedback like input validation should use direct subscriptions
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
- - **Real-time updates**: When computed observables need to update immediately
564
- - **Simple computations**: When the computational cost is minimal
565
- - **Debugging**: Batching can make the flow harder to debug
566
- - **Single observable changes**: No benefit when only one observable changes
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 = Observable(25);
588
+ const age = Observable(25);
577
589
 
578
590
  const template = "Hello ${name}, you are ${age} years old";
579
- const message = template.use({ name, age });
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 = Observable("Marie");
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
- // Clean up manually if needed
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 (useful for forcing updates)
629
- data.trigger(); // Notifies all subscribers without changing the value
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
- user: "John",
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?)` - Remove Watchers
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", loadingHandler);
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)` - Single-Time Listener
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()` - Boolean 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()` - Reset to Initial Value
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" (initial value)
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)` - Value Comparison
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 (same value)
718
- console.log(num1.equals(5)); // true (compare with raw value)
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()` - Boolean Conversion
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)` - Value Interception
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) return 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); // Actually sets 0
750
- age.set(150); // Actually sets 120
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