@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/README.md +249 -70
- package/dist/delegate.d.ts +28 -5
- package/dist/delegate.d.ts.map +1 -1
- package/dist/{lib.d.ts → lib_.d.ts} +9 -7
- package/dist/lib_.d.ts.map +1 -0
- package/dist/main.d.ts +31 -5
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +7 -7
- package/dist/method.d.ts +63 -48
- package/dist/method.d.ts.map +1 -1
- package/dist/registry_.d.ts +24 -0
- package/dist/registry_.d.ts.map +1 -0
- package/dist/type.d.ts +160 -0
- package/dist/type.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/delegate.ts +71 -21
- package/src/{lib.ts → lib_.ts} +8 -6
- package/src/main.ts +31 -5
- package/src/method.ts +73 -58
- package/src/{registry.ts → registry_.ts} +7 -48
- package/src/type.ts +165 -0
- package/dist/action-record.d.ts +0 -54
- package/dist/action-record.d.ts.map +0 -1
- package/dist/lib.d.ts.map +0 -1
- package/dist/registry.d.ts +0 -61
- package/dist/registry.d.ts.map +0 -1
- package/src/action-record.ts +0 -54
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
|
-
* -
|
|
20
|
-
*
|
|
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 './
|
|
42
|
-
import {modifierRegistry, payloadRegistry} from './
|
|
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_
|
|
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.
|
|
154
|
-
* 4.
|
|
155
|
-
* 5.
|
|
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:
|
|
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
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
214
|
-
internalChannel_.dispatch(
|
|
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 —
|
|
247
|
-
* resolution, and `dispatchAction` — happens inside that
|
|
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', (
|
|
323
|
+
* onAction('open_drawer', (action) => openDrawer(action.payload));
|
|
274
324
|
* ```
|
|
275
325
|
*
|
|
276
326
|
* @example — with extra event types
|
package/src/{lib.ts → lib_.ts}
RENAMED
|
@@ -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`
|
|
14
|
+
* The internal action channel — a `ChannelSignal` keyed by action `type`.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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,
|
|
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', (
|
|
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
|
-
* - `
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
* `
|
|
16
|
-
*
|
|
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', (
|
|
22
|
-
* cartService.add(
|
|
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
|
|
42
|
-
* @param handler
|
|
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', (
|
|
50
|
-
* router.setPage(
|
|
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
|
-
|
|
58
|
-
handler: (
|
|
57
|
+
type: K,
|
|
58
|
+
handler: (action: Action<K>) => Awaitable<void>,
|
|
59
59
|
): SubscribeResult {
|
|
60
|
-
logger_.logMethodArgs?.('onAction', {
|
|
61
|
-
|
|
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
|
|
68
|
+
* Dispatches an action to all `onAction` subscribers with a matching `type`.
|
|
66
69
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
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('
|
|
101
|
-
* dispatchAction('
|
|
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 (
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
*
|
|
127
|
-
*
|
|
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
|
|
159
|
-
*
|
|
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.
|
|
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 `$
|
|
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('$
|
|
175
|
-
* return (element as
|
|
189
|
+
* registerPayloadResolver('$data-id', (_event, element) => {
|
|
190
|
+
* return (element as HTMLElement).dataset.id ?? null;
|
|
176
191
|
* });
|
|
177
192
|
* ```
|
|
178
193
|
* ```html
|
|
179
|
-
* <
|
|
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
|
-
|
|
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
|
|
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', (
|
|
126
|
-
* console.log(
|
|
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', (
|
|
144
|
-
*
|
|
101
|
+
* onAction('toggle_feature', (action) => {
|
|
102
|
+
* console.log(action.payload); // true or false
|
|
103
|
+
* featureSignal.set(action.payload);
|
|
145
104
|
* });
|
|
146
105
|
* ```
|
|
147
106
|
*/
|