@alwatr/action 9.13.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/README.md CHANGED
@@ -19,26 +19,27 @@
19
19
 
20
20
  ## How It Works
21
21
 
22
+ ### Action Bus
23
+
24
+ The action bus is powered by a [`ChannelSignal`](../signal/README.md) from `@alwatr/signal`. Dispatching action `'A'` performs a single `Map.get('A')` lookup and invokes only the handlers registered for that specific action — **O(1) per dispatch**, regardless of how many other actions are subscribed.
25
+
22
26
  ### Global Event Delegation
23
27
 
24
- Instead of attaching one listener per element, a single capture-phase listener is registered on `document.body` for each event type. When an event fires anywhere on the page, the handler walks up from `event.target` using `closest('[on-action]')` to find the nearest element with an `on-action` attribute, parses the attribute, runs modifiers, resolves the payload, and calls `dispatchAction`.
28
+ A single capture-phase listener on `document.body` handles all `on-action` elements. When an event fires, the handler walks up from `event.target` using `closest('[on-action]')`, parses the attribute, runs modifiers, resolves the payload, and dispatches the action.
25
29
 
26
30
  ```
27
31
  User clicks a button
28
32
 
29
33
 
30
- document.body capture listener (1 listener total)
34
+ document.body capture listener (1 listener per event type)
31
35
 
32
- └─ closest('[on-action]') → finds element
36
+ └─ closest('[on-action^=click]') → finds element
33
37
  parse attribute → 'click->add-to-cart:42'
34
38
  run modifiers → none
35
39
  resolve payload → '42'
36
- dispatchAction('add-to-cart', '42')
40
+ internalChannel_.dispatch('add-to-cart', '42')
37
41
 
38
-
39
- ChannelSignal.dispatch('add-to-cart', '42') [O(1)]
40
-
41
- └─ Map.get('add-to-cart') → invoke only matching handlers
42
+ └─ Map.get('add-to-cart') → O(1) → invoke only matching handlers
42
43
  ```
43
44
 
44
45
  ### Complexity
@@ -48,10 +49,11 @@ document.body capture listener (1 listener total)
48
49
  | Boot time | O(N elements) | **O(1)** |
49
50
  | Memory | O(N listeners) | **O(1)** |
50
51
  | Dynamic content | Requires re-bootstrap | **Works out-of-box** |
52
+ | `once` modifier | Native option | Remove attribute |
51
53
 
52
- ### Action Bus
54
+ ### `once` modifier
53
55
 
54
- The action bus is powered by a [`ChannelSignal`](../signal/README.md) from `@alwatr/signal`. Dispatching action `'A'` performs a single `Map.get('A')` lookup and invokes only the handlers registered for that specific action **O(1) per dispatch**, regardless of how many other actions are subscribed.
56
+ In delegation mode, `once` is implemented by removing the `on-action` attribute from the element after the first fire. This is simpler than a `WeakSet` cache and naturally handles element reuseif the element is re-rendered with the attribute, it fires again.
55
57
 
56
58
  ---
57
59
 
@@ -69,7 +71,7 @@ npm i @alwatr/action
69
71
 
70
72
  ### 1. Register your action types
71
73
 
72
- Create a declaration file in your package to extend `ActionRecord`. This gives you full type safety and IDE autocomplete across the entire app:
74
+ Extend `ActionRecord` via declaration merging. This gives you full type safety and IDE autocomplete passing an undeclared action name is a **compile error**.
73
75
 
74
76
  ```ts
75
77
  // src/action-record.ts
@@ -83,22 +85,18 @@ declare module '@alwatr/action' {
83
85
  }
84
86
  ```
85
87
 
86
- Passing an action name not declared in `ActionRecord` is a **compile error** — there is no string fallback.
87
-
88
88
  ### 2. Bootstrap delegation
89
89
 
90
90
  ```ts
91
91
  import {setupActionDelegation, onAction} from '@alwatr/action';
92
92
  import './action-record.js'; // ensure the declaration is loaded
93
93
 
94
- // One call — the entire page is covered, including future dynamic content.
95
94
  setupActionDelegation();
96
95
 
97
- // Payload types are inferred automatically from ActionRecord — no generics needed.
96
+ // Payload types are inferred from ActionRecord — no generics needed.
98
97
  onAction('open-drawer', (panel) => openDrawer(panel)); // panel: string
99
- onAction('search-query', (query) => performSearch(query)); // query: string
100
98
  onAction('add-to-cart', (item) => {
101
- cartService.add(item.productId, item.qty); // fully typed, no `!`
99
+ cartService.add(item.productId, item.qty); // fully typed
102
100
  });
103
101
  ```
104
102
 
@@ -126,19 +124,20 @@ onAction('add-to-cart', (item) => {
126
124
  />
127
125
  <button type="submit">Save</button>
128
126
  </form>
127
+
128
+ <!-- Fires only once — attribute is removed after first click -->
129
+ <button on-action="click.once->welcome-dismissed">Got it</button>
129
130
  ```
130
131
 
131
- ### 3. Programmatic dispatch
132
+ ### 4. Programmatic dispatch
132
133
 
133
134
  ```ts
134
135
  import {dispatchAction} from '@alwatr/action';
135
136
 
136
- // Trigger actions from code — after async ops, from service layers, etc.
137
137
  await uploadFile(file);
138
138
  dispatchAction('upload-complete', fileId);
139
139
 
140
140
  dispatchAction('navigate', '/dashboard');
141
- dispatchAction<{code: number}>('show-error', {code: 404});
142
141
  ```
143
142
 
144
143
  ---
@@ -158,15 +157,11 @@ on-action="eventType[.modifier…]->actionId[:payload]"
158
157
 
159
158
  ### Built-in modifiers
160
159
 
161
- | Modifier | Behavior |
162
- | ----------- | -------------------------------------------------------------------- |
163
- | `.prevent` | Calls `event.preventDefault()` |
164
- | `.stop` | Calls `event.stopPropagation()` |
165
- | `.once` | Dispatches the action only once per element (emulated via `WeakSet`) |
166
- | `.validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
167
-
168
- > **Note:** `.passive` is not supported in delegation mode because all delegated
169
- > listeners must be non-passive to allow `.prevent` to work.
160
+ | Modifier | Behavior |
161
+ | ----------- | -------------------------------------------------------------------------------- |
162
+ | `.prevent` | Calls `event.preventDefault()` |
163
+ | `.once` | Removes the `on-action` attribute after first fire — action dispatches only once |
164
+ | `.validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
170
165
 
171
166
  ### Built-in payload resolvers
172
167
 
@@ -181,31 +176,22 @@ on-action="eventType[.modifier…]->actionId[:payload]"
181
176
 
182
177
  ### `ActionRecord` (interface)
183
178
 
184
- The global action type registry. Extend it via declaration merging to register your application's actions and unlock full type safety in `onAction` and `dispatchAction`.
179
+ The global action type registry. Extend via declaration merging to register typed actions.
185
180
 
186
181
  ```ts
187
- // src/action-record.ts
188
182
  declare module '@alwatr/action' {
189
183
  interface ActionRecord {
190
184
  'open-drawer': string;
191
- 'add-to-cart': {productId: number; qty: number};
192
185
  'logout': void;
193
186
  }
194
187
  }
195
188
  ```
196
189
 
197
- Once declared:
198
-
199
- - `onAction('open-drawer', (panel) => …)` — `panel` is inferred as `string`
200
- - `dispatchAction('add-to-cart', {productId: 42, qty: 1})` — payload type enforced
201
- - `dispatchAction('unknown-action', …)` — **compile error**
202
-
203
190
  ---
204
191
 
205
192
  ### `setupActionDelegation(eventTypes?)`
206
193
 
207
- Registers global event delegation on `document.body`. Call once at bootstrap.
208
- Subsequent calls with the same event types are no-ops (idempotent).
194
+ Registers global event delegation on `document.body`. Call once at bootstrap. Idempotent.
209
195
 
210
196
  ```ts
211
197
  function setupActionDelegation(eventTypes?: readonly string[]): void;
@@ -216,18 +202,14 @@ Defaults to `DEFAULT_DELEGATED_EVENTS`: `['click', 'submit', 'input', 'change']`
216
202
  ```ts
217
203
  import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';
218
204
 
219
- // Default events
220
- setupActionDelegation();
221
-
222
- // Add extra event types
223
- setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);
205
+ setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown']);
224
206
  ```
225
207
 
226
208
  ---
227
209
 
228
210
  ### `teardownActionDelegation()`
229
211
 
230
- Removes all delegation listeners. Useful in tests or micro-frontend teardown.
212
+ Removes all delegation listeners and clears the descriptor cache. Useful in tests or micro-frontend teardown.
231
213
 
232
214
  ```ts
233
215
  function teardownActionDelegation(): void;
@@ -237,54 +219,32 @@ function teardownActionDelegation(): void;
237
219
 
238
220
  ### `onAction(actionId, handler)`
239
221
 
240
- Subscribes to a named action. Uses `ChannelSignal.on()` for O(1) routing.
222
+ Subscribes to a named action. O(1) routing via `ChannelSignal`.
241
223
 
242
224
  ```ts
243
- function onAction<T = string>(actionId: string, handler: (payload?: T) => void): SubscribeResult;
225
+ function onAction<K extends keyof ActionRecord>(
226
+ actionId: K,
227
+ handler: (payload: ActionRecord[K]) => void,
228
+ ): SubscribeResult;
244
229
  ```
245
230
 
246
231
  ```ts
247
232
  const sub = onAction('open-drawer', (panel) => openDrawer(panel));
248
-
249
- // Unsubscribe when no longer needed (prevents memory leaks)
250
- sub.unsubscribe();
233
+ sub.unsubscribe(); // prevent memory leaks
251
234
  ```
252
235
 
253
236
  ---
254
237
 
255
238
  ### `dispatchAction(actionId, payload?)`
256
239
 
257
- Dispatches a named action to all matching `onAction` subscribers.
258
-
259
- ```ts
260
- function dispatchAction<T = string>(actionId: string, actionPayload?: T): void;
261
- ```
262
-
263
- ---
264
-
265
- ### `dispatchPageId(element?)`
266
-
267
- Reads the `page-id` attribute from `element` (defaults to `document.body`) and
268
- dispatches a `'page-ready'` action with the page identifier as payload.
240
+ Dispatches a named action. Payload type is enforced by `ActionRecord`.
269
241
 
270
242
  ```ts
271
- function dispatchPageId(element?: HTMLElement): void;
272
- ```
273
-
274
- ```html
275
- <body page-id="home">
276
-
277
- </body>
278
- ```
243
+ // With payload
244
+ dispatchAction('open-drawer', 'settings');
279
245
 
280
- ```ts
281
- import {dispatchPageId, onAction} from '@alwatr/action';
282
-
283
- dispatchPageId(); // → dispatchAction('page-ready', 'home')
284
-
285
- onAction('page-ready', (pageId) => {
286
- console.log('navigated to:', pageId); // 'home'
287
- });
246
+ // Void payload — no second argument
247
+ dispatchAction('logout');
288
248
  ```
289
249
 
290
250
  ---
@@ -292,40 +252,44 @@ onAction('page-ready', (pageId) => {
292
252
  ### `registerModifier(name, handler)`
293
253
 
294
254
  Registers a custom modifier. Return `false` to cancel the dispatch.
295
- Works with both delegation and programmatic dispatch.
255
+
256
+ Handler signature: `(event: Event, element: HTMLElement) => boolean`
296
257
 
297
258
  ```ts
298
259
  import {registerModifier} from '@alwatr/action';
299
260
 
300
- registerModifier('confirm', function () {
301
- return window.confirm('Are you sure?');
261
+ // Arrow function no `this` binding needed
262
+ registerModifier('not-disabled', (_event, element) => {
263
+ return !(element as HTMLButtonElement).disabled;
302
264
  });
303
265
  ```
304
266
 
305
267
  ```html
306
- <button on-action="click.confirm->delete-item:42">Delete</button>
307
- ```
308
-
309
- The handler receives an `ActionContext` as `this`:
310
-
311
- ```ts
312
- interface ActionContext {
313
- readonly element: HTMLElement; // the element with the on-action attribute
314
- }
268
+ <button
269
+ on-action="click.not-disabled->select-item:$data-id"
270
+ data-id="42"
271
+ >
272
+ Select
273
+ </button>
315
274
  ```
316
275
 
317
276
  ---
318
277
 
319
278
  ### `registerPayloadResolver(name, resolver)`
320
279
 
321
- Registers a custom payload resolver. The return value becomes the action payload.
322
- Works with both delegation and programmatic dispatch.
280
+ Registers a custom payload resolver.
281
+
282
+ Handler signature: `(event: Event, element: HTMLElement) => unknown`
323
283
 
324
284
  ```ts
325
285
  import {registerPayloadResolver} from '@alwatr/action';
326
286
 
327
- registerPayloadResolver('$checked', function () {
328
- return (this.element as HTMLInputElement).checked;
287
+ registerPayloadResolver('$checked', (_event, element) => {
288
+ return (element as HTMLInputElement).checked;
289
+ });
290
+
291
+ registerPayloadResolver('$data-id', (_event, element) => {
292
+ return (element as HTMLElement).dataset.id ?? null;
329
293
  });
330
294
  ```
331
295
 
@@ -334,6 +298,12 @@ registerPayloadResolver('$checked', function () {
334
298
  type="checkbox"
335
299
  on-action="change->toggle-feature:$checked"
336
300
  />
301
+ <li
302
+ on-action="click->select-item:$data-id"
303
+ data-id="42"
304
+ >
305
+ Item
306
+ </li>
337
307
  ```
338
308
 
339
309
  ---
@@ -349,11 +319,11 @@ registerPayloadResolver('$checked', function () {
349
319
 
350
320
  ┌─────────────────────────────────────────────────────────┐
351
321
  │ Action Layer (@alwatr/action) │
352
- │ document.body capture listener (1 listener total) │
353
- │ → closest('[on-action]') → parse → run modifiers
354
- │ → dispatchAction('add-to-cart', '42') [O(1)]
322
+ │ document.body capture listener (1 per event type) │
323
+ │ → closest('[on-action]') → parse → modifiers
324
+ │ → internalChannel_.dispatch('add-to-cart', '42') [O(1)]│
355
325
  └────────────────────────┬────────────────────────────────┘
356
- action signal (O(1) routing)
326
+ │ O(1) routing via ChannelSignal
357
327
 
358
328
  ┌─────────────────────────────────────────────────────────┐
359
329
  │ Business Logic Layer │
@@ -372,62 +342,45 @@ registerPayloadResolver('$checked', function () {
372
342
 
373
343
  ---
374
344
 
375
- ## Migration from Previous Versions
376
-
377
- ### `registerActionDirective` / `registerPageIdDirective` removed
378
-
379
- The directive-based approach has been replaced by global delegation.
345
+ ## Page Identity
380
346
 
381
- **Before:**
347
+ For page-ready signals in SSG/SSR apps (reading `page-id` attribute and notifying
348
+ page-specific handlers), use [`@alwatr/page-ready`](../page-ready/README.md) instead.
349
+ It is intentionally separate from the action bus — page identity is a routing/lifecycle
350
+ concern, not a user-interaction action.
382
351
 
383
- ```ts
384
- import {registerActionDirective, registerPageIdDirective} from '@alwatr/action';
385
- import {bootstrapDirectives} from '@alwatr/directive';
386
-
387
- registerActionDirective();
388
- registerPageIdDirective();
389
- bootstrapDirectives();
390
- ```
391
-
392
- **After:**
393
-
394
- ```ts
395
- import {setupActionDelegation, dispatchPageId} from '@alwatr/action';
396
-
397
- setupActionDelegation();
398
- dispatchPageId();
399
- ```
400
-
401
- ### `ActionDirective` / `PageIdDirective` removed
352
+ ---
402
353
 
403
- These classes are no longer exported. Use `setupActionDelegation()` and
404
- `dispatchPageId()` instead.
354
+ ## Migration from Previous Versions
405
355
 
406
- ### `ModifierHandler` / `PayloadResolver` context changed
356
+ ### `ActionContext` removed
407
357
 
408
- The `this` context in custom modifier and resolver functions changed from
409
- `ActionDirective` to `ActionContext`:
358
+ The `this` context in modifier and resolver handlers changed to explicit parameters:
410
359
 
411
360
  **Before:**
412
361
 
413
362
  ```ts
414
363
  registerModifier('not-disabled', function () {
415
- return !(this.element_ as HTMLButtonElement).disabled; // this.element_
364
+ return !(this.element as HTMLButtonElement).disabled;
416
365
  });
417
366
  ```
418
367
 
419
368
  **After:**
420
369
 
421
370
  ```ts
422
- registerModifier('not-disabled', function () {
423
- return !(this.element as HTMLButtonElement).disabled; // this.element (no underscore)
371
+ registerModifier('not-disabled', (_event, element) => {
372
+ return !(element as HTMLButtonElement).disabled;
424
373
  });
425
374
  ```
426
375
 
427
- ### `ActionSignalPayload` removed
376
+ ### `once` behavior changed
377
+
378
+ Previously tracked via `WeakSet`. Now removes the `on-action` attribute after first fire.
379
+ Behavior is equivalent for typical use cases.
380
+
381
+ ### `page-ready` moved to `@alwatr/page-ready`
428
382
 
429
- This type was an implementation detail of the old `EventSignal`-based bus and
430
- is no longer needed. Use `onAction` and `dispatchAction` directly.
383
+ `dispatchPageId` / `onPageReady` are no longer part of this package.
431
384
 
432
385
  ---
433
386
 
@@ -33,8 +33,9 @@
33
33
  * Extend this interface via declaration merging to register your application's
34
34
  * actions and gain full type safety in `onAction` and `dispatchAction`.
35
35
  *
36
- * Built-in system actions are declared here. Application-level actions should
37
- * be declared in a dedicated `action-record.ts` file within each feature package.
36
+ * This interface is intentionally empty in the base package all actions are
37
+ * application-specific and should be declared in a dedicated `action-record.ts`
38
+ * file within each feature package.
38
39
  *
39
40
  * @example — registering actions in a feature package
40
41
  * ```ts
@@ -49,18 +50,5 @@
49
50
  * ```
50
51
  */
51
52
  export interface ActionRecord {
52
- /**
53
- * Dispatched by `dispatchPageId()` when the page identity is read from the
54
- * `page-id` HTML attribute. Payload is the page identifier string.
55
- *
56
- * @example
57
- * ```html
58
- * <body page-id="home">…</body>
59
- * ```
60
- * ```ts
61
- * onAction('page-ready', (pageId) => router.setPage(pageId));
62
- * ```
63
- */
64
- 'page-ready': string;
65
53
  }
66
54
  //# sourceMappingURL=action-record.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"action-record.d.ts","sourceRoot":"","sources":["../src/action-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB"}
1
+ {"version":3,"file":"action-record.d.ts","sourceRoot":"","sources":["../src/action-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,MAAM,WAAW,YAAY;CAAG"}
package/dist/lib.d.ts CHANGED
@@ -1,4 +1,3 @@
1
- import type { ActionRecord } from './action-record.js';
2
1
  /**
3
2
  * Module-scoped logger for `@alwatr/action`.
4
3
  * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.
@@ -17,12 +16,7 @@ export declare const logger_: import("@alwatr/logger").AlwatrLogger;
17
16
  * single `Map.get('A')` lookup and invokes only the handlers registered for
18
17
  * that specific action — never handlers for `'B'`, `'C'`, etc.
19
18
  *
20
- * `ActionRecord & Record<string, unknown>` satisfies the `ChannelSignal`
21
- * constraint (which requires an index signature) while keeping the public API
22
- * strictly limited to declared keys — the `Record<string, unknown>` part is
23
- * only visible to the internal channel, not to `onAction`/`dispatchAction`.
24
- *
25
19
  * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.
26
20
  */
27
- export declare const internalChannel_: import("@alwatr/signal").ChannelSignal<ActionRecord & Record<string, unknown>>;
21
+ export declare const internalChannel_: import("@alwatr/signal").ChannelSignal<Record<string, unknown>>;
28
22
  //# sourceMappingURL=lib.d.ts.map
package/dist/lib.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,OAAO,uCAAgC,CAAC;AAErD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,gBAAgB,gFAAuF,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,eAAO,MAAM,OAAO,uCAAgC,CAAC;AAErD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,iEAAwE,CAAC"}
package/dist/main.d.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * @alwatr/action — Declarative DOM action-dispatch for Unidirectional Data Flow.
3
3
  *
4
- * ## Two ways to activate `on-action` attributes
4
+ * ## Activating `on-action` attributes
5
5
  *
6
- * ### 1. Global delegation (recommended)
7
- *
8
- * One listener on `document.body` handles every `on-action` element past,
9
- * present, and future. O(1) boot time regardless of element count.
6
+ * Call `setupActionDelegation()` once at bootstrap. A single capture-phase
7
+ * listener on `document.body` handles every `on-action` element — including
8
+ * elements added dynamically after bootstrap with O(1) initialization cost.
10
9
  *
11
10
  * ```ts
12
11
  * import {setupActionDelegation, onAction} from '@alwatr/action';
@@ -15,22 +14,20 @@
15
14
  * onAction('open-drawer', (panel) => openDrawer(panel));
16
15
  * ```
17
16
  *
18
- * ### 2. Programmatic dispatch
19
- *
20
- * Dispatch actions from code without any HTML attribute:
17
+ * ## Programmatic dispatch
21
18
  *
22
19
  * ```ts
23
- * import {dispatchAction, onAction} from '@alwatr/action';
20
+ * import {dispatchAction} from '@alwatr/action';
24
21
  *
25
22
  * dispatchAction('navigate', '/dashboard');
26
- * onAction('navigate', (path) => router.push(path));
27
23
  * ```
28
24
  *
29
25
  * ## Registering typed actions
30
26
  *
31
- * Extend `ActionMap` via declaration merging to get full type safety:
27
+ * Extend `ActionRecord` via declaration merging to get full type safety:
32
28
  *
33
29
  * ```ts
30
+ * // src/action-record.ts
34
31
  * declare module '@alwatr/action' {
35
32
  * interface ActionRecord {
36
33
  * 'open-drawer': string;
@@ -46,11 +43,13 @@
46
43
  * - `setupActionDelegation` / `teardownActionDelegation` — global delegation lifecycle
47
44
  * - `DEFAULT_DELEGATED_EVENTS` — default event types covered by delegation
48
45
  * - `onAction` / `dispatchAction` — subscribe to and dispatch named actions
49
- * - `dispatchPageId` — read `page-id` attribute and dispatch `'page-ready'`
50
46
  * - `registerModifier` / `registerPayloadResolver` — extend the attribute syntax
47
+ *
48
+ * ## Page identity
49
+ *
50
+ * For page-ready signals in SSG/SSR apps, use `@alwatr/page-ready` instead.
51
51
  */
52
+ export type { ActionRecord } from './action-record.js';
52
53
  export * from './method.js';
53
54
  export * from './delegate.js';
54
- export * from './page-ready.js';
55
- export * from './action-record.js';
56
55
  //# sourceMappingURL=main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,YAAY,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"}
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/action v9.13.0 */
2
- import{createLogger as F}from"@alwatr/logger";import{createChannelSignal as L}from"@alwatr/signal";var G=F("alwatr-action"),W=L({name:"alwatr-action"});var K=new Map,N=new Map;K.set("prevent",(M)=>{return M.preventDefault(),!0});K.set("validate",function(M,q){let x=q instanceof HTMLFormElement?q:q.closest("form");if(!x)return!1;return x.checkValidity()});N.set("$value",function(M,q){return"value"in q?q.value:null});N.set("$formdata",function(M,q){let x=q instanceof HTMLFormElement?q:q.closest("form");return x?Object.fromEntries(new FormData(x).entries()):null});function f(M,q){return G.logMethodArgs?.("onAction",{actionId:M}),W.on(M,q)}function V(M,q){G.logMethodArgs?.("dispatchAction",{actionId:M,actionPayload:q}),W.dispatch(M,q)}function b(M,q){if(G.logMethodArgs?.("registerModifier",{name:M}),K.has(M))G.accident("registerModifier","modifier_already_registered",{name:M});K.set(M,q)}function p(M,q){if(G.logMethodArgs?.("registerPayloadResolver",{name:M}),N.has(M))G.accident("registerPayloadResolver","payload_resolver_already_registered",{name:M});N.set(M,q)}var S=/^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/,P=new Map;function j(M){G.logMethodArgs?.("parseDescriptor__",{attributeValue:M});let q=P.get(M);if(q!==void 0)return q;let x=M.match(S);if(!x)return G.accident("parseDescriptor__","invalid_syntax",{attributeValue:M}),P.set(M,null),null;let[z,...D]=x[1].split(".");if(!z)return G.accident("parseDescriptor__","missing_event_type",{attributeValue:M}),P.set(M,null),null;let H=new Set(D),O=x[2],J=x[3],Q={eventType:z,modifiers:H,actionId:O,payload:J};return P.set(M,Q),Q}var Z="on-action";function U(M){let q=M.type;G.logMethodArgs?.("handleDelegatedEvent__",{eventType:q});let x=M.target;if(!x)return;let z=x.closest?.(`[${Z}^=${q}]`);if(!z)return;let D=z.getAttribute?.(Z)?.trim();if(!D){G.accident("handleDelegatedEvent__","empty_attribute",{eventType:q,attributeValue:D,actionElement:z});return}if(!(z instanceof HTMLElement)){G.accident("handleDelegatedEvent__","target_not_html_element",{eventType:q,attributeValue:D,actionElement:z});return}let H=j(D);if(!H){G.accident("handleDelegatedEvent__","invalid_attribute",{eventType:q,attributeValue:D,actionElement:z});return}if(H.eventType!==q)return;if(G.logMethodArgs?.("handleDelegatedEvent__.action",H),H.modifiers.has("once"))z.removeAttribute(Z),P.delete(D);for(let J of H.modifiers){if(J==="once")continue;let Q=K.get(J);if(!Q){G.accident("handleDelegatedEvent__","unknown_modifier",{modifier:J,attributeValue:D});return}if(Q(M,z)===!1)return}let O=H.payload;if(O){let J=N.get(O);if(J)O=J(M,z)}W.dispatch(H.actionId,O)}var X=new Set,w=["click","submit","input","change"];function h(M=w){G.logMethodArgs?.("setupActionDelegation",{eventTypes:M});for(let q of M){if(X.has(q))continue;X.add(q),document.body.addEventListener(q,U,{capture:!0})}}function v(){G.logMethod?.("teardownActionDelegation");for(let M of X)document.body.removeEventListener(M,U,{capture:!0});X.clear(),P.clear()}import{createLogger as A}from"@alwatr/logger";import{createChannelSignal as $}from"@alwatr/signal";var Y=A("page-ready"),B=$({name:"page-ready"});function c(M,q){Y.logMethodArgs?.("onPageReady",{pageId:M}),B.on(M,q)}function s(){Y.logMethod?.("dispatchPageReady");let M=document.querySelector("[page-id]");if(!M){Y.incident?.("dispatchPageReady","element_not_found");return}let q=M.getAttribute("page-id")?.trim();if(!q){Y.accident("dispatchPageReady","empty_page_id",{element:M});return}B.dispatch(q)}export{v as teardownActionDelegation,h as setupActionDelegation,p as registerPayloadResolver,b as registerModifier,c as onPageReady,f as onAction,s as dispatchPageReady,V as dispatchAction,w as DEFAULT_DELEGATED_EVENTS};
1
+ /* 📦 @alwatr/action v9.14.0 */
2
+ import{createLogger as U}from"@alwatr/logger";import{createChannelSignal as w}from"@alwatr/signal";var D=U("alwatr-action"),Z=w({name:"alwatr-action"});var N=new Map,Q=new Map;N.set("prevent",(q)=>{return q.preventDefault(),!0});N.set("validate",(q,z)=>{let G=z instanceof HTMLFormElement?z:z.closest("form");if(!G)return!1;return G.checkValidity()});Q.set("$value",(q,z)=>{return"value"in z?z.value:null});Q.set("$formdata",(q,z)=>{let G=z instanceof HTMLFormElement?z:z.closest("form");return G?Object.fromEntries(new FormData(G).entries()):null});function R(q,z){return D.logMethodArgs?.("onAction",{actionId:q}),Z.on(q,z)}function k(...q){let[z,G]=q;D.logMethodArgs?.("dispatchAction",{actionId:z,actionPayload:G}),Z.dispatch(z,G)}function E(q,z){if(D.logMethodArgs?.("registerModifier",{name:q}),N.has(q))D.accident("registerModifier","modifier_already_registered",{name:q});N.set(q,z)}function V(q,z){if(D.logMethodArgs?.("registerPayloadResolver",{name:q}),Q.has(q))D.accident("registerPayloadResolver","payload_resolver_already_registered",{name:q});Q.set(q,z)}var x=/^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/,X=new Map;function B(q){D.logMethodArgs?.("parseDescriptor__",{attributeValue:q});let z=X.get(q);if(z!==void 0)return z;let G=q.match(x);if(!G)return D.accident("parseDescriptor__","invalid_syntax",{attributeValue:q}),X.set(q,null),null;let[S,...F]=G[1].split(".");if(!S)return D.accident("parseDescriptor__","missing_event_type",{attributeValue:q}),X.set(q,null),null;let J=new Set(F),W=G[2],K=G[3],Y={eventType:S,modifiers:J,actionId:W,payload:K};return X.set(q,Y),Y}var M="on-action";function O(q){let z=q.type;D.logMethodArgs?.("handleDelegatedEvent__",{eventType:z});let G=q.target;if(!G)return;let S=G.closest?.(`[${M}^=${z}]`);if(!S)return;let F=S.getAttribute?.(M)?.trim();if(!F){D.accident("handleDelegatedEvent__","empty_attribute",{eventType:z,attributeValue:F,actionElement:S});return}if(!(S instanceof HTMLElement)){D.accident("handleDelegatedEvent__","target_not_html_element",{eventType:z,attributeValue:F,actionElement:S});return}let J=B(F);if(!J){D.accident("handleDelegatedEvent__","invalid_attribute",{eventType:z,attributeValue:F,actionElement:S});return}if(J.eventType!==z)return;if(D.logMethodArgs?.("handleDelegatedEvent__.action",J),J.modifiers.has("once"))S.removeAttribute(M),X.delete(F);for(let K of J.modifiers){if(K==="once")continue;let Y=N.get(K);if(!Y){D.accident("handleDelegatedEvent__","unknown_modifier",{modifier:K,attributeValue:F});return}if(Y(q,S)===!1)return}let W=J.payload;if(W){let K=Q.get(W);if(K)W=K(q,S)}Z.dispatch(J.actionId,W)}var H=new Set,P=["click","submit","input","change"];function y(q=P){D.logMethodArgs?.("setupActionDelegation",{eventTypes:q});for(let z of q){if(H.has(z))continue;H.add(z),document.body.addEventListener(z,O,{capture:!0})}}function p(){D.logMethod?.("teardownActionDelegation");for(let q of H)document.body.removeEventListener(q,O,{capture:!0});H.clear(),X.clear()}export{p as teardownActionDelegation,y as setupActionDelegation,V as registerPayloadResolver,E as registerModifier,R as onAction,k as dispatchAction,P as DEFAULT_DELEGATED_EVENTS};
3
3
 
4
- //# debugId=2E017DAE8A17903F64756E2164756E21
4
+ //# debugId=7E5E853AFB39285764756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/lib.ts", "../src/registry.ts", "../src/method.ts", "../src/delegate.ts", "../src/page-ready.ts"],
3
+ "sources": ["../src/lib.ts", "../src/registry.ts", "../src/method.ts", "../src/delegate.ts"],
4
4
  "sourcesContent": [
5
- "import {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\nimport type {ActionRecord} from './action-record.js';\n\n/**\n * Module-scoped logger for `@alwatr/action`.\n * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.\n *\n * @internal\n */\nexport const logger_ = createLogger('alwatr-action');\n\n/**\n * The action channel — a `ChannelSignal` strictly typed by `ActionRecord`.\n *\n * Only action names declared in `ActionRecord` (via declaration merging) are\n * accepted at compile time. Passing an unknown action name to `onAction` or\n * `dispatchAction` is a **compile error** — there is no string fallback.\n *\n * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a\n * single `Map.get('A')` lookup and invokes only the handlers registered for\n * that specific action — never handlers for `'B'`, `'C'`, etc.\n *\n * `ActionRecord & Record<string, unknown>` satisfies the `ChannelSignal`\n * constraint (which requires an index signature) while keeping the public API\n * strictly limited to declared keys — the `Record<string, unknown>` part is\n * only visible to the internal channel, not to `onAction`/`dispatchAction`.\n *\n * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.\n */\nexport const internalChannel_ = createChannelSignal<ActionRecord & Record<string, unknown>>({name: 'alwatr-action'});\n",
6
- "// ─── Type Definitions ────────────────────────────────────────────────────────\n\n/**\n * A modifier handler used in `on-action` attribute syntax.\n *\n * Called with an `ActionContext` as `this` and the triggering DOM `event`.\n * Return `true` (or any truthy value) to allow the action to proceed,\n * or `false` to cancel the dispatch.\n *\n * @example\n * ```ts\n * // A modifier that only allows the action when the element is not disabled\n * const notDisabledHandler: ModifierHandler = function () {\n * return !(this.element as HTMLButtonElement).disabled;\n * };\n * ```\n */\nexport type ModifierHandler = (event: Event, element: HTMLElement) => boolean;\n\n/**\n * A payload resolver used in `on-action` attribute syntax.\n *\n * Called with an `ActionContext` as `this` and the triggering DOM `event`\n * at dispatch time. The return value becomes the `actionPayload` passed to\n * `onAction` subscribers. Use this to compute dynamic payloads from DOM state.\n *\n * @example\n * ```ts\n * // A resolver that returns the element's dataset id\n * const dataIdResolver: PayloadResolver = function () {\n * return (this.element as HTMLElement).dataset.id ?? null;\n * };\n * ```\n */\nexport type PayloadResolver = (event: Event, element: HTMLElement) => unknown;\n\n// ─── Registries ──────────────────────────────────────────────────────────────\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-action` attribute syntax\n * (e.g. `click.prevent->action-id`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-action` attribute syntax\n * (e.g. `click->action-id:$value`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-action=\"submit.prevent->submit-form\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `.prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\" novalidate>`\n */\nmodifierRegistry.set('validate', function (_, element) {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-action=\"input->search-query:$value\" />`\n */\npayloadRegistry.set('$value', function (_, element) {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\">`\n * ```ts\n * onAction<Record<string, FormDataEntryValue>>('submit-form', (data) => {\n * console.log(data); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', function (_, element) {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form).entries()) : null;\n});\n",
7
- "import {internalChannel_, logger_} from './lib.js';\nimport type {SubscribeResult} from '@alwatr/signal';\nimport {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';\nimport type {ActionRecord} from './action-record.js';\n\n// Re-export extension types so consumers can import them from the package root.\nexport type {ModifierHandler, PayloadResolver};\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter\n * is automatically typed to the corresponding `ActionRecord` value — no manual\n * generic annotation needed:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * onAction('add-to-cart', (item) => {\n * cartService.add(item.productId, item.qty); // fully typed, no `!` needed\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'open-drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param handler - Callback invoked with the typed payload on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('page-ready', (pageId) => {\n * router.setPage(pageId); // pageId: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n actionId: K,\n handler: (payload: ActionRecord[K]) => void,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {actionId});\n // Cast through `unknown` to bridge the gap between the strict public signature\n // (ActionRecord[K]) and the internal channel's wider type (ActionRecord & Record<string, unknown>).\n return internalChannel_.on(actionId as string, handler as (payload: unknown) => void);\n}\n\n/**\n * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.\n *\n * `actionId` must be a key of `ActionRecord`. The `payload` parameter is\n * automatically typed — passing the wrong type is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅\n * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error\n * dispatchAction('unknown-action', 'x'); // ❌ compile error\n * ```\n *\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'navigate': string;\n * 'logout': void;\n * }\n * }\n * ```\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-action` HTML attribute with `setupActionDelegation`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param actionPayload - The payload; type is enforced by `ActionRecord`.\n *\n * @example — with payload\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction('page-ready', 'home');\n * dispatchAction('navigate', '/dashboard');\n * ```\n *\n * @example — void payload (no second argument)\n * ```ts\n * dispatchAction('logout');\n * ```\n */\n// Overload for actions with a void/undefined payload — second argument omitted.\nexport function dispatchAction<K extends keyof ActionRecord>(\n ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]\n): void;\n// Implementation\nexport function dispatchAction(actionId: keyof ActionRecord, actionPayload?: unknown): void {\n logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});\n internalChannel_.dispatch(actionId as string, actionPayload);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-action` attribute syntax.\n *\n * A modifier is a dot-chained token placed after the event type\n * (e.g. `click.mymod->action-id`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always\n * available. This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no dots or arrows).\n * @param handler - The `ModifierHandler` function bound to an `ActionContext`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', function () {\n * return window.confirm('Are you sure?');\n * });\n * ```\n * ```html\n * <button on-action=\"click.confirm->delete-item:42\">Delete</button>\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-action` attribute syntax.\n *\n * A payload resolver is a colon-suffixed token in the attribute value\n * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time\n * with an `ActionContext` as `this` and the DOM event as the argument.\n * The return value becomes the `actionPayload` passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`) are always available. This function\n * lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - The `PayloadResolver` function bound to an `ActionContext`.\n *\n * @example — a `$checked` resolver for checkbox state\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$checked', function () {\n * return (this.element as HTMLInputElement).checked;\n * });\n * ```\n * ```html\n * <input type=\"checkbox\" on-action=\"change->toggle-feature:$checked\" />\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n",
8
- "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the **Qwik-inspired global delegation** pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-action`\n * attribute whose event type matches.\n * - Modifiers and payload resolvers run in the same pipeline as before.\n * - `dispatchAction` is called with the resolved payload.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by delete attribute elements after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib.js';\nimport {modifierRegistry, payloadRegistry} from './registry.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-action` attribute value into its three segments.\n *\n * Full syntax: `eventType[.modifier…]->actionId[:payload]`\n *\n * | Capture group | Matches | Example |\n * | ------------- | ------------------------------------------- | -------------------- |\n * | 1 | Event type + optional dot-chained modifiers | `click.prevent.once` |\n * | 2 | Action identifier | `open-drawer` |\n * | 3 | Optional payload token or literal | `main` / `$value` |\n *\n * @example\n * ```\n * 'click.prevent.once->open-drawer:main' → ['click.prevent.once', 'open-drawer', 'main']\n * 'input->search-query:$value' → ['input', 'search-query', '$value']\n * 'submit.prevent->submit-form' → ['submit.prevent', 'submit-form', undefined]\n * ```\n */\nconst syntaxRegex = /^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-action` attribute value.\n *\n * Caching avoids re-parsing the same attribute string on every event fire.\n * The cache is keyed by the raw attribute string so identical values share\n * the same descriptor object.\n */\ninterface ActionDescriptor {\n /** The DOM event type to listen for (e.g. `'click'`, `'input'`). */\n readonly eventType: string;\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or `$`-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * LRU-style cache for parsed `on-action` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-action` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * Using a plain `Map` here is intentional — attribute strings are short-lived\n * keys and the map size is bounded by the number of distinct `on-action` values\n * in the page, which is typically small.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-action` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const [eventType, ...modifierList] = match[1].split('.');\n if (!eventType) {\n logger_.accident('parseDescriptor__', 'missing_event_type', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const modifiers = new Set(modifierList);\n const actionId = match[2];\n const payload: string | undefined = match[3];\n\n const descriptor: ActionDescriptor = {\n eventType,\n modifiers,\n actionId,\n payload,\n };\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\nconst onActionAttrib__ = 'on-action';\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-action` attribute whose event type matches the current event.\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Run each modifier in order; if any returns `false`, abort.\n * 4. Resolve the payload token (literal or `$`-resolver).\n * 5. Call `dispatchAction(actionId, payload)`.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n // Walk up the DOM to find the closest element with a matching on-action attribute.\n // We use `closest` on the composedPath target to support Shadow DOM correctly.\n const target = event.target as Element | null;\n if (!target) return;\n\n // Find the nearest ancestor (or self) that has an on-action attribute\n const actionElement = target.closest?.(`[${onActionAttrib__}^=${eventType}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(onActionAttrib__)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, attributeValue, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) {\n logger_.accident('handleDelegatedEvent__', 'invalid_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (descriptor.eventType !== eventType) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', descriptor);\n\n // Step 1: handle once modifier\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(onActionAttrib__); // remove on-action to prevent repeat the action\n descriptorCache__.delete(attributeValue); // free memory for once\n }\n\n // Step 2: run modifiers\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled separately\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {modifier, attributeValue});\n return; // unknown modifier — abort to avoid silent misbehaviour\n }\n if (handler(event, actionElement) === false) return;\n }\n\n // Step 3: resolve payload\n let payload: unknown = descriptor.payload;\n if (payload) {\n const resolver = payloadRegistry.get(payload as string);\n if (resolver) payload = resolver(event, actionElement);\n }\n\n // Step 4: dispatch\n internalChannel_.dispatch(descriptor.actionId, payload);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-action` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All `on-action` processing — modifier execution,\n * payload resolution, and `dispatchAction` — happens inside that one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('open-drawer', (panel) => openDrawer(panel));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n",
9
- "import {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\n\nconst logger = createLogger('page-ready');\n\nconst pageReadyChannel_ = createChannelSignal<Record<string, undefined>>({\n name: 'page-ready',\n});\n\nexport function onPageReady<T extends string>(pageId: T, handler: () => void) {\n logger.logMethodArgs?.('onPageReady', {pageId});\n pageReadyChannel_.on(pageId, handler);\n}\n\nexport function dispatchPageReady(): void {\n logger.logMethod?.('dispatchPageReady');\n const element = document.querySelector('[page-id]');\n if (!element) {\n logger.incident?.('dispatchPageReady', 'element_not_found');\n return;\n }\n\n const pageId = element.getAttribute('page-id')?.trim();\n\n if (!pageId) {\n logger.accident('dispatchPageReady', 'empty_page_id', {element});\n return;\n }\n\n pageReadyChannel_.dispatch(pageId);\n}\n"
5
+ "import {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\n\n/**\n * Module-scoped logger for `@alwatr/action`.\n * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.\n *\n * @internal\n */\nexport const logger_ = createLogger('alwatr-action');\n\n/**\n * The action channel — a `ChannelSignal` strictly typed by `ActionRecord`.\n *\n * Only action names declared in `ActionRecord` (via declaration merging) are\n * accepted at compile time. Passing an unknown action name to `onAction` or\n * `dispatchAction` is a **compile error** — there is no string fallback.\n *\n * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a\n * single `Map.get('A')` lookup and invokes only the handlers registered for\n * that specific action — never handlers for `'B'`, `'C'`, etc.\n *\n * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.\n */\nexport const internalChannel_ = createChannelSignal<Record<string, unknown>>({name: 'alwatr-action'});\n",
6
+ "// ─── Type Definitions ────────────────────────────────────────────────────────\n\n/**\n * A modifier handler used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. Return `true` (or any truthy value) to allow the\n * action to proceed, or `false` to cancel the dispatch.\n *\n * Using explicit parameters instead of `this` binding makes handlers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A modifier that only allows the action when the element is not disabled\n * const notDisabledHandler: ModifierHandler = (_event, element) => {\n * return !(element as HTMLButtonElement).disabled;\n * };\n * ```\n */\nexport type ModifierHandler = (event: Event, element: HTMLElement) => boolean;\n\n/**\n * A payload resolver used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. The return value becomes the `actionPayload` passed\n * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.\n *\n * Using explicit parameters instead of `this` binding makes resolvers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A resolver that returns the element's dataset id\n * const dataIdResolver: PayloadResolver = (_event, element) => {\n * return (element as HTMLElement).dataset.id ?? null;\n * };\n * ```\n */\nexport type PayloadResolver = (event: Event, element: HTMLElement) => unknown;\n\n// ─── Registries ──────────────────────────────────────────────────────────────\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-action` attribute syntax\n * (e.g. `click.prevent->action-id`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-action` attribute syntax\n * (e.g. `click->action-id:$value`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-action=\"submit.prevent->submit-form\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `.prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\" novalidate>`\n */\nmodifierRegistry.set('validate', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-action=\"input->search-query:$value\" />`\n */\npayloadRegistry.set('$value', (_event, element) => {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\">`\n * ```ts\n * onAction('submit-form', (data) => {\n * console.log(data); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form).entries()) : null;\n});\n",
7
+ "import type {Awaitable} from '@alwatr/type-helper';\nimport {internalChannel_, logger_} from './lib.js';\nimport type {SubscribeResult} from '@alwatr/signal';\nimport {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';\nimport type {ActionRecord} from './action-record.js';\n\n// Re-export extension types so consumers can import them from the package root.\nexport type {ModifierHandler, PayloadResolver};\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter\n * is automatically typed to the corresponding `ActionRecord` value — no manual\n * generic annotation needed:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * onAction('add-to-cart', (item) => {\n * cartService.add(item.productId, item.qty); // fully typed, no `!` needed\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'open-drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param handler - Callback invoked with the typed payload on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('page-ready', (pageId) => {\n * router.setPage(pageId); // pageId: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n actionId: K,\n handler: (payload: ActionRecord[K]) => Awaitable<void>,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {actionId});\n return internalChannel_.on(actionId, handler as (payload: unknown) => Awaitable<void>);\n}\n\n/**\n * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.\n *\n * `actionId` must be a key of `ActionRecord`. The `payload` parameter is\n * automatically typed — passing the wrong type is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅\n * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error\n * dispatchAction('unknown-action', 'x'); // ❌ compile error\n * ```\n *\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'navigate': string;\n * 'logout': void;\n * }\n * }\n * ```\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-action` HTML attribute with `setupActionDelegation`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param actionPayload - The payload; type is enforced by `ActionRecord`.\n *\n * @example — with payload\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction('page-ready', 'home');\n * dispatchAction('navigate', '/dashboard');\n * ```\n *\n * @example — void payload (no second argument)\n * ```ts\n * dispatchAction('logout');\n * ```\n */\nexport function dispatchAction<K extends keyof ActionRecord>(\n ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]\n): void {\n const [actionId, actionPayload] = args;\n logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});\n internalChannel_.dispatch(actionId, actionPayload);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-action` attribute syntax.\n *\n * A modifier is a dot-chained token placed after the event type\n * (e.g. `click.mymod->action-id`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always\n * available. This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no dots or arrows).\n * @param handler - A `ModifierHandler` receiving `(event, element)`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', () => window.confirm('Are you sure?'));\n * ```\n * ```html\n * <button on-action=\"click.confirm->delete-item:42\">Delete</button>\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-action` attribute syntax.\n *\n * A payload resolver is a colon-suffixed token in the attribute value\n * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time\n * with an `ActionContext` as `this` and the DOM event as the argument.\n * The return value becomes the `actionPayload` passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`) are always available. This function\n * lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - A `PayloadResolver` receiving `(event, element)`.\n *\n * @example — a `$checked` resolver for checkbox state\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$checked', (_event, element) => {\n * return (element as HTMLInputElement).checked;\n * });\n * ```\n * ```html\n * <input type=\"checkbox\" on-action=\"change->toggle-feature:$checked\" />\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n",
8
+ "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the **Qwik-inspired global delegation** pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-action`\n * attribute whose event type matches.\n * - Modifiers and payload resolvers run in the same pipeline as before.\n * - `dispatchAction` is called with the resolved payload.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by delete attribute elements after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib.js';\nimport {modifierRegistry, payloadRegistry} from './registry.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-action` attribute value into its three segments.\n *\n * Full syntax: `eventType[.modifier…]->actionId[:payload]`\n *\n * | Capture group | Matches | Example |\n * | ------------- | ------------------------------------------- | -------------------- |\n * | 1 | Event type + optional dot-chained modifiers | `click.prevent.once` |\n * | 2 | Action identifier | `open-drawer` |\n * | 3 | Optional payload token or literal | `main` / `$value` |\n *\n * @example\n * ```\n * 'click.prevent.once->open-drawer:main' → ['click.prevent.once', 'open-drawer', 'main']\n * 'input->search-query:$value' → ['input', 'search-query', '$value']\n * 'submit.prevent->submit-form' → ['submit.prevent', 'submit-form', undefined]\n * ```\n */\nconst syntaxRegex = /^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-action` attribute value.\n *\n * Caching avoids re-parsing the same attribute string on every event fire.\n * The cache is keyed by the raw attribute string so identical values share\n * the same descriptor object.\n */\ninterface ActionDescriptor {\n /** The DOM event type to listen for (e.g. `'click'`, `'input'`). */\n readonly eventType: string;\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or `$`-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * LRU-style cache for parsed `on-action` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-action` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * Using a plain `Map` here is intentional — attribute strings are short-lived\n * keys and the map size is bounded by the number of distinct `on-action` values\n * in the page, which is typically small.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-action` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const [eventType, ...modifierList] = match[1].split('.');\n if (!eventType) {\n logger_.accident('parseDescriptor__', 'missing_event_type', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const modifiers = new Set(modifierList);\n const actionId = match[2];\n const payload: string | undefined = match[3];\n\n const descriptor: ActionDescriptor = {\n eventType,\n modifiers,\n actionId,\n payload,\n };\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\nconst onActionAttrib__ = 'on-action';\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-action` attribute whose event type matches the current event.\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Run each modifier in order; if any returns `false`, abort.\n * 4. Resolve the payload token (literal or `$`-resolver).\n * 5. Call `dispatchAction(actionId, payload)`.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n // Walk up the DOM to find the closest element with a matching on-action attribute.\n // We use `closest` on the composedPath target to support Shadow DOM correctly.\n const target = event.target as Element | null;\n if (!target) return;\n\n // Find the nearest ancestor (or self) that has an on-action attribute\n const actionElement = target.closest?.(`[${onActionAttrib__}^=${eventType}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(onActionAttrib__)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, attributeValue, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) {\n logger_.accident('handleDelegatedEvent__', 'invalid_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (descriptor.eventType !== eventType) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', descriptor);\n\n // Step 1: handle once modifier\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(onActionAttrib__); // remove on-action to prevent repeat the action\n descriptorCache__.delete(attributeValue); // free memory for once\n }\n\n // Step 2: run modifiers\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled separately\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {modifier, attributeValue});\n return; // unknown modifier — abort to avoid silent misbehaviour\n }\n if (handler(event, actionElement) === false) return;\n }\n\n // Step 3: resolve payload\n let payload: unknown = descriptor.payload;\n if (payload) {\n const resolver = payloadRegistry.get(payload as string);\n if (resolver) payload = resolver(event, actionElement);\n }\n\n // Step 4: dispatch\n internalChannel_.dispatch(descriptor.actionId, payload);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-action` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All `on-action` processing — modifier execution,\n * payload resolution, and `dispatchAction` — happens inside that one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('open-drawer', (panel) => openDrawer(panel));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n"
10
9
  ],
11
- "mappings": "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",
12
- "debugId": "2E017DAE8A17903F64756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "7E5E853AFB39285764756E2164756E21",
13
12
  "names": []
14
13
  }
package/dist/method.d.ts CHANGED
@@ -1,3 +1,4 @@
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';
3
4
  import type { ActionRecord } from './action-record.js';
@@ -46,7 +47,7 @@ export type { ModifierHandler, PayloadResolver };
46
47
  * sub.unsubscribe(); // stop listening when no longer needed
47
48
  * ```
48
49
  */
49
- export declare function onAction<K extends keyof ActionRecord>(actionId: K, handler: (payload: ActionRecord[K]) => void): SubscribeResult;
50
+ export declare function onAction<K extends keyof ActionRecord>(actionId: K, handler: (payload: ActionRecord[K]) => Awaitable<void>): SubscribeResult;
50
51
  /**
51
52
  * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.
52
53
  *
@@ -107,15 +108,13 @@ export declare function dispatchAction<K extends keyof ActionRecord>(...args: Ac
107
108
  * handler — avoid duplicate registrations in production code.
108
109
  *
109
110
  * @param name - The modifier token (lowercase, no dots or arrows).
110
- * @param handler - The `ModifierHandler` function bound to an `ActionContext`.
111
+ * @param handler - A `ModifierHandler` receiving `(event, element)`.
111
112
  *
112
113
  * @example — a `confirm` modifier that shows a browser dialog
113
114
  * ```ts
114
115
  * import {registerModifier} from '@alwatr/action';
115
116
  *
116
- * registerModifier('confirm', function () {
117
- * return window.confirm('Are you sure?');
118
- * });
117
+ * registerModifier('confirm', () => window.confirm('Are you sure?'));
119
118
  * ```
120
119
  * ```html
121
120
  * <button on-action="click.confirm->delete-item:42">Delete</button>
@@ -137,14 +136,14 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
137
136
  * resolver — avoid duplicate registrations in production code.
138
137
  *
139
138
  * @param name - The resolver token (should start with `$` by convention).
140
- * @param resolver - The `PayloadResolver` function bound to an `ActionContext`.
139
+ * @param resolver - A `PayloadResolver` receiving `(event, element)`.
141
140
  *
142
141
  * @example — a `$checked` resolver for checkbox state
143
142
  * ```ts
144
143
  * import {registerPayloadResolver} from '@alwatr/action';
145
144
  *
146
- * registerPayloadResolver('$checked', function () {
147
- * return (this.element as HTMLInputElement).checked;
145
+ * registerPayloadResolver('$checked', (_event, element) => {
146
+ * return (element as HTMLInputElement).checked;
148
147
  * });
149
148
  * ```
150
149
  * ```html
@@ -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;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,IAAI,GAC1C,eAAe,CAKjB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,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,CAAC;AASR;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;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
+ {"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,15 +1,18 @@
1
1
  /**
2
2
  * A modifier handler used in `on-action` attribute syntax.
3
3
  *
4
- * Called with an `ActionContext` as `this` and the triggering DOM `event`.
5
- * Return `true` (or any truthy value) to allow the action to proceed,
6
- * or `false` to cancel the 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.
7
10
  *
8
11
  * @example
9
12
  * ```ts
10
13
  * // A modifier that only allows the action when the element is not disabled
11
- * const notDisabledHandler: ModifierHandler = function () {
12
- * return !(this.element as HTMLButtonElement).disabled;
14
+ * const notDisabledHandler: ModifierHandler = (_event, element) => {
15
+ * return !(element as HTMLButtonElement).disabled;
13
16
  * };
14
17
  * ```
15
18
  */
@@ -17,15 +20,18 @@ export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
17
20
  /**
18
21
  * A payload resolver used in `on-action` attribute syntax.
19
22
  *
20
- * Called with an `ActionContext` as `this` and the triggering DOM `event`
21
- * at dispatch time. The return value becomes the `actionPayload` passed to
22
- * `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
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.
26
+ *
27
+ * Using explicit parameters instead of `this` binding makes resolvers
28
+ * compatible with arrow functions and easier to test in isolation.
23
29
  *
24
30
  * @example
25
31
  * ```ts
26
32
  * // A resolver that returns the element's dataset id
27
- * const dataIdResolver: PayloadResolver = function () {
28
- * return (this.element as HTMLElement).dataset.id ?? null;
33
+ * const dataIdResolver: PayloadResolver = (_event, element) => {
34
+ * return (element as HTMLElement).dataset.id ?? null;
29
35
  * };
30
36
  * ```
31
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../src/registry.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC;AAE9E;;;;;;;;;;;;;;GAcG;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"}
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.13.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,13 +21,13 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/logger": "9.11.2",
25
- "@alwatr/signal": "9.13.0"
24
+ "@alwatr/logger": "9.14.0",
25
+ "@alwatr/signal": "9.14.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@alwatr/nano-build": "9.10.1",
29
- "@alwatr/standard": "9.11.2",
30
- "@alwatr/type-helper": "9.11.2",
28
+ "@alwatr/nano-build": "9.14.0",
29
+ "@alwatr/standard": "9.14.0",
30
+ "@alwatr/type-helper": "9.14.0",
31
31
  "typescript": "^6.0.3"
32
32
  },
33
33
  "scripts": {
@@ -79,5 +79,5 @@
79
79
  "vanilla-js",
80
80
  "web-development"
81
81
  ],
82
- "gitHead": "da284d23e3173d589fa69376e51a098c5e89649d"
82
+ "gitHead": "4e499b23191d4460ea60f34cde8a99b472741f1a"
83
83
  }
@@ -34,8 +34,9 @@
34
34
  * Extend this interface via declaration merging to register your application's
35
35
  * actions and gain full type safety in `onAction` and `dispatchAction`.
36
36
  *
37
- * Built-in system actions are declared here. Application-level actions should
38
- * be declared in a dedicated `action-record.ts` file within each feature package.
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.
39
40
  *
40
41
  * @example — registering actions in a feature package
41
42
  * ```ts
@@ -49,18 +50,5 @@
49
50
  * }
50
51
  * ```
51
52
  */
52
- export interface ActionRecord {
53
- /**
54
- * Dispatched by `dispatchPageId()` when the page identity is read from the
55
- * `page-id` HTML attribute. Payload is the page identifier string.
56
- *
57
- * @example
58
- * ```html
59
- * <body page-id="home">…</body>
60
- * ```
61
- * ```ts
62
- * onAction('page-ready', (pageId) => router.setPage(pageId));
63
- * ```
64
- */
65
- 'page-ready': string;
66
- }
53
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
54
+ export interface ActionRecord {}
package/src/lib.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import {createLogger} from '@alwatr/logger';
2
2
  import {createChannelSignal} from '@alwatr/signal';
3
- import type {ActionRecord} from './action-record.js';
4
3
 
5
4
  /**
6
5
  * Module-scoped logger for `@alwatr/action`.
@@ -21,11 +20,6 @@ export const logger_ = createLogger('alwatr-action');
21
20
  * single `Map.get('A')` lookup and invokes only the handlers registered for
22
21
  * that specific action — never handlers for `'B'`, `'C'`, etc.
23
22
  *
24
- * `ActionRecord & Record<string, unknown>` satisfies the `ChannelSignal`
25
- * constraint (which requires an index signature) while keeping the public API
26
- * strictly limited to declared keys — the `Record<string, unknown>` part is
27
- * only visible to the internal channel, not to `onAction`/`dispatchAction`.
28
- *
29
23
  * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.
30
24
  */
31
- export const internalChannel_ = createChannelSignal<ActionRecord & Record<string, unknown>>({name: 'alwatr-action'});
25
+ export const internalChannel_ = createChannelSignal<Record<string, unknown>>({name: 'alwatr-action'});
package/src/main.ts CHANGED
@@ -1,12 +1,11 @@
1
1
  /**
2
2
  * @alwatr/action — Declarative DOM action-dispatch for Unidirectional Data Flow.
3
3
  *
4
- * ## Two ways to activate `on-action` attributes
4
+ * ## Activating `on-action` attributes
5
5
  *
6
- * ### 1. Global delegation (recommended)
7
- *
8
- * One listener on `document.body` handles every `on-action` element past,
9
- * present, and future. O(1) boot time regardless of element count.
6
+ * Call `setupActionDelegation()` once at bootstrap. A single capture-phase
7
+ * listener on `document.body` handles every `on-action` element — including
8
+ * elements added dynamically after bootstrap with O(1) initialization cost.
10
9
  *
11
10
  * ```ts
12
11
  * import {setupActionDelegation, onAction} from '@alwatr/action';
@@ -15,22 +14,20 @@
15
14
  * onAction('open-drawer', (panel) => openDrawer(panel));
16
15
  * ```
17
16
  *
18
- * ### 2. Programmatic dispatch
19
- *
20
- * Dispatch actions from code without any HTML attribute:
17
+ * ## Programmatic dispatch
21
18
  *
22
19
  * ```ts
23
- * import {dispatchAction, onAction} from '@alwatr/action';
20
+ * import {dispatchAction} from '@alwatr/action';
24
21
  *
25
22
  * dispatchAction('navigate', '/dashboard');
26
- * onAction('navigate', (path) => router.push(path));
27
23
  * ```
28
24
  *
29
25
  * ## Registering typed actions
30
26
  *
31
- * Extend `ActionMap` via declaration merging to get full type safety:
27
+ * Extend `ActionRecord` via declaration merging to get full type safety:
32
28
  *
33
29
  * ```ts
30
+ * // src/action-record.ts
34
31
  * declare module '@alwatr/action' {
35
32
  * interface ActionRecord {
36
33
  * 'open-drawer': string;
@@ -46,10 +43,12 @@
46
43
  * - `setupActionDelegation` / `teardownActionDelegation` — global delegation lifecycle
47
44
  * - `DEFAULT_DELEGATED_EVENTS` — default event types covered by delegation
48
45
  * - `onAction` / `dispatchAction` — subscribe to and dispatch named actions
49
- * - `dispatchPageId` — read `page-id` attribute and dispatch `'page-ready'`
50
46
  * - `registerModifier` / `registerPayloadResolver` — extend the attribute syntax
47
+ *
48
+ * ## Page identity
49
+ *
50
+ * For page-ready signals in SSG/SSR apps, use `@alwatr/page-ready` instead.
51
51
  */
52
+ export type {ActionRecord} from './action-record.js';
52
53
  export * from './method.js';
53
54
  export * from './delegate.js';
54
- export * from './page-ready.js';
55
- export * from './action-record.js';
package/src/method.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type {Awaitable} from '@alwatr/type-helper';
1
2
  import {internalChannel_, logger_} from './lib.js';
2
3
  import type {SubscribeResult} from '@alwatr/signal';
3
4
  import {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';
@@ -54,12 +55,10 @@ export type {ModifierHandler, PayloadResolver};
54
55
  */
55
56
  export function onAction<K extends keyof ActionRecord>(
56
57
  actionId: K,
57
- handler: (payload: ActionRecord[K]) => void,
58
+ handler: (payload: ActionRecord[K]) => Awaitable<void>,
58
59
  ): SubscribeResult {
59
60
  logger_.logMethodArgs?.('onAction', {actionId});
60
- // Cast through `unknown` to bridge the gap between the strict public signature
61
- // (ActionRecord[K]) and the internal channel's wider type (ActionRecord & Record<string, unknown>).
62
- return internalChannel_.on(actionId as string, handler as (payload: unknown) => void);
61
+ return internalChannel_.on(actionId, handler as (payload: unknown) => Awaitable<void>);
63
62
  }
64
63
 
65
64
  /**
@@ -107,14 +106,12 @@ export function onAction<K extends keyof ActionRecord>(
107
106
  * dispatchAction('logout');
108
107
  * ```
109
108
  */
110
- // Overload for actions with a void/undefined payload — second argument omitted.
111
109
  export function dispatchAction<K extends keyof ActionRecord>(
112
110
  ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]
113
- ): void;
114
- // Implementation
115
- export function dispatchAction(actionId: keyof ActionRecord, actionPayload?: unknown): void {
111
+ ): void {
112
+ const [actionId, actionPayload] = args;
116
113
  logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});
117
- internalChannel_.dispatch(actionId as string, actionPayload);
114
+ internalChannel_.dispatch(actionId, actionPayload);
118
115
  }
119
116
 
120
117
  // ─── Extension API ────────────────────────────────────────────────────────────
@@ -133,15 +130,13 @@ export function dispatchAction(actionId: keyof ActionRecord, actionPayload?: unk
133
130
  * handler — avoid duplicate registrations in production code.
134
131
  *
135
132
  * @param name - The modifier token (lowercase, no dots or arrows).
136
- * @param handler - The `ModifierHandler` function bound to an `ActionContext`.
133
+ * @param handler - A `ModifierHandler` receiving `(event, element)`.
137
134
  *
138
135
  * @example — a `confirm` modifier that shows a browser dialog
139
136
  * ```ts
140
137
  * import {registerModifier} from '@alwatr/action';
141
138
  *
142
- * registerModifier('confirm', function () {
143
- * return window.confirm('Are you sure?');
144
- * });
139
+ * registerModifier('confirm', () => window.confirm('Are you sure?'));
145
140
  * ```
146
141
  * ```html
147
142
  * <button on-action="click.confirm->delete-item:42">Delete</button>
@@ -170,14 +165,14 @@ export function registerModifier(name: string, handler: ModifierHandler): void {
170
165
  * resolver — avoid duplicate registrations in production code.
171
166
  *
172
167
  * @param name - The resolver token (should start with `$` by convention).
173
- * @param resolver - The `PayloadResolver` function bound to an `ActionContext`.
168
+ * @param resolver - A `PayloadResolver` receiving `(event, element)`.
174
169
  *
175
170
  * @example — a `$checked` resolver for checkbox state
176
171
  * ```ts
177
172
  * import {registerPayloadResolver} from '@alwatr/action';
178
173
  *
179
- * registerPayloadResolver('$checked', function () {
180
- * return (this.element as HTMLInputElement).checked;
174
+ * registerPayloadResolver('$checked', (_event, element) => {
175
+ * return (element as HTMLInputElement).checked;
181
176
  * });
182
177
  * ```
183
178
  * ```html
package/src/registry.ts CHANGED
@@ -3,15 +3,18 @@
3
3
  /**
4
4
  * A modifier handler used in `on-action` attribute syntax.
5
5
  *
6
- * Called with an `ActionContext` as `this` and the triggering DOM `event`.
7
- * Return `true` (or any truthy value) to allow the action to proceed,
8
- * or `false` to cancel the dispatch.
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.
9
12
  *
10
13
  * @example
11
14
  * ```ts
12
15
  * // A modifier that only allows the action when the element is not disabled
13
- * const notDisabledHandler: ModifierHandler = function () {
14
- * return !(this.element as HTMLButtonElement).disabled;
16
+ * const notDisabledHandler: ModifierHandler = (_event, element) => {
17
+ * return !(element as HTMLButtonElement).disabled;
15
18
  * };
16
19
  * ```
17
20
  */
@@ -20,15 +23,18 @@ export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
20
23
  /**
21
24
  * A payload resolver used in `on-action` attribute syntax.
22
25
  *
23
- * Called with an `ActionContext` as `this` and the triggering DOM `event`
24
- * at dispatch time. The return value becomes the `actionPayload` passed to
25
- * `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
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.
26
32
  *
27
33
  * @example
28
34
  * ```ts
29
35
  * // A resolver that returns the element's dataset id
30
- * const dataIdResolver: PayloadResolver = function () {
31
- * return (this.element as HTMLElement).dataset.id ?? null;
36
+ * const dataIdResolver: PayloadResolver = (_event, element) => {
37
+ * return (element as HTMLElement).dataset.id ?? null;
32
38
  * };
33
39
  * ```
34
40
  */
@@ -87,7 +93,7 @@ modifierRegistry.set('prevent', (event) => {
87
93
  *
88
94
  * @example `<form on-action="submit.prevent.validate->submit-form:$formdata" novalidate>`
89
95
  */
90
- modifierRegistry.set('validate', function (_, element) {
96
+ modifierRegistry.set('validate', (_event, element) => {
91
97
  const form = element instanceof HTMLFormElement ? element : element.closest('form');
92
98
  if (!form) return false;
93
99
  return form.checkValidity();
@@ -103,7 +109,7 @@ modifierRegistry.set('validate', function (_, element) {
103
109
  *
104
110
  * @example `<input on-action="input->search-query:$value" />`
105
111
  */
106
- payloadRegistry.set('$value', function (_, element) {
112
+ payloadRegistry.set('$value', (_event, element) => {
107
113
  return 'value' in element ? (element as {value: unknown}).value : null;
108
114
  });
109
115
 
@@ -116,12 +122,12 @@ payloadRegistry.set('$value', function (_, element) {
116
122
  *
117
123
  * @example `<form on-action="submit.prevent.validate->submit-form:$formdata">`
118
124
  * ```ts
119
- * onAction<Record<string, FormDataEntryValue>>('submit-form', (data) => {
125
+ * onAction('submit-form', (data) => {
120
126
  * console.log(data); // {username: 'ali', password: '…'}
121
127
  * });
122
128
  * ```
123
129
  */
124
- payloadRegistry.set('$formdata', function (_, element) {
130
+ payloadRegistry.set('$formdata', (_event, element) => {
125
131
  const form = element instanceof HTMLFormElement ? element : element.closest('form');
126
132
  return form ? Object.fromEntries(new FormData(form).entries()) : null;
127
133
  });
@@ -1,3 +0,0 @@
1
- export declare function onPageReady<T extends string>(pageId: T, handler: () => void): void;
2
- export declare function dispatchPageReady(): void;
3
- //# sourceMappingURL=page-ready.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"page-ready.d.ts","sourceRoot":"","sources":["../src/page-ready.ts"],"names":[],"mappings":"AASA,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,IAAI,QAG3E;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAgBxC"}
package/src/page-ready.ts DELETED
@@ -1,31 +0,0 @@
1
- import {createLogger} from '@alwatr/logger';
2
- import {createChannelSignal} from '@alwatr/signal';
3
-
4
- const logger = createLogger('page-ready');
5
-
6
- const pageReadyChannel_ = createChannelSignal<Record<string, undefined>>({
7
- name: 'page-ready',
8
- });
9
-
10
- export function onPageReady<T extends string>(pageId: T, handler: () => void) {
11
- logger.logMethodArgs?.('onPageReady', {pageId});
12
- pageReadyChannel_.on(pageId, handler);
13
- }
14
-
15
- export function dispatchPageReady(): void {
16
- logger.logMethod?.('dispatchPageReady');
17
- const element = document.querySelector('[page-id]');
18
- if (!element) {
19
- logger.incident?.('dispatchPageReady', 'element_not_found');
20
- return;
21
- }
22
-
23
- const pageId = element.getAttribute('page-id')?.trim();
24
-
25
- if (!pageId) {
26
- logger.accident('dispatchPageReady', 'empty_page_id', {element});
27
- return;
28
- }
29
-
30
- pageReadyChannel_.dispatch(pageId);
31
- }