@alwatr/action 9.25.0 → 9.27.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/src/delegate.ts DELETED
@@ -1,359 +0,0 @@
1
- /**
2
- * @file delegate.ts
3
- *
4
- * Global Event Delegation engine for `@alwatr/action`.
5
- *
6
- * ## Why delegation instead of per-element listeners?
7
- *
8
- * The classic directive approach attaches one `addEventListener` per element.
9
- * With 100 buttons on a page, that means 100 listener registrations at boot
10
- * time — O(N) initialization cost, O(N) memory for listener references, and
11
- * zero support for elements added after bootstrap.
12
- *
13
- * This module implements the global delegation pattern:
14
- * - A single listener per event type is attached to `document.body` with
15
- * `capture: true` (so it fires even for non-bubbling events).
16
- * - When an event fires, the handler walks up the DOM from `event.target`
17
- * using `closest()` to find the nearest element with an `on-<eventType>`
18
- * attribute (e.g. `on-click`, `on-submit`).
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.
25
- *
26
- * ## Complexity
27
- *
28
- * | Metric | Per-element listeners | Global delegation |
29
- * | --------------- | --------------------- | ----------------- |
30
- * | Boot time | O(N elements) | O(1) — 1 loop |
31
- * | Memory | O(N listeners) | O(1) — 1 handler |
32
- * | Dynamic content | Requires re-bootstrap | Works out-of-box |
33
- * | `once` modifier | Native option | Manual tracking |
34
- *
35
- * ## Trade-offs vs. the directive approach
36
- *
37
- * - `passive` is not supported as a per-element option (all delegated listeners
38
- * are non-passive so that `prevent` can call `preventDefault()`).
39
- * - `stop` stops further bubbling but the delegation handler has already
40
- * captured the event at `body` level — it does not prevent other delegation
41
- * handlers from running on the same element.
42
- * - `once` is emulated by removing the attribute after first fire.
43
- */
44
-
45
- import {internalChannel_, logger_} from './lib_.js';
46
- import {modifierRegistry, payloadRegistry} from './registry_.js';
47
- import type {Action, ActionRecord} from './type.js';
48
-
49
- // ─── Syntax Parser ────────────────────────────────────────────────────────────
50
-
51
- /**
52
- * Parses the `on-<eventType>` attribute value into its segments.
53
- *
54
- * Syntax: `actionId[:payload][; modifier1,modifier2,…]`
55
- *
56
- * The event type is encoded in the **attribute name** itself (`on-click`,
57
- * `on-submit`, etc.) rather than inside the value. This makes the HTML more
58
- * readable and aligns with native event attribute conventions.
59
- *
60
- * | Capture group | Matches | Example |
61
- * | ------------- | -------------------------------------- | -------------------------- |
62
- * | 1 | Action identifier | `ui_open_drawer` |
63
- * | 2 | Optional payload token or literal | `main_menu` / `$value` |
64
- * | 3 | Optional comma-separated modifier list | `prevent,validate` |
65
- *
66
- * @example
67
- * ```
68
- * 'ui_close_drawer'
69
- * → actionId='ui_close_drawer', payload=undefined, modifiers={}
70
- *
71
- * 'ui_open_drawer:main_menu'
72
- * → actionId='ui_open_drawer', payload='main_menu', modifiers={}
73
- *
74
- * 'ui_submit_form:$formdata; prevent,validate'
75
- * → actionId='ui_submit_form', payload='$formdata', modifiers={'prevent','validate'}
76
- * ```
77
- */
78
- const syntaxRegex = /^(ui_[a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
79
-
80
- // ─── Parsed Action Descriptor ─────────────────────────────────────────────────
81
-
82
- /**
83
- * Parsed and cached representation of a single `on-<eventType>` attribute value.
84
- *
85
- * Does not store `eventType` — the caller always has it from `event.type`,
86
- * and the attribute name already encodes it (e.g. `on-click`), so storing it
87
- * here would be redundant. This also keeps the cache key simple: just the raw
88
- * attribute value string, with no composite key needed.
89
- */
90
- interface ActionDescriptor {
91
- /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */
92
- readonly modifiers: ReadonlySet<string>;
93
- /** The action identifier dispatched to `onAction` subscribers. */
94
- readonly actionId: string;
95
- /** Raw payload token from the attribute (literal string or $-resolver key). */
96
- readonly payload: string | undefined;
97
- }
98
-
99
- /**
100
- * Cache for parsed `on-<eventType>` attribute values.
101
- *
102
- * Attribute strings are typically repeated across many elements (e.g. every
103
- * "add to cart" button shares the same `on-click` value). Caching the parsed
104
- * descriptor avoids redundant regex work on every event.
105
- *
106
- * The cache key is the raw attribute value string. No composite key with
107
- * event type is needed because the attribute name already encodes the event
108
- * type — `on-click="ui_open_drawer"` and `on-submit="ui_open_drawer"` are two
109
- * separate attributes with the same value string, but they are read from
110
- * different attribute names and never collide in this cache.
111
- *
112
- * @internal
113
- */
114
- const descriptorCache__ = new Map<string, ActionDescriptor | null>();
115
-
116
- /**
117
- * Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.
118
- *
119
- * Returns `null` when the syntax is invalid. Results are cached by the raw
120
- * attribute value string so repeated calls for the same value are O(1).
121
- *
122
- * @internal
123
- */
124
- function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
125
- logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});
126
-
127
- const cached = descriptorCache__.get(attributeValue);
128
- // Explicit `undefined` check: `null` means "already parsed and invalid".
129
- if (cached !== undefined) return cached;
130
-
131
- const match = attributeValue.match(syntaxRegex);
132
- if (!match) {
133
- logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});
134
- descriptorCache__.set(attributeValue, null);
135
- return null;
136
- }
137
-
138
- const actionId = match[1];
139
- const payload: string | undefined = match[2];
140
- // match[3] is the raw modifier list string, e.g. "prevent,validate"
141
- const modifierString = match[3];
142
- const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();
143
- const descriptor: ActionDescriptor = {modifiers, actionId, payload};
144
-
145
- descriptorCache__.set(attributeValue, descriptor);
146
- return descriptor;
147
- }
148
-
149
- // ─── Core Delegation Handler ──────────────────────────────────────────────────
150
-
151
- /**
152
- * Central event handler attached to `document.body` for every delegated event type.
153
- *
154
- * Execution flow for each incoming event:
155
- * 1. Walk up from `event.target` to find the nearest element with an
156
- * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).
157
- * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.
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.
164
- *
165
- * @internal
166
- */
167
- function handleDelegatedEvent__(event: Event): void {
168
- const eventType = event.type;
169
- logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});
170
-
171
- const target = event.target as Element | null;
172
- if (!target) return;
173
-
174
- // Attribute name encodes the event type: on-click, on-submit, etc.
175
- const actionAttrib = `on-${eventType}`;
176
-
177
- // Walk up the DOM to find the closest element with the matching on-<eventType> attribute.
178
- const actionElement = target.closest?.(`[${actionAttrib}]`);
179
- if (!actionElement) return;
180
-
181
- const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();
182
- if (!attributeValue) {
183
- logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});
184
- return;
185
- }
186
-
187
- if (!(actionElement instanceof HTMLElement)) {
188
- logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});
189
- return;
190
- }
191
-
192
- const descriptor = parseDescriptor__(attributeValue);
193
- if (!descriptor) return;
194
-
195
- logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});
196
-
197
- // Step 1: handle `once` modifier — remove attribute before running other modifiers
198
- // so that even if a modifier aborts, the element will not fire again.
199
- if (descriptor.modifiers.has('once')) {
200
- actionElement.removeAttribute(actionAttrib);
201
- }
202
-
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.
219
- for (const modifier of descriptor.modifiers) {
220
- if (modifier === 'once') continue; // handled above
221
- const handler = modifierRegistry.get(modifier);
222
- if (!handler) {
223
- logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});
224
- return; // unknown modifier — abort to avoid silent misbehavior
225
- }
226
- if (handler(event, actionElement, action) === false) return;
227
- }
228
-
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;
242
- }
243
-
244
- // Step 6: dispatch the fully assembled Action object.
245
- internalChannel_.dispatch(action.type, action);
246
- }
247
-
248
- // ─── Setup ────────────────────────────────────────────────────────────────────
249
-
250
- /**
251
- * The set of event types currently delegated to `document.body`.
252
- *
253
- * Tracked so that `setupActionDelegation` is idempotent — calling it multiple
254
- * times with the same event types does not register duplicate listeners.
255
- *
256
- * @internal
257
- */
258
- const delegatedEventTypes__ = new Set<string>();
259
-
260
- /**
261
- * Default DOM event types that cover the vast majority of interactive elements.
262
- *
263
- * - `click` — buttons, links, checkboxes, custom interactive elements
264
- * - `submit` — form submission
265
- * - `input` — live text input, range sliders
266
- * - `change` — select boxes, checkboxes, radio buttons (fires on commit)
267
- *
268
- * Pass additional types to `setupActionDelegation` when your app uses other
269
- * events (e.g. `'keydown'`, `'pointerup'`).
270
- */
271
- export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];
272
-
273
- /**
274
- * Registers global event delegation for `on-<eventType>` attributes.
275
- *
276
- * Attaches a single `capture`-phase listener on `document.body` for each
277
- * event type in `eventTypes`. All processing — context resolution, modifier
278
- * execution, payload resolution, and `dispatchAction` — happens inside that
279
- * one handler.
280
- *
281
- * **Call this once at application bootstrap**, before any user interaction.
282
- * Subsequent calls with the same event types are no-ops (idempotent).
283
- *
284
- * ### Why `capture: true`?
285
- *
286
- * Capture-phase listeners fire before bubble-phase listeners and also catch
287
- * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation
288
- * handler always runs, even when a child element calls `stopPropagation()`.
289
- *
290
- * ### Dynamic content
291
- *
292
- * Because the listener lives on `document.body`, any element added to the DOM
293
- * after this call — via `innerHTML`, `lit-html`, a framework renderer, or
294
- * server-sent HTML — is automatically covered. No re-bootstrap is needed.
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="ui_add_to_cart:42">Add</button>
305
- * </section>
306
- * ```
307
- *
308
- * ```ts
309
- * onAction('ui_add_to_cart', (action) => {
310
- * console.log(action.context); // 'product-list'
311
- * });
312
- * ```
313
- *
314
- * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.
315
- *
316
- * @example — minimal bootstrap
317
- * ```ts
318
- * import {setupActionDelegation, onAction} from '@alwatr/action';
319
- *
320
- * // One call activates the entire page.
321
- * setupActionDelegation();
322
- *
323
- * onAction('ui_open_drawer', (action) => openDrawer(action.payload));
324
- * ```
325
- *
326
- * @example — with extra event types
327
- * ```ts
328
- * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';
329
- *
330
- * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);
331
- * ```
332
- */
333
- export function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {
334
- logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});
335
-
336
- for (const eventType of eventTypes) {
337
- if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip
338
- delegatedEventTypes__.add(eventType);
339
- // capture: true — fires before bubble-phase listeners and catches non-bubbling events.
340
- document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});
341
- }
342
- }
343
-
344
- /**
345
- * Removes all global delegation listeners registered by `setupActionDelegation`.
346
- *
347
- * Useful in test environments where each test needs a clean slate, or in
348
- * micro-frontend setups where a sub-app is unmounted.
349
- *
350
- * After calling this, `setupActionDelegation` can be called again to re-register.
351
- */
352
- export function teardownActionDelegation(): void {
353
- logger_.logMethod?.('teardownActionDelegation');
354
- for (const eventType of delegatedEventTypes__) {
355
- document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});
356
- }
357
- delegatedEventTypes__.clear();
358
- descriptorCache__.clear();
359
- }
package/src/lib_.ts DELETED
@@ -1,27 +0,0 @@
1
- import {createLogger} from '@alwatr/logger';
2
- import {createChannelSignal} from '@alwatr/signal';
3
- import type {Action} from './type.js';
4
-
5
- /**
6
- * Module-scoped logger for `@alwatr/action`.
7
- * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.
8
- *
9
- * @internal
10
- */
11
- export const logger_ = createLogger('alwatr-action');
12
-
13
- /**
14
- * The internal action channel — a `ChannelSignal` keyed by action `type`.
15
- *
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.
20
- *
21
- * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a
22
- * single `Map.get('A')` lookup and invokes only the handlers registered for
23
- * that specific type — never handlers for `'B'`, `'C'`, etc.
24
- *
25
- * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.
26
- */
27
- export const internalChannel_ = createChannelSignal<Record<string, Action>>({name: 'alwatr-action'});
package/src/method.ts DELETED
@@ -1,219 +0,0 @@
1
- import type {Awaitable, VoidFunc} from '@alwatr/type-helper';
2
- import type {SubscribeResult} from '@alwatr/signal';
3
-
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';
7
-
8
- // ─── Core Action API ──────────────────────────────────────────────────────────
9
-
10
- /**
11
- * Subscribes to a named action dispatched anywhere in the application.
12
- *
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`:
17
- *
18
- * ```ts
19
- * // ActionRecord declares: 'ui_add_to_cart': {productId: number; qty: number}
20
- * onAction('ui_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
- * });
24
- * ```
25
- *
26
- * Passing an action name not declared in `ActionRecord` is a **compile error**.
27
- * Register new actions by extending `ActionRecord` via declaration merging:
28
- *
29
- * ```ts
30
- * // src/action-record.ts
31
- * declare module '@alwatr/action' {
32
- * interface ActionRecord {
33
- * 'ui_open_drawer': string;
34
- * }
35
- * }
36
- * ```
37
- *
38
- * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching
39
- * action `'A'` never invokes handlers registered for action `'B'`.
40
- *
41
- * @param type - A key of `ActionRecord`.
42
- * @param handler - Callback invoked with the full `Action<K>` on each dispatch.
43
- * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
44
- *
45
- * @example
46
- * ```ts
47
- * import {onAction} from '@alwatr/action';
48
- *
49
- * const sub = onAction('ui_page_ready', (action) => {
50
- * router.setPage(action.payload); // payload: string — inferred from ActionRecord
51
- * });
52
- *
53
- * sub.unsubscribe(); // stop listening when no longer needed
54
- * ```
55
- */
56
- export function onAction<K extends keyof ActionRecord>(
57
- type: K | K[],
58
- handler: (action: Action<K>) => Awaitable<void>,
59
- ): SubscribeResult {
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
- if (Array.isArray(type)) {
65
- const typeList = type as K[];
66
- const unsubscribeList: VoidFunc[] = [];
67
- for (const type_ of typeList) {
68
- unsubscribeList.push(internalChannel_.on(type_, handler as (action: Action) => Awaitable<void>).unsubscribe);
69
- }
70
- return {
71
- unsubscribe: () => {
72
- for (const unsubscribe of unsubscribeList) {
73
- unsubscribe();
74
- }
75
- unsubscribeList.length = 0;
76
- },
77
- };
78
- }
79
- // else single type
80
- return internalChannel_.on(type, handler as (action: Action) => Awaitable<void>);
81
- }
82
-
83
- /**
84
- * Dispatches an action to all `onAction` subscribers with a matching `type`.
85
- *
86
- * Accepts a full `Action<K>` object. The `payload` field is automatically
87
- * typed from `ActionRecord[K]` — passing the wrong shape is a **compile error**:
88
- *
89
- * ```ts
90
- * // ActionRecord declares: 'ui_add_to_cart': {productId: number; qty: number}
91
- * dispatchAction({type: 'ui_add_to_cart', payload: {productId: 42, qty: 1}}); // ✅
92
- * dispatchAction({type: 'ui_add_to_cart', payload: 'wrong'}); // ❌ compile error
93
- * dispatchAction({type: 'unknown_action', payload: 'x'}); // ❌ compile error
94
- * ```
95
- *
96
- * The `context` and `meta` fields are optional. When dispatching from code
97
- * (not from the DOM), omit `context` — it is only meaningful for DOM-originated
98
- * actions where an `[action-context]` ancestor exists.
99
- *
100
- * Use `dispatchAction` when triggering an action from code — e.g. after an
101
- * async operation, from a service layer, or in tests. For DOM-driven actions,
102
- * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.
103
- *
104
- * @param action - A full `Action<K>` object with at minimum `type` and `payload`.
105
- *
106
- * @example — with payload (code-originated action — no 'ui_' prefix)
107
- * ```ts
108
- * import {dispatchAction} from '@alwatr/action';
109
- *
110
- * dispatchAction({type: 'navigate', payload: '/dashboard'});
111
- * dispatchAction({type: 'upload_complete', payload: fileId});
112
- * ```
113
- *
114
- * @example — void payload (payload field is optional and can be omitted entirely)
115
- * ```ts
116
- * dispatchAction({type: 'auth_expired'});
117
- * // or explicitly:
118
- * dispatchAction({type: 'auth_expired', payload: undefined});
119
- * ```
120
- *
121
- * @example — with context and meta
122
- * ```ts
123
- * dispatchAction({
124
- * type: 'slider_change',
125
- * payload: 75,
126
- * context: 'volume_slider',
127
- * meta: {traceId: 'abc-123'},
128
- * });
129
- * ```
130
- */
131
- export function dispatchAction<K extends keyof ActionRecord>(action: DispatchParam<K>): void {
132
- logger_.logMethodArgs?.('dispatchAction', action);
133
- internalChannel_.dispatch(action.type, action as Action<K>);
134
- }
135
-
136
- // ─── Extension API ────────────────────────────────────────────────────────────
137
-
138
- /**
139
- * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.
140
- *
141
- * A modifier is a comma-separated token placed after the `;` separator
142
- * (e.g. `on-click="action-id; mymod"`). Its handler runs before the payload is
143
- * resolved and the action is dispatched. Returning `false` cancels the dispatch.
144
- *
145
- * The handler also receives the **mutable** `action` object being built, so it
146
- * can attach data to `action.meta` before the action reaches subscribers.
147
- *
148
- * Built-in modifiers (`prevent`, `validate`, `once`) are always available.
149
- * This function lets you add domain-specific ones.
150
- *
151
- * Registering the same name twice logs an accident and overwrites the previous
152
- * handler — avoid duplicate registrations in production code.
153
- *
154
- * @param name - The modifier token (lowercase, no special characters).
155
- * @param handler - A `ModifierHandler` receiving `(event, element, action)`.
156
- *
157
- * @example — a `confirm` modifier that shows a browser dialog
158
- * ```ts
159
- * import {registerModifier} from '@alwatr/action';
160
- *
161
- * registerModifier('confirm', () => window.confirm('Are you sure?'));
162
- * ```
163
- * ```html
164
- * <button on-click="ui_delete_item:42; confirm">Delete</button>
165
- * ```
166
- *
167
- * @example — a `trace` modifier that stamps a trace ID into meta
168
- * ```ts
169
- * registerModifier('trace', (_event, _element, action) => {
170
- * action.meta ??= {};
171
- * action.meta['traceId'] = crypto.randomUUID();
172
- * return true;
173
- * });
174
- * ```
175
- */
176
- export function registerModifier(name: string, handler: ModifierHandler): void {
177
- logger_.logMethodArgs?.('registerModifier', {name});
178
- if (modifierRegistry.has(name)) {
179
- logger_.accident('registerModifier', 'modifier_already_registered', {name});
180
- }
181
- modifierRegistry.set(name, handler);
182
- }
183
-
184
- /**
185
- * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.
186
- *
187
- * A payload resolver is a colon-prefixed token in the attribute value
188
- * (e.g. `on-click="action-id:$mytoken"`). Its function is called at dispatch time
189
- * with the DOM event and the element. The return value becomes the `payload`
190
- * field of the `Action` object passed to `onAction` subscribers.
191
- *
192
- * Built-in resolvers (`$value`, `$formdata`, `$checked`) are always available.
193
- * This function lets you add domain-specific ones.
194
- *
195
- * Registering the same name twice logs an accident and overwrites the previous
196
- * resolver — avoid duplicate registrations in production code.
197
- *
198
- * @param name - The resolver token (should start with `$` by convention).
199
- * @param resolver - A `PayloadResolver` receiving `(event, element)`.
200
- *
201
- * @example — a `$data-id` resolver that reads a data attribute
202
- * ```ts
203
- * import {registerPayloadResolver} from '@alwatr/action';
204
- *
205
- * registerPayloadResolver('$data-id', (_event, element) => {
206
- * return (element as HTMLElement).dataset.id ?? null;
207
- * });
208
- * ```
209
- * ```html
210
- * <button on-click="ui_select_item:$data-id" data-id="42">Select</button>
211
- * ```
212
- */
213
- export function registerPayloadResolver(name: string, resolver: PayloadResolver): void {
214
- logger_.logMethodArgs?.('registerPayloadResolver', {name});
215
- if (payloadRegistry.has(name)) {
216
- logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});
217
- }
218
- payloadRegistry.set(name, resolver);
219
- }