@alwatr/action 9.12.0 → 9.14.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/dist/method.d.ts CHANGED
@@ -1,74 +1,99 @@
1
+ import type { Awaitable } from '@alwatr/type-helper';
1
2
  import type { SubscribeResult } from '@alwatr/signal';
2
3
  import { type ModifierHandler, type PayloadResolver } from './registry.js';
4
+ import type { ActionRecord } from './action-record.js';
3
5
  export type { ModifierHandler, PayloadResolver };
4
6
  /**
5
7
  * Subscribes to a named action dispatched anywhere in the application.
6
8
  *
7
- * The handler is invoked every time `dispatchAction(actionId, payload)` is
8
- * called whether from an `on-action` directive or from code and the
9
- * `actionId` matches. Multiple subscribers for the same `actionId` are all
10
- * notified in subscription order.
9
+ * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter
10
+ * is automatically typed to the corresponding `ActionRecord` valueno manual
11
+ * generic annotation needed:
11
12
  *
12
- * The generic parameter `T` narrows the type of the received payload.
13
- * Defaults to `string`, which covers the common case of attribute-driven
14
- * literal payloads.
13
+ * ```ts
14
+ * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
15
+ * onAction('add-to-cart', (item) => {
16
+ * cartService.add(item.productId, item.qty); // fully typed, no `!` needed
17
+ * });
18
+ * ```
15
19
  *
16
- * @param actionId - The action identifier to listen for (e.g. `'open-drawer'`).
17
- * @param handler - Callback invoked with the resolved payload each time the
18
- * action is dispatched. `payload` is `undefined` when the
19
- * action was dispatched without a value.
20
- * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
20
+ * Passing an action name not declared in `ActionRecord` is a **compile error**.
21
+ * Register new actions by extending `ActionRecord` via declaration merging:
21
22
  *
22
- * @example — basic string payload
23
23
  * ```ts
24
- * import {onAction} from '@alwatr/action';
24
+ * // src/action-record.ts
25
+ * declare module '@alwatr/action' {
26
+ * interface ActionRecord {
27
+ * 'open-drawer': string;
28
+ * }
29
+ * }
30
+ * ```
25
31
  *
26
- * const sub = onAction('open-drawer', (panel) => {
27
- * openDrawer(panel); // panel === 'settings'
28
- * });
32
+ * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching
33
+ * action `'A'` never invokes handlers registered for action `'B'`.
29
34
  *
30
- * // Stop listening when the component is destroyed
31
- * sub.unsubscribe();
32
- * ```
35
+ * @param actionId - A key of `ActionRecord`.
36
+ * @param handler - Callback invoked with the typed payload on each dispatch.
37
+ * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
33
38
  *
34
- * @example — typed object payload
39
+ * @example
35
40
  * ```ts
36
41
  * import {onAction} from '@alwatr/action';
37
42
  *
38
- * onAction<{productId: number; qty: number}>('add-to-cart', (item) => {
39
- * cartService.add(item!.productId, item!.qty);
43
+ * const sub = onAction('page-ready', (pageId) => {
44
+ * router.setPage(pageId); // pageId: string — inferred from ActionRecord
40
45
  * });
46
+ *
47
+ * sub.unsubscribe(); // stop listening when no longer needed
41
48
  * ```
42
49
  */
43
- export declare function onAction<T = string>(actionId: string, handler: (payload?: T) => void): SubscribeResult;
50
+ export declare function onAction<K extends keyof ActionRecord>(actionId: K, handler: (payload: ActionRecord[K]) => Awaitable<void>): SubscribeResult;
44
51
  /**
45
52
  * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.
46
53
  *
47
- * This is the programmatic counterpart to the `on-action` HTML attribute.
48
- * Use it when you need to trigger an action from code rather than from a DOM
49
- * event (e.g. after an async operation completes, or from a service layer).
54
+ * `actionId` must be a key of `ActionRecord`. The `payload` parameter is
55
+ * automatically typed passing the wrong type is a **compile error**:
50
56
  *
51
- * The generic parameter `T` types the payload. Omit it to default to `string`.
57
+ * ```ts
58
+ * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
59
+ * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅
60
+ * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error
61
+ * dispatchAction('unknown-action', 'x'); // ❌ compile error
62
+ * ```
52
63
  *
53
- * @param actionId - The action identifier (e.g. `'navigate'`).
54
- * @param actionPayload - Optional value passed to every matching subscriber.
64
+ * Register new actions by extending `ActionRecord` via declaration merging:
55
65
  *
56
- * @example — dispatch without payload
57
66
  * ```ts
58
- * import {dispatchAction} from '@alwatr/action';
59
- *
60
- * dispatchAction('logout');
67
+ * // src/action-record.ts
68
+ * declare module '@alwatr/action' {
69
+ * interface ActionRecord {
70
+ * 'navigate': string;
71
+ * 'logout': void;
72
+ * }
73
+ * }
61
74
  * ```
62
75
  *
63
- * @example dispatch with a typed payload
76
+ * Use `dispatchAction` when triggering an action from code — e.g. after an
77
+ * async operation, from a service layer, or in tests. For DOM-driven actions,
78
+ * use the `on-action` HTML attribute with `setupActionDelegation`.
79
+ *
80
+ * @param actionId - A key of `ActionRecord`.
81
+ * @param actionPayload - The payload; type is enforced by `ActionRecord`.
82
+ *
83
+ * @example — with payload
64
84
  * ```ts
65
85
  * import {dispatchAction} from '@alwatr/action';
66
86
  *
87
+ * dispatchAction('page-ready', 'home');
67
88
  * dispatchAction('navigate', '/dashboard');
68
- * dispatchAction<{code: number}>('show-error', {code: 404});
89
+ * ```
90
+ *
91
+ * @example — void payload (no second argument)
92
+ * ```ts
93
+ * dispatchAction('logout');
69
94
  * ```
70
95
  */
71
- export declare function dispatchAction<T = string>(actionId: string, actionPayload?: T): void;
96
+ export declare function dispatchAction<K extends keyof ActionRecord>(...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]): void;
72
97
  /**
73
98
  * Registers a custom modifier that can be used in `on-action` attribute syntax.
74
99
  *
@@ -76,22 +101,20 @@ export declare function dispatchAction<T = string>(actionId: string, actionPaylo
76
101
  * (e.g. `click.mymod->action-id`). Its handler runs before the payload is
77
102
  * resolved and the action is dispatched. Returning `false` cancels the dispatch.
78
103
  *
79
- * Built-in modifiers (`prevent`, `stop`, `validate`, `once`, `passive`) are
80
- * always available. This function lets you add domain-specific ones.
104
+ * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always
105
+ * available. This function lets you add domain-specific ones.
81
106
  *
82
107
  * Registering the same name twice logs an accident and overwrites the previous
83
108
  * handler — avoid duplicate registrations in production code.
84
109
  *
85
110
  * @param name - The modifier token (lowercase, no dots or arrows).
86
- * @param handler - The `ModifierHandler` function bound to the directive instance.
111
+ * @param handler - A `ModifierHandler` receiving `(event, element)`.
87
112
  *
88
113
  * @example — a `confirm` modifier that shows a browser dialog
89
114
  * ```ts
90
115
  * import {registerModifier} from '@alwatr/action';
91
116
  *
92
- * registerModifier('confirm', function () {
93
- * return window.confirm('Are you sure?');
94
- * });
117
+ * registerModifier('confirm', () => window.confirm('Are you sure?'));
95
118
  * ```
96
119
  * ```html
97
120
  * <button on-action="click.confirm->delete-item:42">Delete</button>
@@ -103,7 +126,7 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
103
126
  *
104
127
  * A payload resolver is a colon-suffixed token in the attribute value
105
128
  * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time
106
- * with the directive instance as `this` and the DOM event as the argument.
129
+ * with an `ActionContext` as `this` and the DOM event as the argument.
107
130
  * The return value becomes the `actionPayload` passed to `onAction` subscribers.
108
131
  *
109
132
  * Built-in resolvers (`$value`, `$formdata`) are always available. This function
@@ -113,29 +136,19 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
113
136
  * resolver — avoid duplicate registrations in production code.
114
137
  *
115
138
  * @param name - The resolver token (should start with `$` by convention).
116
- * @param resolver - The `PayloadResolver` function bound to the directive instance.
139
+ * @param resolver - A `PayloadResolver` receiving `(event, element)`.
117
140
  *
118
141
  * @example — a `$checked` resolver for checkbox state
119
142
  * ```ts
120
143
  * import {registerPayloadResolver} from '@alwatr/action';
121
144
  *
122
- * registerPayloadResolver('$checked', function () {
123
- * return (this.element_ as HTMLInputElement).checked;
145
+ * registerPayloadResolver('$checked', (_event, element) => {
146
+ * return (element as HTMLInputElement).checked;
124
147
  * });
125
148
  * ```
126
149
  * ```html
127
150
  * <input type="checkbox" on-action="change->toggle-feature:$checked" />
128
151
  * ```
129
- *
130
- * @example — a `$dataset-id` resolver for data attributes
131
- * ```ts
132
- * registerPayloadResolver('$dataset-id', function () {
133
- * return (this.element_ as HTMLElement).dataset.id ?? null;
134
- * });
135
- * ```
136
- * ```html
137
- * <li on-action="click->select-item:$dataset-id" data-id="42">Item</li>
138
- * ```
139
152
  */
140
153
  export declare function registerPayloadResolver(name: string, resolver: PayloadResolver): void;
141
154
  //# sourceMappingURL=method.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../src/method.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAoC,KAAK,eAAe,EAAE,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAG5G,YAAY,EAAC,eAAe,EAAE,eAAe,EAAC,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,QAAQ,CAAC,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,GAAG,eAAe,CAQtG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC,GAAG,IAAI,CAGpF;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAM7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAMrF"}
1
+ {"version":3,"file":"method.d.ts","sourceRoot":"","sources":["../src/method.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAEnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAAoC,KAAK,eAAe,EAAE,KAAK,eAAe,EAAC,MAAM,eAAe,CAAC;AAC5G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAGrD,YAAY,EAAC,eAAe,EAAE,eAAe,EAAC,CAAC;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,QAAQ,CAAC,CAAC,SAAS,MAAM,YAAY,EACnD,QAAQ,EAAE,CAAC,EACX,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,GACrD,eAAe,CAGjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,YAAY,EACzD,GAAG,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,SAAS,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,GAChH,IAAI,CAIN;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,CAM7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,IAAI,CAMrF"}
@@ -1,36 +1,41 @@
1
- import type { ActionDirective } from './directive.js';
2
1
  /**
3
- * A modifier handler attached to an `on-action` directive.
2
+ * A modifier handler used in `on-action` attribute syntax.
4
3
  *
5
- * Called with the directive instance as `this` and the triggering DOM `event`.
6
- * Return `true` to allow the action to proceed, or `false` to cancel it.
7
- * Returning `false` is the only way a modifier can veto a dispatch.
4
+ * Receives the triggering DOM `event` and the `element` that owns the
5
+ * `on-action` attribute. Return `true` (or any truthy value) to allow the
6
+ * action to proceed, or `false` to cancel the dispatch.
7
+ *
8
+ * Using explicit parameters instead of `this` binding makes handlers
9
+ * compatible with arrow functions and easier to test in isolation.
8
10
  *
9
11
  * @example
10
12
  * ```ts
11
13
  * // A modifier that only allows the action when the element is not disabled
12
- * const notDisabledHandler: ModifierHandler = function () {
13
- * return !(this.element_ as HTMLButtonElement).disabled;
14
+ * const notDisabledHandler: ModifierHandler = (_event, element) => {
15
+ * return !(element as HTMLButtonElement).disabled;
14
16
  * };
15
17
  * ```
16
18
  */
17
- export type ModifierHandler = (this: ActionDirective, event: Event) => boolean;
19
+ export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
18
20
  /**
19
- * A payload resolver attached to an `on-action` directive.
21
+ * A payload resolver used in `on-action` attribute syntax.
22
+ *
23
+ * Receives the triggering DOM `event` and the `element` that owns the
24
+ * `on-action` attribute. The return value becomes the `actionPayload` passed
25
+ * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
20
26
  *
21
- * Called with the directive instance as `this` and the triggering DOM `event`
22
- * at dispatch time. The return value becomes the `actionPayload` of the
23
- * dispatched action. Use this to compute dynamic payloads from the DOM state.
27
+ * Using explicit parameters instead of `this` binding makes resolvers
28
+ * compatible with arrow functions and easier to test in isolation.
24
29
  *
25
30
  * @example
26
31
  * ```ts
27
32
  * // A resolver that returns the element's dataset id
28
- * const dataIdResolver: PayloadResolver = function () {
29
- * return (this.element_ as HTMLElement).dataset.id ?? null;
33
+ * const dataIdResolver: PayloadResolver = (_event, element) => {
34
+ * return (element as HTMLElement).dataset.id ?? null;
30
35
  * };
31
36
  * ```
32
37
  */
33
- export type PayloadResolver = (this: ActionDirective, event: Event) => unknown;
38
+ export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
34
39
  /**
35
40
  * Registry of all named modifier handlers.
36
41
  *
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAIpD;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;AAE/E;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,KAAK,KAAK,OAAO,CAAC;AAI/E;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,8BAAqC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,8BAAqC,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAE9E;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAI9E;;;;;;;;;GASG;AACH,eAAO,MAAM,gBAAgB,8BAAqC,CAAC;AAEnE;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,8BAAqC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/action",
3
- "version": "9.12.0",
3
+ "version": "9.14.0",
4
4
  "description": "Declarative DOM action-dispatch — bridge HTML attributes to typed signal handlers.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,15 +21,13 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/directive": "9.11.2",
25
- "@alwatr/logger": "9.11.2",
26
- "@alwatr/signal": "9.12.0"
24
+ "@alwatr/logger": "9.14.0",
25
+ "@alwatr/signal": "9.14.0"
27
26
  },
28
27
  "devDependencies": {
29
- "@alwatr/nano-build": "9.10.1",
30
- "@alwatr/standard": "9.11.2",
31
- "@alwatr/type-helper": "9.11.2",
32
- "@happy-dom/global-registrator": "^20.9.0",
28
+ "@alwatr/nano-build": "9.14.0",
29
+ "@alwatr/standard": "9.14.0",
30
+ "@alwatr/type-helper": "9.14.0",
33
31
  "typescript": "^6.0.3"
34
32
  },
35
33
  "scripts": {
@@ -81,5 +79,5 @@
81
79
  "vanilla-js",
82
80
  "web-development"
83
81
  ],
84
- "gitHead": "b4ca873ebd15cd2e25b34273d76febfd75267b62"
82
+ "gitHead": "4e499b23191d4460ea60f34cde8a99b472741f1a"
85
83
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @file action-record.ts
3
+ *
4
+ * Global action type registry via TypeScript declaration merging.
5
+ *
6
+ * ## How it works
7
+ *
8
+ * `ActionRecord` is an open interface — any package in the monorepo (or any
9
+ * consumer application) can extend it with their own action names and payload
10
+ * types using declaration merging, without modifying this file:
11
+ *
12
+ * ```ts
13
+ * // In your package: src/action-record.ts
14
+ * declare module '@alwatr/action' {
15
+ * interface ActionRecord {
16
+ * 'open-drawer': string;
17
+ * 'add-to-cart': {productId: number; qty: number};
18
+ * 'logout': void;
19
+ * }
20
+ * }
21
+ * ```
22
+ *
23
+ * Once declared, `onAction` and `dispatchAction` become fully type-safe for
24
+ * those action names — the compiler enforces the correct payload type at every
25
+ * call site and provides autocomplete for action identifiers.
26
+ *
27
+ * Only actions declared in `ActionRecord` are accepted. Passing an unknown
28
+ * action name is a **compile error** — there is no string fallback.
29
+ */
30
+
31
+ /**
32
+ * Global registry mapping action identifiers to their payload types.
33
+ *
34
+ * Extend this interface via declaration merging to register your application's
35
+ * actions and gain full type safety in `onAction` and `dispatchAction`.
36
+ *
37
+ * This interface is intentionally empty in the base package — all actions are
38
+ * application-specific and should be declared in a dedicated `action-record.ts`
39
+ * file within each feature package.
40
+ *
41
+ * @example — registering actions in a feature package
42
+ * ```ts
43
+ * // pkg/my-feature/src/action-record.ts
44
+ * declare module '@alwatr/action' {
45
+ * interface ActionRecord {
46
+ * 'open-drawer': string;
47
+ * 'add-to-cart': {productId: number; qty: number};
48
+ * 'logout': void;
49
+ * }
50
+ * }
51
+ * ```
52
+ */
53
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
54
+ export interface ActionRecord {}