@alwatr/action 9.16.0 → 9.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/delegate.ts CHANGED
@@ -16,8 +16,12 @@
16
16
  * - When an event fires, the handler walks up the DOM from `event.target`
17
17
  * using `closest()` to find the nearest element with an `on-<eventType>`
18
18
  * attribute (e.g. `on-click`, `on-submit`).
19
- * - Modifiers and payload resolvers run in the same pipeline as before.
20
- * - `dispatchAction` is called with the resolved payload.
19
+ * - The nearest `[action-context]` ancestor is also resolved and attached to
20
+ * the `Action` object as `context` enabling the same action type to be
21
+ * scoped to different UI regions.
22
+ * - Modifiers run with access to the mutable `Action` object so they can
23
+ * enrich `meta` before the action reaches subscribers.
24
+ * - `dispatchAction` is called with the fully assembled `Action` object.
21
25
  *
22
26
  * ## Complexity
23
27
  *
@@ -38,8 +42,9 @@
38
42
  * - `once` is emulated by removing the attribute after first fire.
39
43
  */
40
44
 
41
- import {internalChannel_, logger_} from './lib.js';
42
- import {modifierRegistry, payloadRegistry} from './registry.js';
45
+ import {internalChannel_, logger_} from './lib_.js';
46
+ import {modifierRegistry, payloadRegistry} from './registry_.js';
47
+ import type {Action, ActionRecord} from './type.js';
43
48
 
44
49
  // ─── Syntax Parser ────────────────────────────────────────────────────────────
45
50
 
@@ -70,7 +75,7 @@ import {modifierRegistry, payloadRegistry} from './registry.js';
70
75
  * → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}
71
76
  * ```
72
77
  */
73
- const syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
78
+ const syntaxRegex = /^([a-z0-9_:-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
74
79
 
75
80
  // ─── Parsed Action Descriptor ─────────────────────────────────────────────────
76
81
 
@@ -150,9 +155,12 @@ function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
150
155
  * 1. Walk up from `event.target` to find the nearest element with an
151
156
  * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).
152
157
  * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.
153
- * 3. Run each modifier in order; if any returns `false`, abort.
154
- * 4. Resolve the payload token (literal or $-resolver).
155
- * 5. Call `dispatchAction(actionId, payload)`.
158
+ * 3. Resolve `context` from the nearest `[action-context]` ancestor.
159
+ * 4. Build a mutable `Action` object with `type`, `payload` (raw), and `context`.
160
+ * 5. Run each modifier in order with access to the mutable `Action`; if any
161
+ * returns `false`, abort.
162
+ * 6. Resolve the payload token (literal or $-resolver) and assign to `action.payload`.
163
+ * 7. Call `dispatchAction(action)` with the fully assembled object.
156
164
  *
157
165
  * @internal
158
166
  */
@@ -186,13 +194,28 @@ function handleDelegatedEvent__(event: Event): void {
186
194
 
187
195
  logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});
188
196
 
189
- // Step 1: handle once modifier — remove attribute before running other modifiers
197
+ // Step 1: handle `once` modifier — remove attribute before running other modifiers
190
198
  // so that even if a modifier aborts, the element will not fire again.
191
199
  if (descriptor.modifiers.has('once')) {
192
200
  actionElement.removeAttribute(actionAttrib);
193
201
  }
194
202
 
195
- // Step 2: run modifiers
203
+ // Step 2: resolve `context` from the nearest [action-context] ancestor.
204
+ // Walk up from the action element itself (inclusive) to find the context scope.
205
+ // This allows the action element itself to carry action-context if needed.
206
+ const actionContext = actionElement.closest('[action-context]')?.getAttribute('action-context') ?? undefined;
207
+
208
+ // Step 3: build the mutable Action object.
209
+ // `payload` starts as the raw token string; it will be resolved in step 5.
210
+ // Modifiers in step 4 may mutate `meta` to attach cross-cutting data.
211
+ const action: Action = {
212
+ type: descriptor.actionId as keyof ActionRecord,
213
+ context: actionContext,
214
+ // Payload is temporarily set to the raw token; resolved below after modifiers run.
215
+ payload: descriptor.payload as ActionRecord[keyof ActionRecord],
216
+ };
217
+
218
+ // Step 4: run modifiers — each receives the mutable action so it can enrich meta.
196
219
  for (const modifier of descriptor.modifiers) {
197
220
  if (modifier === 'once') continue; // handled above
198
221
  const handler = modifierRegistry.get(modifier);
@@ -200,18 +223,26 @@ function handleDelegatedEvent__(event: Event): void {
200
223
  logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});
201
224
  return; // unknown modifier — abort to avoid silent misbehavior
202
225
  }
203
- if (handler(event, actionElement) === false) return;
226
+ if (handler(event, actionElement, action) === false) return;
204
227
  }
205
228
 
206
- // Step 3: resolve payload
207
- let payload: unknown = descriptor.payload;
208
- if (payload) {
209
- const resolver = payloadRegistry.get(payload as string);
210
- if (resolver) payload = resolver(event, actionElement);
229
+ // Step 5: resolve payload — replace raw token with the actual value.
230
+ // If the raw token starts with '$', look it up in the payload resolver registry.
231
+ // Otherwise treat it as a literal string payload.
232
+ if (descriptor.payload) {
233
+ const resolver = payloadRegistry.get(descriptor.payload);
234
+ if (resolver) {
235
+ // Cast needed: payload is typed as ActionRecord[K] but we're building generically.
236
+ (action as {payload: unknown}).payload = resolver(event, actionElement);
237
+ }
238
+ // else: keep the literal string already set on action.payload
239
+ } else {
240
+ // No payload token in the attribute — set to undefined.
241
+ (action as {payload: unknown}).payload = undefined;
211
242
  }
212
243
 
213
- // Step 4: dispatch
214
- internalChannel_.dispatch(descriptor.actionId, payload);
244
+ // Step 6: dispatch the fully assembled Action object.
245
+ internalChannel_.dispatch(action.type, action);
215
246
  }
216
247
 
217
248
  // ─── Setup ────────────────────────────────────────────────────────────────────
@@ -243,8 +274,9 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
243
274
  * Registers global event delegation for `on-<eventType>` attributes.
244
275
  *
245
276
  * Attaches a single `capture`-phase listener on `document.body` for each
246
- * event type in `eventTypes`. All processing — modifier execution, payload
247
- * resolution, and `dispatchAction` — happens inside that one handler.
277
+ * event type in `eventTypes`. All processing — context resolution, modifier
278
+ * execution, payload resolution, and `dispatchAction` — happens inside that
279
+ * one handler.
248
280
  *
249
281
  * **Call this once at application bootstrap**, before any user interaction.
250
282
  * Subsequent calls with the same event types are no-ops (idempotent).
@@ -261,6 +293,24 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
261
293
  * after this call — via `innerHTML`, `lit-html`, a framework renderer, or
262
294
  * server-sent HTML — is automatically covered. No re-bootstrap is needed.
263
295
  *
296
+ * ### Context scoping
297
+ *
298
+ * Wrap a group of elements in a `[action-context]` container to scope their
299
+ * actions. The delegation handler automatically resolves the nearest ancestor
300
+ * and attaches its value to `action.context`:
301
+ *
302
+ * ```html
303
+ * <section action-context="product-list">
304
+ * <button on-click="add_to_cart:42">Add</button>
305
+ * </section>
306
+ * ```
307
+ *
308
+ * ```ts
309
+ * onAction('add_to_cart', (action) => {
310
+ * console.log(action.context); // 'product-list'
311
+ * });
312
+ * ```
313
+ *
264
314
  * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.
265
315
  *
266
316
  * @example — minimal bootstrap
@@ -270,7 +320,7 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
270
320
  * // One call activates the entire page.
271
321
  * setupActionDelegation();
272
322
  *
273
- * onAction('open_drawer', (panel) => openDrawer(panel));
323
+ * onAction('open_drawer', (action) => openDrawer(action.payload));
274
324
  * ```
275
325
  *
276
326
  * @example — with extra event types
@@ -1,5 +1,6 @@
1
1
  import {createLogger} from '@alwatr/logger';
2
2
  import {createChannelSignal} from '@alwatr/signal';
3
+ import type {Action} from './type.js';
3
4
 
4
5
  /**
5
6
  * Module-scoped logger for `@alwatr/action`.
@@ -10,16 +11,17 @@ import {createChannelSignal} from '@alwatr/signal';
10
11
  export const logger_ = createLogger('alwatr-action');
11
12
 
12
13
  /**
13
- * The action channel — a `ChannelSignal` strictly typed by `ActionRecord`.
14
+ * The internal action channel — a `ChannelSignal` keyed by action `type`.
14
15
  *
15
- * Only action names declared in `ActionRecord` (via declaration merging) are
16
- * accepted at compile time. Passing an unknown action name to `onAction` or
17
- * `dispatchAction` is a **compile error** there is no string fallback.
16
+ * Each message on this channel is a full `Action` object (AFSA), not just a
17
+ * raw payload. Subscribers registered via `onAction('foo', handler)` receive
18
+ * the entire `Action<'foo'>` so they have access to `context`, `meta`, and
19
+ * `payload` in one place.
18
20
  *
19
21
  * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a
20
22
  * single `Map.get('A')` lookup and invokes only the handlers registered for
21
- * that specific action — never handlers for `'B'`, `'C'`, etc.
23
+ * that specific type — never handlers for `'B'`, `'C'`, etc.
22
24
  *
23
25
  * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.
24
26
  */
25
- export const internalChannel_ = createChannelSignal<Record<string, unknown>>({name: 'alwatr-action'});
27
+ export const internalChannel_ = createChannelSignal<Record<string, Action>>({name: 'alwatr-action'});
package/src/main.ts CHANGED
@@ -1,6 +1,12 @@
1
1
  /**
2
2
  * @alwatr/action — Declarative DOM action-dispatch for Unidirectional Data Flow.
3
3
  *
4
+ * Implements the **Alwatr Flux Standard Action (AFSA)** pattern: every action
5
+ * flowing through the bus is a single, typed `Action<K>` object carrying
6
+ * `type`, `payload`, `context`, and optional `meta`. This replaces the previous
7
+ * two-argument `(id, payload)` API with a unified structure that is extensible
8
+ * without breaking existing call sites.
9
+ *
4
10
  * ## Activating `on-<eventType>` attributes
5
11
  *
6
12
  * Call `setupActionDelegation()` once at bootstrap. A single capture-phase
@@ -11,7 +17,7 @@
11
17
  * import {setupActionDelegation, onAction} from '@alwatr/action';
12
18
  *
13
19
  * setupActionDelegation();
14
- * onAction('open_drawer', (panel) => openDrawer(panel));
20
+ * onAction('open_drawer', (action) => openDrawer(action.payload));
15
21
  * ```
16
22
  *
17
23
  * ## Attribute syntax
@@ -26,12 +32,31 @@
26
32
  * <form on-submit="submit_form:$formdata; prevent,validate" novalidate>…</form>
27
33
  * ```
28
34
  *
35
+ * ## Context scoping
36
+ *
37
+ * Wrap elements in an `[action-context]` container to scope their actions.
38
+ * The delegation handler resolves the nearest ancestor and attaches its value
39
+ * to `action.context`:
40
+ *
41
+ * ```html
42
+ * <section action-context="product-list">
43
+ * <button on-click="add_to_cart:42">Add</button>
44
+ * </section>
45
+ * ```
46
+ *
47
+ * ```ts
48
+ * onAction('add_to_cart', (action) => {
49
+ * console.log(action.context); // 'product-list'
50
+ * console.log(action.payload); // '42'
51
+ * });
52
+ * ```
53
+ *
29
54
  * ## Programmatic dispatch
30
55
  *
31
56
  * ```ts
32
57
  * import {dispatchAction} from '@alwatr/action';
33
58
  *
34
- * dispatchAction('navigate', '/dashboard');
59
+ * dispatchAction({type: 'navigate', payload: '/dashboard'});
35
60
  * ```
36
61
  *
37
62
  * ## Registering typed actions
@@ -51,7 +76,8 @@
51
76
  *
52
77
  * ## Public API
53
78
  *
54
- * - `ActionRecord` extend this interface to register typed actions
79
+ * - `Action` the AFSA object interface (`type`, `payload`, `context`, `meta`)
80
+ * - `ActionRecord` — extend this interface to register typed actions
55
81
  * - `setupActionDelegation` / `teardownActionDelegation` — global delegation lifecycle
56
82
  * - `DEFAULT_DELEGATED_EVENTS` — default event types covered by delegation
57
83
  * - `onAction` / `dispatchAction` — subscribe to and dispatch named actions
@@ -61,6 +87,6 @@
61
87
  *
62
88
  * For page-ready signals in SSG/SSR apps, use `@alwatr/page-ready` instead.
63
89
  */
64
- export type {ActionRecord} from './action-record.js';
65
- export * from './method.js';
66
90
  export * from './delegate.js';
91
+ export * from './method.js';
92
+ export type * from './type.js';
package/src/method.ts CHANGED
@@ -1,25 +1,25 @@
1
1
  import type {Awaitable} from '@alwatr/type-helper';
2
- import {internalChannel_, logger_} from './lib.js';
3
2
  import type {SubscribeResult} from '@alwatr/signal';
4
- import {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';
5
- import type {ActionRecord} from './action-record.js';
6
3
 
7
- // Re-export extension types so consumers can import them from the package root.
8
- export type {ModifierHandler, PayloadResolver};
4
+ import {internalChannel_, logger_} from './lib_.js';
5
+ import {modifierRegistry, payloadRegistry} from './registry_.js';
6
+ import type {Action, ActionRecord, DispatchParam, ModifierHandler, PayloadResolver} from './type.js';
9
7
 
10
8
  // ─── Core Action API ──────────────────────────────────────────────────────────
11
9
 
12
10
  /**
13
11
  * Subscribes to a named action dispatched anywhere in the application.
14
12
  *
15
- * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter
16
- * is automatically typed to the corresponding `ActionRecord` value — no manual
17
- * generic annotation needed:
13
+ * `type` must be a key of `ActionRecord`. The handler receives the full
14
+ * `Action<K>` object giving access to `payload`, `context`, and `meta`
15
+ * in one place. No manual generic annotation is needed; the compiler infers
16
+ * the correct `payload` type from `ActionRecord`:
18
17
  *
19
18
  * ```ts
20
19
  * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
21
- * onAction('add_to_cart', (item) => {
22
- * cartService.add(item.productId, item.qty); // fully typed, no `!` needed
20
+ * onAction('add_to_cart', (action) => {
21
+ * cartService.add(action.payload.productId, action.payload.qty); // fully typed
22
+ * console.log(action.context); // e.g. 'product-list' (from DOM) or undefined
23
23
  * });
24
24
  * ```
25
25
  *
@@ -38,80 +38,83 @@ export type {ModifierHandler, PayloadResolver};
38
38
  * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching
39
39
  * action `'A'` never invokes handlers registered for action `'B'`.
40
40
  *
41
- * @param actionId - A key of `ActionRecord`.
42
- * @param handler - Callback invoked with the typed payload on each dispatch.
41
+ * @param type - A key of `ActionRecord`.
42
+ * @param handler - Callback invoked with the full `Action<K>` on each dispatch.
43
43
  * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
44
44
  *
45
45
  * @example
46
46
  * ```ts
47
47
  * import {onAction} from '@alwatr/action';
48
48
  *
49
- * const sub = onAction('page_ready', (pageId) => {
50
- * router.setPage(pageId); // pageId: string — inferred from ActionRecord
49
+ * const sub = onAction('page_ready', (action) => {
50
+ * router.setPage(action.payload); // payload: string — inferred from ActionRecord
51
51
  * });
52
52
  *
53
53
  * sub.unsubscribe(); // stop listening when no longer needed
54
54
  * ```
55
55
  */
56
56
  export function onAction<K extends keyof ActionRecord>(
57
- actionId: K,
58
- handler: (payload: ActionRecord[K]) => Awaitable<void>,
57
+ type: K,
58
+ handler: (action: Action<K>) => Awaitable<void>,
59
59
  ): SubscribeResult {
60
- logger_.logMethodArgs?.('onAction', {actionId});
61
- return internalChannel_.on(actionId, handler as (payload: unknown) => Awaitable<void>);
60
+ logger_.logMethodArgs?.('onAction', {type});
61
+ // The internal channel stores Action<any>; we cast to Action<K> here because
62
+ // the channel key guarantees the type matches — only Action<K> objects are
63
+ // ever dispatched under key K.
64
+ return internalChannel_.on(type, handler as (action: Action) => Awaitable<void>);
62
65
  }
63
66
 
64
67
  /**
65
- * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.
68
+ * Dispatches an action to all `onAction` subscribers with a matching `type`.
66
69
  *
67
- * `actionId` must be a key of `ActionRecord`. The `payload` parameter is
68
- * automatically typed — passing the wrong type is a **compile error**:
70
+ * Accepts a full `Action<K>` object. The `payload` field is automatically
71
+ * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:
69
72
  *
70
73
  * ```ts
71
74
  * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
72
- * dispatchAction('add_to_cart', {productId: 42, qty: 1}); // ✅
73
- * dispatchAction('add_to_cart', 'wrong'); // ❌ compile error
74
- * dispatchAction('unknown_action', 'x'); // ❌ compile error
75
+ * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
76
+ * dispatchAction({type: 'add_to_cart', payload: 'wrong'}); // ❌ compile error
77
+ * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error
75
78
  * ```
76
79
  *
77
- * Register new actions by extending `ActionRecord` via declaration merging:
78
- *
79
- * ```ts
80
- * // src/action-record.ts
81
- * declare module '@alwatr/action' {
82
- * interface ActionRecord {
83
- * 'navigate': string;
84
- * 'logout': void;
85
- * }
86
- * }
87
- * ```
80
+ * The `context` and `meta` fields are optional. When dispatching from code
81
+ * (not from the DOM), omit `context` — it is only meaningful for DOM-originated
82
+ * actions where an `[action-context]` ancestor exists.
88
83
  *
89
84
  * Use `dispatchAction` when triggering an action from code — e.g. after an
90
85
  * async operation, from a service layer, or in tests. For DOM-driven actions,
91
86
  * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.
92
87
  *
93
- * @param actionId - A key of `ActionRecord`.
94
- * @param actionPayload - The payload; type is enforced by `ActionRecord`.
88
+ * @param action - A full `Action<K>` object with at minimum `type` and `payload`.
95
89
  *
96
90
  * @example — with payload
97
91
  * ```ts
98
92
  * import {dispatchAction} from '@alwatr/action';
99
93
  *
100
- * dispatchAction('page_ready', 'home');
101
- * dispatchAction('navigate', '/dashboard');
94
+ * dispatchAction({type: 'navigate', payload: '/dashboard'});
95
+ * dispatchAction({type: 'add_to_cart', payload: {productId: 42, qty: 1}});
102
96
  * ```
103
97
  *
104
- * @example — void payload (no second argument)
98
+ * @example — void payload (payload field is optional and can be omitted entirely)
105
99
  * ```ts
106
- * dispatchAction('logout');
100
+ * dispatchAction({type: 'logout'});
101
+ * // or explicitly:
102
+ * dispatchAction({type: 'logout', payload: undefined});
103
+ * ```
104
+ *
105
+ * @example — with context and meta
106
+ * ```ts
107
+ * dispatchAction({
108
+ * type: 'slider_change',
109
+ * payload: 75,
110
+ * context: 'volume_slider',
111
+ * meta: {traceId: 'abc-123'},
112
+ * });
107
113
  * ```
108
114
  */
109
- export function dispatchAction<K extends keyof ActionRecord>(
110
- ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]
111
- ): void {
112
- const [actionId, actionPayload] = args;
113
- logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});
114
- internalChannel_.dispatch(actionId, actionPayload);
115
+ export function dispatchAction<K extends keyof ActionRecord>(action: DispatchParam<K>): void {
116
+ logger_.logMethodArgs?.('dispatchAction', action);
117
+ internalChannel_.dispatch(action.type, action as Action<K>);
115
118
  }
116
119
 
117
120
  // ─── Extension API ────────────────────────────────────────────────────────────
@@ -123,14 +126,17 @@ export function dispatchAction<K extends keyof ActionRecord>(
123
126
  * (e.g. `on-click="action-id; mymod"`). Its handler runs before the payload is
124
127
  * resolved and the action is dispatched. Returning `false` cancels the dispatch.
125
128
  *
126
- * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always
127
- * available. This function lets you add domain-specific ones.
129
+ * The handler also receives the **mutable** `action` object being built, so it
130
+ * can attach data to `action.meta` before the action reaches subscribers.
131
+ *
132
+ * Built-in modifiers (`prevent`, `validate`, `once`) are always available.
133
+ * This function lets you add domain-specific ones.
128
134
  *
129
135
  * Registering the same name twice logs an accident and overwrites the previous
130
136
  * handler — avoid duplicate registrations in production code.
131
137
  *
132
138
  * @param name - The modifier token (lowercase, no special characters).
133
- * @param handler - A `ModifierHandler` receiving `(event, element)`.
139
+ * @param handler - A `ModifierHandler` receiving `(event, element, action)`.
134
140
  *
135
141
  * @example — a `confirm` modifier that shows a browser dialog
136
142
  * ```ts
@@ -141,6 +147,15 @@ export function dispatchAction<K extends keyof ActionRecord>(
141
147
  * ```html
142
148
  * <button on-click="delete_item:42; confirm">Delete</button>
143
149
  * ```
150
+ *
151
+ * @example — a `trace` modifier that stamps a trace ID into meta
152
+ * ```ts
153
+ * registerModifier('trace', (_event, _element, action) => {
154
+ * action.meta ??= {};
155
+ * action.meta['traceId'] = crypto.randomUUID();
156
+ * return true;
157
+ * });
158
+ * ```
144
159
  */
145
160
  export function registerModifier(name: string, handler: ModifierHandler): void {
146
161
  logger_.logMethodArgs?.('registerModifier', {name});
@@ -155,11 +170,11 @@ export function registerModifier(name: string, handler: ModifierHandler): void {
155
170
  *
156
171
  * A payload resolver is a colon-prefixed token in the attribute value
157
172
  * (e.g. `on-click="action-id:$mytoken"`). Its function is called at dispatch time
158
- * with an `ActionContext` as `this` and the DOM event as the argument.
159
- * The return value becomes the `actionPayload` passed to `onAction` subscribers.
173
+ * with the DOM event and the element. The return value becomes the `payload`
174
+ * field of the `Action` object passed to `onAction` subscribers.
160
175
  *
161
- * Built-in resolvers (`$value`, `$formdata`) are always available. This function
162
- * lets you add domain-specific ones.
176
+ * Built-in resolvers (`$value`, `$formdata`, `$checked`) are always available.
177
+ * This function lets you add domain-specific ones.
163
178
  *
164
179
  * Registering the same name twice logs an accident and overwrites the previous
165
180
  * resolver — avoid duplicate registrations in production code.
@@ -167,16 +182,16 @@ export function registerModifier(name: string, handler: ModifierHandler): void {
167
182
  * @param name - The resolver token (should start with `$` by convention).
168
183
  * @param resolver - A `PayloadResolver` receiving `(event, element)`.
169
184
  *
170
- * @example — a `$checked` resolver for checkbox state
185
+ * @example — a `$data-id` resolver that reads a data attribute
171
186
  * ```ts
172
187
  * import {registerPayloadResolver} from '@alwatr/action';
173
188
  *
174
- * registerPayloadResolver('$checked', (_event, element) => {
175
- * return (element as HTMLInputElement).checked;
189
+ * registerPayloadResolver('$data-id', (_event, element) => {
190
+ * return (element as HTMLElement).dataset.id ?? null;
176
191
  * });
177
192
  * ```
178
193
  * ```html
179
- * <input type="checkbox" on-change="toggle_feature:$checked" />
194
+ * <button on-click="select_item:$data-id" data-id="42">Select</button>
180
195
  * ```
181
196
  */
182
197
  export function registerPayloadResolver(name: string, resolver: PayloadResolver): void {
@@ -1,46 +1,4 @@
1
- // ─── Type Definitions ────────────────────────────────────────────────────────
2
-
3
- /**
4
- * A modifier handler used in `on-action` attribute syntax.
5
- *
6
- * Receives the triggering DOM `event` and the `element` that owns the
7
- * `on-action` attribute. Return `true` (or any truthy value) to allow the
8
- * action to proceed, or `false` to cancel the dispatch.
9
- *
10
- * Using explicit parameters instead of `this` binding makes handlers
11
- * compatible with arrow functions and easier to test in isolation.
12
- *
13
- * @example
14
- * ```ts
15
- * // A modifier that only allows the action when the element is not disabled
16
- * const notDisabledHandler: ModifierHandler = (_event, element) => {
17
- * return !(element as HTMLButtonElement).disabled;
18
- * };
19
- * ```
20
- */
21
- export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
22
-
23
- /**
24
- * A payload resolver used in `on-action` attribute syntax.
25
- *
26
- * Receives the triggering DOM `event` and the `element` that owns the
27
- * `on-action` attribute. The return value becomes the `actionPayload` passed
28
- * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
29
- *
30
- * Using explicit parameters instead of `this` binding makes resolvers
31
- * compatible with arrow functions and easier to test in isolation.
32
- *
33
- * @example
34
- * ```ts
35
- * // A resolver that returns the element's dataset id
36
- * const dataIdResolver: PayloadResolver = (_event, element) => {
37
- * return (element as HTMLElement).dataset.id ?? null;
38
- * };
39
- * ```
40
- */
41
- export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
42
-
43
- // ─── Registries ──────────────────────────────────────────────────────────────
1
+ import type {ModifierHandler, PayloadResolver} from './type.js';
44
2
 
45
3
  /**
46
4
  * Registry of all named modifier handlers.
@@ -89,7 +47,7 @@ modifierRegistry.set('prevent', (event) => {
89
47
  * allowing native constraint-validation UI to surface errors. If no form is
90
48
  * found the dispatch is also cancelled.
91
49
  *
92
- * Pair with `.prevent` on `submit` events to avoid page reloads:
50
+ * Pair with `prevent` on `submit` events to avoid page reloads:
93
51
  *
94
52
  * @example `<form on-submit="submit_form:$formdata; prevent,validate" novalidate>`
95
53
  */
@@ -122,8 +80,8 @@ payloadRegistry.set('$value', (_event, element) => {
122
80
  *
123
81
  * @example `<form on-submit="submit_form:$formdata; prevent,validate">`
124
82
  * ```ts
125
- * onAction('submit_form', (data) => {
126
- * console.log(data); // {username: 'ali', password: '…'}
83
+ * onAction('submit_form', (action) => {
84
+ * console.log(action.payload); // {username: 'ali', password: '…'}
127
85
  * });
128
86
  * ```
129
87
  */
@@ -140,8 +98,9 @@ payloadRegistry.set('$formdata', (_event, element) => {
140
98
  *
141
99
  * @example `<input type="checkbox" on-change="toggle_feature:$checked" />`
142
100
  * ```ts
143
- * onAction('toggle_feature', (isChecked) => {
144
- * featureSignal.set(isChecked as boolean);
101
+ * onAction('toggle_feature', (action) => {
102
+ * console.log(action.payload); // true or false
103
+ * featureSignal.set(action.payload);
145
104
  * });
146
105
  * ```
147
106
  */