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.
Files changed (44) 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/{src/devtools/hrm → devtools/transformers/templates}/hrm.orbservable.hook.template.js +8 -0
  9. package/devtools/widget/Widget.js +48 -0
  10. package/devtools/widget/widget.css +81 -0
  11. package/devtools/widget.js +23 -0
  12. package/dist/native-document.components.min.js +1953 -1245
  13. package/dist/native-document.dev.js +2022 -1375
  14. package/dist/native-document.dev.js.map +1 -1
  15. package/dist/native-document.devtools.min.js +1 -1
  16. package/dist/native-document.min.js +1 -1
  17. package/docs/cache.md +1 -1
  18. package/docs/core-concepts.md +1 -1
  19. package/docs/native-document-element.md +51 -15
  20. package/docs/observables.md +333 -315
  21. package/docs/state-management.md +198 -193
  22. package/package.json +1 -1
  23. package/readme.md +1 -1
  24. package/rollup.config.js +1 -1
  25. package/src/core/data/ObservableArray.js +67 -0
  26. package/src/core/data/ObservableChecker.js +2 -0
  27. package/src/core/data/ObservableItem.js +97 -0
  28. package/src/core/data/ObservableObject.js +183 -0
  29. package/src/core/data/Store.js +364 -34
  30. package/src/core/data/observable-helpers/object.js +2 -166
  31. package/src/core/utils/formatters.js +91 -0
  32. package/src/core/utils/localstorage.js +57 -0
  33. package/src/core/utils/validator.js +0 -2
  34. package/src/fetch/NativeFetch.js +5 -2
  35. package/types/observable.d.ts +73 -15
  36. package/types/plugins-manager.d.ts +1 -1
  37. package/types/store.d.ts +33 -6
  38. package/hrm.js +0 -7
  39. package/src/devtools/app/App.js +0 -66
  40. package/src/devtools/app/app.css +0 -0
  41. package/src/devtools/hrm/transformComponent.js +0 -129
  42. package/src/devtools/index.js +0 -18
  43. package/src/devtools/widget/DevToolsWidget.js +0 -26
  44. /package/{src/devtools/hrm → devtools/transformers/templates}/hrm.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:**
@@ -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 Proxies
312
+ ## Working with Object Observable
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,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 = Observable("Marie");
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
- // Clean up manually if needed
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 (useful for forcing updates)
629
- data.trigger(); // Notifies all subscribers without changing the value
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
- user: "John",
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?)` - Remove Watchers
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", loadingHandler);
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)` - Single-Time Listener
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()` - Boolean 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()` - Reset to Initial Value
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" (initial value)
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)` - Value Comparison
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 (same value)
718
- console.log(num1.equals(5)); // true (compare with raw value)
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()` - Boolean Conversion
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)` - Value Interception
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) return 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); // Actually sets 0
750
- age.set(150); // Actually sets 120
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. **Use proxies for convenience**: `obs.$value` instead of `obs.val()`
768
- 4. **Group related data** with `Observable.object()` for individual property reactivity
769
- 5. **Use `Observable.value()`** to extract plain values from complex structures
770
- 6. **Prefer computed** for derived values
771
- 7. **Clean up** unused observables to prevent memory leaks
772
- 8. **Use `trigger()`** when you need to force updates without value changes
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