@alwatr/flux 9.16.0 → 9.17.0

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/README.md CHANGED
@@ -52,7 +52,7 @@ No magic. No hidden re-renders. No performance cliffs.
52
52
 
53
53
  ### 3. **Absolute Type Safety**
54
54
 
55
- Through TypeScript's **Declaration Merging**, the entire action bus is fully typed:
55
+ Through TypeScript's **Declaration Merging**, the entire action bus is fully typed. Every action is a single **`Action<K>`** object (Alwatr Flux Standard Action — AFSA) carrying `type`, `payload`, `context`, and `meta`:
56
56
 
57
57
  ```typescript
58
58
  // Define your actions once
@@ -64,14 +64,16 @@ declare module '@alwatr/flux' {
64
64
  }
65
65
  }
66
66
 
67
- // Get compile-time safety everywhere
68
- onAction('add_to_cart', (item) => {
69
- // item is typed as {productId: number; qty: number}
70
- cartService.add(item.productId, item.qty);
67
+ // Get compile-time safety everywhere — handler receives the full Action object
68
+ onAction('add_to_cart', (action) => {
69
+ // action.payload is typed as {productId: number; qty: number}
70
+ cartService.add(action.payload.productId, action.payload.qty);
71
+ // action.context is the nearest [action-context] ancestor value (or undefined)
72
+ console.log(action.context); // e.g. 'product-list'
71
73
  });
72
74
 
73
- dispatchAction('add_to_cart', {productId: 42, qty: 1}); // ✅
74
- dispatchAction('add_to_cart', 'wrong'); // ❌ Compile error
75
+ dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
76
+ dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ Compile error
75
77
  ```
76
78
 
77
79
  ---
@@ -120,7 +122,7 @@ lastName.set('Smith'); // Only fullName and the effect re-run — nothing else
120
122
 
121
123
  ### 🧩 **Declarative HTML Syntax**
122
124
 
123
- Connect DOM events to typed actions without writing JavaScript:
125
+ Connect DOM events to typed actions without writing JavaScript. Wrap elements in `[action-context]` to scope the same action type to different UI regions:
124
126
 
125
127
  ```html
126
128
  <!-- Simple action -->
@@ -153,6 +155,28 @@ Connect DOM events to typed actions without writing JavaScript:
153
155
 
154
156
  <!-- Fire once and remove -->
155
157
  <button on-click="track_impression:hero_banner; once">Learn More</button>
158
+
159
+ <!-- Context scoping — same action type, different regions -->
160
+ <section action-context="volume">
161
+ <input
162
+ type="range"
163
+ on-input="slider:change:$value"
164
+ />
165
+ </section>
166
+ <section action-context="brightness">
167
+ <input
168
+ type="range"
169
+ on-input="slider:change:$value"
170
+ />
171
+ </section>
172
+ ```
173
+
174
+ ```typescript
175
+ // Handler receives the full Action object — payload, context, and meta together
176
+ onAction('slider:change', (action) => {
177
+ if (action.context === 'volume') audioService.setVolume(Number(action.payload));
178
+ if (action.context === 'brightness') displayService.setBrightness(Number(action.payload));
179
+ });
156
180
  ```
157
181
 
158
182
  **Built-in modifiers:**
@@ -308,55 +332,59 @@ createEffect({
308
332
  Flux implements a **strict layered architecture** where each layer has a single responsibility:
309
333
 
310
334
  ```
311
- ┌─────────────────────────────────────────────────────────────┐
312
- │ VIEW LAYER
313
- │ (HTML templates, Directives, lit-html rendering)
314
-
315
- │ • Reads state from Signals
316
- │ • Dispatches Actions via on-<event> attributes
317
- │ • Never manipulates state directly
318
- └──────────────────┬──────────────────────────────────────────┘
335
+ ┌───────────────────────────────────────────────────────────┐
336
+ │ VIEW LAYER
337
+ │ (HTML templates, Directives, lit-html rendering)
338
+
339
+ │ • Reads state from Signals
340
+ │ • Dispatches Actions via on-<event> attributes
341
+ │ • Never manipulates state directly
342
+ └──────────────────┬────────────────────────────────────────┘
319
343
  │ on-click="add_to_cart:42"
320
344
 
321
- ┌─────────────────────────────────────────────────────────────┐
322
- │ ACTION LAYER
323
- │ (@alwatr/action — Global Event Delegation)
324
-
325
- │ • Captures DOM events via document.body listener
326
- │ • Parses on-<event> attributes
327
- │ • Runs modifiers (prevent, validate, once)
328
- │ • Resolves payload ($value, $formdata)
329
- │ • Dispatches typed action to ChannelSignal
330
- └──────────────────┬──────────────────────────────────────────┘
331
- dispatchAction('add_to_cart', 42)
345
+ ┌───────────────────────────────────────────────────────────┐
346
+ │ ACTION LAYER
347
+ │ (@alwatr/action — Global Event Delegation + AFSA)
348
+
349
+ │ • Captures DOM events via document.body listener
350
+ │ • Resolves [action-context] ancestor → action.context
351
+ │ • Parses on-<event> attributes
352
+ │ • Runs modifiers (prevent, validate, once)
353
+ │ • Modifiers may enrich action.meta
354
+ │ • Resolves payload ($value, $formdata) │
355
+ Dispatches full Action {type, payload, context, meta} │
356
+ └──────────────────┬────────────────────────────────────────┘
357
+ │ dispatchAction({type: 'add_to_cart', payload: 42, context: 'cart'})
332
358
 
333
- ┌─────────────────────────────────────────────────────────────┐
334
- │ CONTROLLER LAYER
335
- │ (Business Logic, Services, Use Cases)
336
-
337
- │ • Subscribes to Actions via onAction()
338
- │ • Executes business logic
339
- Updates State via Signal.set()
340
- │ • Never touches DOM directly
341
- └──────────────────┬──────────────────────────────────────────┘
359
+ ┌───────────────────────────────────────────────────────────┐
360
+ │ CONTROLLER LAYER
361
+ │ (Business Logic, Services, Use Cases)
362
+
363
+ │ • Subscribes to Actions via onAction()
364
+ │ • Receives full Action object (type, payload, context,
365
+ meta) no need to pass context separately
366
+ │ • Executes business logic
367
+ │ • Updates State via Signal.set() │
368
+ │ • Never touches DOM directly │
369
+ └──────────────────┬────────────────────────────────────────┘
342
370
  │ cartSignal.set(newCart)
343
371
 
344
- ┌─────────────────────────────────────────────────────────────┐
345
- │ STATE LAYER
346
- │ (@alwatr/signal — Reactive State Management)
347
-
348
- │ • StateSignal — mutable state
349
- │ • ComputedSignal — derived state (memoized)
350
- │ • EffectSignal — side effects
351
- │ • PersistentStateSignal — localStorage sync
352
- │ • SessionStateSignal — sessionStorage sync
353
- └──────────────────┬──────────────────────────────────────────┘
372
+ ┌───────────────────────────────────────────────────────────┐
373
+ │ STATE LAYER
374
+ │ (@alwatr/signal — Reactive State Management)
375
+
376
+ │ • StateSignal — mutable state
377
+ │ • ComputedSignal — derived state (memoized)
378
+ │ • EffectSignal — side effects
379
+ │ • PersistentStateSignal — localStorage sync
380
+ │ • SessionStateSignal — sessionStorage sync
381
+ └──────────────────┬────────────────────────────────────────┘
354
382
  │ signal.subscribe(render)
355
383
 
356
- ┌─────────────────────────────────────────────────────────────┐
357
- │ VIEW LAYER
358
- │ (Re-render only affected DOM nodes)
359
- └─────────────────────────────────────────────────────────────┘
384
+ ┌───────────────────────────────────────────────────────────┐
385
+ │ VIEW LAYER
386
+ │ (Re-render only affected DOM nodes)
387
+ └───────────────────────────────────────────────────────────┘
360
388
  ```
361
389
 
362
390
  **Key architectural benefits:**
@@ -369,6 +397,63 @@ Flux implements a **strict layered architecture** where each layer has a single
369
397
 
370
398
  ---
371
399
 
400
+ ## 🎯 The Action Object (AFSA)
401
+
402
+ Every action flowing through the bus — whether triggered from HTML attributes or dispatched programmatically — is a single **`Action<K>`** object (Alwatr Flux Standard Action):
403
+
404
+ ```typescript
405
+ interface Action<K extends keyof ActionRecord> {
406
+ /** Action identifier — must be a key of ActionRecord. */
407
+ type: K;
408
+
409
+ /**
410
+ * DOM context from the nearest [action-context] ancestor.
411
+ * undefined for programmatic dispatches or when no ancestor exists.
412
+ * Example: 'product-list', 'checkout-form', 'volume-slider'
413
+ */
414
+ context?: string;
415
+
416
+ /** Business payload — type is inferred from ActionRecord[K]. */
417
+ payload: ActionRecord[K];
418
+
419
+ /**
420
+ * Open-ended metadata bag for cross-cutting concerns.
421
+ * Modifiers in the delegation pipeline may write to this before
422
+ * the action reaches subscribers.
423
+ */
424
+ meta?: Record<string, unknown>;
425
+ }
426
+ ```
427
+
428
+ This unified structure replaces the previous two-argument `(id, payload)` API. Every handler now receives the full picture:
429
+
430
+ ```typescript
431
+ onAction('add_to_cart', (action) => {
432
+ console.log(action.type); // 'add_to_cart'
433
+ console.log(action.payload); // {productId: 42, qty: 1} — fully typed
434
+ console.log(action.context); // 'product-list' — from [action-context] ancestor
435
+ console.log(action.meta); // {traceId: '…'} — set by modifiers, or undefined
436
+ });
437
+ ```
438
+
439
+ Modifiers can enrich `meta` before the action reaches subscribers:
440
+
441
+ ```typescript
442
+ import {registerModifier} from '@alwatr/flux';
443
+
444
+ registerModifier('trace', (_event, _element, action) => {
445
+ action.meta ??= {};
446
+ action.meta['traceId'] = crypto.randomUUID();
447
+ return true;
448
+ });
449
+ ```
450
+
451
+ ```html
452
+ <button on-click="submit_order:42; trace">Place Order</button>
453
+ ```
454
+
455
+ ---
456
+
372
457
  ## 📦 Installation
373
458
 
374
459
  ```bash
@@ -443,8 +528,9 @@ onAction('decrement', () => {
443
528
  counterSignal.update((count) => count - 1);
444
529
  });
445
530
 
446
- onAction('set_count', (value) => {
447
- counterSignal.set(value);
531
+ // Handler receives the full Action object — payload is typed from ActionRecord
532
+ onAction('set_count', (action) => {
533
+ counterSignal.set(action.payload); // action.payload: number
448
534
  });
449
535
  ```
450
536
 
@@ -612,39 +698,56 @@ setupActionDelegation();
612
698
  setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'focus']);
613
699
  ```
614
700
 
615
- #### `onAction<K>(actionId, handler)`
701
+ #### `onAction<K>(type, handler)`
616
702
 
617
- Subscribes to a typed action.
703
+ Subscribes to a typed action. The handler receives the full `Action<K>` object.
618
704
 
619
705
  ```typescript
620
- const sub = onAction('add_to_cart', (item) => {
621
- cartService.add(item.productId, item.qty);
706
+ const sub = onAction('add_to_cart', (action) => {
707
+ cartService.add(action.payload.productId, action.payload.qty);
708
+ console.log(action.context); // e.g. 'product-list' or undefined
709
+ console.log(action.meta); // any metadata set by modifiers
622
710
  });
623
711
 
624
712
  sub.unsubscribe(); // Clean up when done
625
713
  ```
626
714
 
627
- #### `dispatchAction<K>(actionId, payload?)`
715
+ #### `dispatchAction<K>(action)`
628
716
 
629
- Dispatches a typed action programmatically.
717
+ Dispatches a typed action programmatically. Takes a full `Action<K>` object.
630
718
 
631
719
  ```typescript
632
- dispatchAction('navigate', '/home');
633
- dispatchAction('logout'); // void payload
720
+ dispatchAction({type: 'navigate', payload: '/home'});
721
+ dispatchAction({type: 'logout', payload: undefined}); // void payload
722
+
723
+ // With context and meta
724
+ dispatchAction({
725
+ type: 'add_to_cart',
726
+ payload: {productId: 42, qty: 1},
727
+ context: 'product-list',
728
+ meta: {source: 'recommendation'},
729
+ });
634
730
  ```
635
731
 
636
732
  #### `registerModifier(name, handler)`
637
733
 
638
- Adds a custom modifier for `on-<event>` attributes.
734
+ Adds a custom modifier for `on-<event>` attributes. The handler receives the mutable `action` object and may write to `action.meta`.
639
735
 
640
736
  ```typescript
641
737
  registerModifier('confirm', () => {
642
738
  return window.confirm('Are you sure?');
643
739
  });
740
+
741
+ // A modifier that stamps a trace ID into meta
742
+ registerModifier('trace', (_event, _element, action) => {
743
+ action.meta ??= {};
744
+ action.meta['traceId'] = crypto.randomUUID();
745
+ return true;
746
+ });
644
747
  ```
645
748
 
646
749
  ```html
647
- <button on-click="delete_item:42; confirm">Delete</button>
750
+ <button on-click="delete_item:42; confirm,trace">Delete</button>
648
751
  ```
649
752
 
650
753
  #### `registerPayloadResolver(name, resolver)`
@@ -889,23 +992,23 @@ import {todosSignal} from './state.js';
889
992
 
890
993
  let nextId = 1;
891
994
 
892
- onAction('add_todo', (text) => {
995
+ onAction('add_todo', (action) => {
893
996
  todosSignal.update((todos) => [
894
997
  ...todos,
895
- {id: nextId++, text, done: false},
998
+ {id: nextId++, text: action.payload, done: false},
896
999
  ]);
897
1000
  });
898
1001
 
899
- onAction('toggle_todo', (id) => {
1002
+ onAction('toggle_todo', (action) => {
900
1003
  todosSignal.update((todos) =>
901
1004
  todos.map((todo) =>
902
- todo.id === id ? {...todo, done: !todo.done} : todo
1005
+ todo.id === action.payload ? {...todo, done: !todo.done} : todo
903
1006
  )
904
1007
  );
905
1008
  });
906
1009
 
907
- onAction('remove_todo', (id) => {
908
- todosSignal.update((todos) => todos.filter((t) => t.id !== id));
1010
+ onAction('remove_todo', (action) => {
1011
+ todosSignal.update((todos) => todos.filter((t) => t.id !== action.payload));
909
1012
  });
910
1013
 
911
1014
  // view.html
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/flux v9.16.0 */
1
+ /* 📦 @alwatr/flux v9.17.0 */
2
2
  export*from"@alwatr/signal";export*from"@alwatr/action";export*from"@alwatr/directive";export*from"@alwatr/render-state";export*from"@alwatr/local-storage";export*from"@alwatr/session-storage";export*from"@alwatr/page-ready";
3
3
 
4
- //# debugId=C06C2CEF2EC7ADAE64756E2164756E21
4
+ //# debugId=0386D7BA1F615EBF64756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -5,6 +5,6 @@
5
5
  "// UI and reactive bundle — signals, actions, directives, and client-side storage.\n// This package aggregates all UI-layer nanolibs for convenient single-import usage.\n\nexport * from '@alwatr/signal';\nexport * from '@alwatr/action';\nexport * from '@alwatr/directive';\nexport * from '@alwatr/render-state';\nexport * from '@alwatr/local-storage';\nexport * from '@alwatr/session-storage';\nexport * from '@alwatr/page-ready';\nexport type * from '@alwatr/type-helper';\n"
6
6
  ],
7
7
  "mappings": ";AAGA,4BACA,4BACA,+BACA,kCACA,mCACA,qCACA",
8
- "debugId": "C06C2CEF2EC7ADAE64756E2164756E21",
8
+ "debugId": "0386D7BA1F615EBF64756E2164756E21",
9
9
  "names": []
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/flux",
3
- "version": "9.16.0",
3
+ "version": "9.17.0",
4
4
  "description": "UI and reactive library bundle for ECMAScript (JavaScript/TypeScript) projects — signals, actions, directives, and storage.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/action": "9.16.0",
24
+ "@alwatr/action": "9.17.0",
25
25
  "@alwatr/directive": "9.16.0",
26
26
  "@alwatr/local-storage": "9.16.0",
27
27
  "@alwatr/page-ready": "9.16.0",
@@ -81,5 +81,5 @@
81
81
  "ui",
82
82
  "unidirectional-data-flow"
83
83
  ],
84
- "gitHead": "c210044f6e8ab444ec2f9e600f095761cbd279bd"
84
+ "gitHead": "782563375f55c55d29719cbcfebaca251d69ddcd"
85
85
  }