@alwatr/action 9.13.0 → 9.16.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 +190 -176
- package/dist/action-record.d.ts +7 -19
- package/dist/action-record.d.ts.map +1 -1
- package/dist/delegate.d.ts +8 -8
- package/dist/delegate.d.ts.map +1 -1
- package/dist/lib.d.ts +1 -7
- package/dist/lib.d.ts.map +1 -1
- package/dist/main.d.ts +27 -16
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +7 -8
- package/dist/method.d.ts +26 -27
- package/dist/method.d.ts.map +1 -1
- package/dist/registry.d.ts +20 -14
- package/dist/registry.d.ts.map +1 -1
- package/package.json +7 -7
- package/src/action-record.ts +9 -21
- package/src/delegate.ts +67 -73
- package/src/lib.ts +1 -7
- package/src/main.ts +27 -16
- package/src/method.ts +30 -35
- package/src/registry.ts +46 -23
- package/dist/page-ready.d.ts +0 -3
- package/dist/page-ready.d.ts.map +0 -1
- package/src/page-ready.ts +0 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/action",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.16.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.
|
|
25
|
-
"@alwatr/signal": "9.
|
|
24
|
+
"@alwatr/logger": "9.16.0",
|
|
25
|
+
"@alwatr/signal": "9.16.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@alwatr/nano-build": "9.
|
|
29
|
-
"@alwatr/standard": "9.
|
|
30
|
-
"@alwatr/type-helper": "9.
|
|
28
|
+
"@alwatr/nano-build": "9.14.0",
|
|
29
|
+
"@alwatr/standard": "9.16.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": "
|
|
82
|
+
"gitHead": "c210044f6e8ab444ec2f9e600f095761cbd279bd"
|
|
83
83
|
}
|
package/src/action-record.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* // In your package: src/action-record.ts
|
|
14
14
|
* declare module '@alwatr/action' {
|
|
15
15
|
* interface ActionRecord {
|
|
16
|
-
* '
|
|
17
|
-
* '
|
|
16
|
+
* 'open_drawer': string;
|
|
17
|
+
* 'add_to_cart': {productId: number; qty: number};
|
|
18
18
|
* 'logout': void;
|
|
19
19
|
* }
|
|
20
20
|
* }
|
|
@@ -34,33 +34,21 @@
|
|
|
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
|
-
*
|
|
38
|
-
* be declared in a dedicated `action-record.ts`
|
|
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
|
|
42
43
|
* // pkg/my-feature/src/action-record.ts
|
|
43
44
|
* declare module '@alwatr/action' {
|
|
44
45
|
* interface ActionRecord {
|
|
45
|
-
* '
|
|
46
|
-
* '
|
|
46
|
+
* 'open_drawer': string;
|
|
47
|
+
* 'add_to_cart': {productId: number; qty: number};
|
|
47
48
|
* 'logout': void;
|
|
48
49
|
* }
|
|
49
50
|
* }
|
|
50
51
|
* ```
|
|
51
52
|
*/
|
|
52
|
-
|
|
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/delegate.ts
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
* time — O(N) initialization cost, O(N) memory for listener references, and
|
|
11
11
|
* zero support for elements added after bootstrap.
|
|
12
12
|
*
|
|
13
|
-
* This module implements the
|
|
13
|
+
* This module implements the global delegation pattern:
|
|
14
14
|
* - A single listener per event type is attached to `document.body` with
|
|
15
15
|
* `capture: true` (so it fires even for non-bubbling events).
|
|
16
16
|
* - When an event fires, the handler walks up the DOM from `event.target`
|
|
17
|
-
* using `closest()` to find the nearest element with an `on
|
|
18
|
-
* attribute
|
|
17
|
+
* using `closest()` to find the nearest element with an `on-<eventType>`
|
|
18
|
+
* attribute (e.g. `on-click`, `on-submit`).
|
|
19
19
|
* - Modifiers and payload resolvers run in the same pipeline as before.
|
|
20
20
|
* - `dispatchAction` is called with the resolved payload.
|
|
21
21
|
*
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* - `stop` stops further bubbling but the delegation handler has already
|
|
36
36
|
* captured the event at `body` level — it does not prevent other delegation
|
|
37
37
|
* handlers from running on the same element.
|
|
38
|
-
* - `once` is emulated by
|
|
38
|
+
* - `once` is emulated by removing the attribute after first fire.
|
|
39
39
|
*/
|
|
40
40
|
|
|
41
41
|
import {internalChannel_, logger_} from './lib.js';
|
|
@@ -44,65 +44,75 @@ import {modifierRegistry, payloadRegistry} from './registry.js';
|
|
|
44
44
|
// ─── Syntax Parser ────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
* Parses the `on
|
|
47
|
+
* Parses the `on-<eventType>` attribute value into its segments.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
49
|
+
* Syntax: `actionId[:payload][; modifier1,modifier2,…]`
|
|
50
50
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* |
|
|
51
|
+
* The event type is encoded in the **attribute name** itself (`on-click`,
|
|
52
|
+
* `on-submit`, etc.) rather than inside the value. This makes the HTML more
|
|
53
|
+
* readable and aligns with native event attribute conventions.
|
|
54
|
+
*
|
|
55
|
+
* | Capture group | Matches | Example |
|
|
56
|
+
* | ------------- | -------------------------------------- | ---------------------- |
|
|
57
|
+
* | 1 | Action identifier | `open_drawer` |
|
|
58
|
+
* | 2 | Optional payload token or literal | `main_menu` / `$value` |
|
|
59
|
+
* | 3 | Optional comma-separated modifier list | `prevent,validate` |
|
|
56
60
|
*
|
|
57
61
|
* @example
|
|
58
62
|
* ```
|
|
59
|
-
* '
|
|
60
|
-
*
|
|
61
|
-
*
|
|
63
|
+
* 'close_drawer'
|
|
64
|
+
* → actionId='close_drawer', payload=undefined, modifiers={}
|
|
65
|
+
*
|
|
66
|
+
* 'open_drawer:main_menu'
|
|
67
|
+
* → actionId='open_drawer', payload='main_menu', modifiers={}
|
|
68
|
+
*
|
|
69
|
+
* 'my_submit_handler:$formdata; prevent,validate'
|
|
70
|
+
* → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}
|
|
62
71
|
* ```
|
|
63
72
|
*/
|
|
64
|
-
const syntaxRegex = /^([a-z0-
|
|
73
|
+
const syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
|
|
65
74
|
|
|
66
75
|
// ─── Parsed Action Descriptor ─────────────────────────────────────────────────
|
|
67
76
|
|
|
68
77
|
/**
|
|
69
|
-
* Parsed and cached representation of a single `on
|
|
78
|
+
* Parsed and cached representation of a single `on-<eventType>` attribute value.
|
|
70
79
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* the
|
|
80
|
+
* Does not store `eventType` — the caller always has it from `event.type`,
|
|
81
|
+
* and the attribute name already encodes it (e.g. `on-click`), so storing it
|
|
82
|
+
* here would be redundant. This also keeps the cache key simple: just the raw
|
|
83
|
+
* attribute value string, with no composite key needed.
|
|
74
84
|
*/
|
|
75
85
|
interface ActionDescriptor {
|
|
76
|
-
/** The DOM event type to listen for (e.g. `'click'`, `'input'`). */
|
|
77
|
-
readonly eventType: string;
|
|
78
86
|
/** Set of active modifier names (e.g. `{'prevent', 'once'}`). */
|
|
79
87
|
readonly modifiers: ReadonlySet<string>;
|
|
80
88
|
/** The action identifier dispatched to `onAction` subscribers. */
|
|
81
89
|
readonly actionId: string;
|
|
82
|
-
/** Raw payload token from the attribute (literal string or
|
|
90
|
+
/** Raw payload token from the attribute (literal string or $-resolver key). */
|
|
83
91
|
readonly payload: string | undefined;
|
|
84
92
|
}
|
|
85
93
|
|
|
86
94
|
/**
|
|
87
|
-
*
|
|
95
|
+
* Cache for parsed `on-<eventType>` attribute values.
|
|
88
96
|
*
|
|
89
97
|
* Attribute strings are typically repeated across many elements (e.g. every
|
|
90
|
-
* "add to cart" button shares the same `on-
|
|
98
|
+
* "add to cart" button shares the same `on-click` value). Caching the parsed
|
|
91
99
|
* descriptor avoids redundant regex work on every event.
|
|
92
100
|
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
101
|
+
* The cache key is the raw attribute value string. No composite key with
|
|
102
|
+
* event type is needed because the attribute name already encodes the event
|
|
103
|
+
* type — `on-click="open_drawer"` and `on-submit="open_drawer"` are two
|
|
104
|
+
* separate attributes with the same value string, but they are read from
|
|
105
|
+
* different attribute names and never collide in this cache.
|
|
96
106
|
*
|
|
97
107
|
* @internal
|
|
98
108
|
*/
|
|
99
109
|
const descriptorCache__ = new Map<string, ActionDescriptor | null>();
|
|
100
110
|
|
|
101
111
|
/**
|
|
102
|
-
* Parses an `on
|
|
112
|
+
* Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.
|
|
103
113
|
*
|
|
104
114
|
* Returns `null` when the syntax is invalid. Results are cached by the raw
|
|
105
|
-
* attribute string so repeated calls for the same value are O(1).
|
|
115
|
+
* attribute value string so repeated calls for the same value are O(1).
|
|
106
116
|
*
|
|
107
117
|
* @internal
|
|
108
118
|
*/
|
|
@@ -120,23 +130,12 @@ function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
|
|
|
120
130
|
return null;
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const modifiers = new Set(modifierList);
|
|
131
|
-
const actionId = match[2];
|
|
132
|
-
const payload: string | undefined = match[3];
|
|
133
|
-
|
|
134
|
-
const descriptor: ActionDescriptor = {
|
|
135
|
-
eventType,
|
|
136
|
-
modifiers,
|
|
137
|
-
actionId,
|
|
138
|
-
payload,
|
|
139
|
-
};
|
|
133
|
+
const actionId = match[1];
|
|
134
|
+
const payload: string | undefined = match[2];
|
|
135
|
+
// match[3] is the raw modifier list string, e.g. "prevent,validate"
|
|
136
|
+
const modifierString = match[3];
|
|
137
|
+
const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();
|
|
138
|
+
const descriptor: ActionDescriptor = {modifiers, actionId, payload};
|
|
140
139
|
|
|
141
140
|
descriptorCache__.set(attributeValue, descriptor);
|
|
142
141
|
return descriptor;
|
|
@@ -144,16 +143,15 @@ function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
|
|
|
144
143
|
|
|
145
144
|
// ─── Core Delegation Handler ──────────────────────────────────────────────────
|
|
146
145
|
|
|
147
|
-
const onActionAttrib__ = 'on-action';
|
|
148
146
|
/**
|
|
149
147
|
* Central event handler attached to `document.body` for every delegated event type.
|
|
150
148
|
*
|
|
151
149
|
* Execution flow for each incoming event:
|
|
152
150
|
* 1. Walk up from `event.target` to find the nearest element with an
|
|
153
|
-
* `on
|
|
151
|
+
* `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).
|
|
154
152
|
* 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.
|
|
155
153
|
* 3. Run each modifier in order; if any returns `false`, abort.
|
|
156
|
-
* 4. Resolve the payload token (literal or
|
|
154
|
+
* 4. Resolve the payload token (literal or $-resolver).
|
|
157
155
|
* 5. Call `dispatchAction(actionId, payload)`.
|
|
158
156
|
*
|
|
159
157
|
* @internal
|
|
@@ -162,49 +160,45 @@ function handleDelegatedEvent__(event: Event): void {
|
|
|
162
160
|
const eventType = event.type;
|
|
163
161
|
logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});
|
|
164
162
|
|
|
165
|
-
// Walk up the DOM to find the closest element with a matching on-action attribute.
|
|
166
|
-
// We use `closest` on the composedPath target to support Shadow DOM correctly.
|
|
167
163
|
const target = event.target as Element | null;
|
|
168
164
|
if (!target) return;
|
|
169
165
|
|
|
170
|
-
//
|
|
171
|
-
const
|
|
166
|
+
// Attribute name encodes the event type: on-click, on-submit, etc.
|
|
167
|
+
const actionAttrib = `on-${eventType}`;
|
|
168
|
+
|
|
169
|
+
// Walk up the DOM to find the closest element with the matching on-<eventType> attribute.
|
|
170
|
+
const actionElement = target.closest?.(`[${actionAttrib}]`);
|
|
172
171
|
if (!actionElement) return;
|
|
173
172
|
|
|
174
|
-
const attributeValue = actionElement.getAttribute?.(
|
|
173
|
+
const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();
|
|
175
174
|
if (!attributeValue) {
|
|
176
|
-
logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType,
|
|
175
|
+
logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});
|
|
177
176
|
return;
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
if (!(actionElement instanceof HTMLElement)) {
|
|
181
|
-
logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType,
|
|
180
|
+
logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});
|
|
182
181
|
return;
|
|
183
182
|
}
|
|
184
183
|
|
|
185
184
|
const descriptor = parseDescriptor__(attributeValue);
|
|
186
|
-
if (!descriptor)
|
|
187
|
-
logger_.accident('handleDelegatedEvent__', 'invalid_attribute', {eventType, attributeValue, actionElement});
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
if (descriptor.eventType !== eventType) return;
|
|
185
|
+
if (!descriptor) return;
|
|
192
186
|
|
|
193
|
-
logger_.logMethodArgs?.('handleDelegatedEvent__.action', descriptor);
|
|
187
|
+
logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});
|
|
194
188
|
|
|
195
|
-
// Step 1: handle once modifier
|
|
189
|
+
// Step 1: handle once modifier — remove attribute before running other modifiers
|
|
190
|
+
// so that even if a modifier aborts, the element will not fire again.
|
|
196
191
|
if (descriptor.modifiers.has('once')) {
|
|
197
|
-
actionElement.removeAttribute(
|
|
198
|
-
descriptorCache__.delete(attributeValue); // free memory for once
|
|
192
|
+
actionElement.removeAttribute(actionAttrib);
|
|
199
193
|
}
|
|
200
194
|
|
|
201
195
|
// Step 2: run modifiers
|
|
202
196
|
for (const modifier of descriptor.modifiers) {
|
|
203
|
-
if (modifier === 'once') continue; // handled
|
|
197
|
+
if (modifier === 'once') continue; // handled above
|
|
204
198
|
const handler = modifierRegistry.get(modifier);
|
|
205
199
|
if (!handler) {
|
|
206
|
-
logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {modifier, attributeValue});
|
|
207
|
-
return; // unknown modifier — abort to avoid silent
|
|
200
|
+
logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});
|
|
201
|
+
return; // unknown modifier — abort to avoid silent misbehavior
|
|
208
202
|
}
|
|
209
203
|
if (handler(event, actionElement) === false) return;
|
|
210
204
|
}
|
|
@@ -246,11 +240,11 @@ const delegatedEventTypes__ = new Set<string>();
|
|
|
246
240
|
export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];
|
|
247
241
|
|
|
248
242
|
/**
|
|
249
|
-
* Registers global event delegation for `on
|
|
243
|
+
* Registers global event delegation for `on-<eventType>` attributes.
|
|
250
244
|
*
|
|
251
245
|
* Attaches a single `capture`-phase listener on `document.body` for each
|
|
252
|
-
* event type in `eventTypes`. All
|
|
253
|
-
*
|
|
246
|
+
* event type in `eventTypes`. All processing — modifier execution, payload
|
|
247
|
+
* resolution, and `dispatchAction` — happens inside that one handler.
|
|
254
248
|
*
|
|
255
249
|
* **Call this once at application bootstrap**, before any user interaction.
|
|
256
250
|
* Subsequent calls with the same event types are no-ops (idempotent).
|
|
@@ -276,7 +270,7 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
|
|
|
276
270
|
* // One call activates the entire page.
|
|
277
271
|
* setupActionDelegation();
|
|
278
272
|
*
|
|
279
|
-
* onAction('
|
|
273
|
+
* onAction('open_drawer', (panel) => openDrawer(panel));
|
|
280
274
|
* ```
|
|
281
275
|
*
|
|
282
276
|
* @example — with extra event types
|
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<
|
|
25
|
+
export const internalChannel_ = createChannelSignal<Record<string, unknown>>({name: 'alwatr-action'});
|
package/src/main.ts
CHANGED
|
@@ -1,40 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @alwatr/action — Declarative DOM action-dispatch for Unidirectional Data Flow.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## Activating `on-<eventType>` attributes
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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-click`, `on-submit`, etc. element —
|
|
8
|
+
* including elements added dynamically after bootstrap — with O(1) initialization cost.
|
|
10
9
|
*
|
|
11
10
|
* ```ts
|
|
12
11
|
* import {setupActionDelegation, onAction} from '@alwatr/action';
|
|
13
12
|
*
|
|
14
13
|
* setupActionDelegation();
|
|
15
|
-
* onAction('
|
|
14
|
+
* onAction('open_drawer', (panel) => openDrawer(panel));
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* ## Attribute syntax
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"
|
|
16
21
|
* ```
|
|
17
22
|
*
|
|
18
|
-
*
|
|
23
|
+
* ```html
|
|
24
|
+
* <button on-click="open_drawer:main">Open</button>
|
|
25
|
+
* <input on-input="search_query:$value" />
|
|
26
|
+
* <form on-submit="submit_form:$formdata; prevent,validate" novalidate>…</form>
|
|
27
|
+
* ```
|
|
19
28
|
*
|
|
20
|
-
*
|
|
29
|
+
* ## Programmatic dispatch
|
|
21
30
|
*
|
|
22
31
|
* ```ts
|
|
23
|
-
* import {dispatchAction
|
|
32
|
+
* import {dispatchAction} from '@alwatr/action';
|
|
24
33
|
*
|
|
25
34
|
* dispatchAction('navigate', '/dashboard');
|
|
26
|
-
* onAction('navigate', (path) => router.push(path));
|
|
27
35
|
* ```
|
|
28
36
|
*
|
|
29
37
|
* ## Registering typed actions
|
|
30
38
|
*
|
|
31
|
-
* Extend `
|
|
39
|
+
* Extend `ActionRecord` via declaration merging to get full type safety:
|
|
32
40
|
*
|
|
33
41
|
* ```ts
|
|
42
|
+
* // src/action-record.ts
|
|
34
43
|
* declare module '@alwatr/action' {
|
|
35
44
|
* interface ActionRecord {
|
|
36
|
-
* '
|
|
37
|
-
* '
|
|
45
|
+
* 'open_drawer': string;
|
|
46
|
+
* 'add_to_cart': {productId: number; qty: number};
|
|
38
47
|
* 'logout': void;
|
|
39
48
|
* }
|
|
40
49
|
* }
|
|
@@ -46,10 +55,12 @@
|
|
|
46
55
|
* - `setupActionDelegation` / `teardownActionDelegation` — global delegation lifecycle
|
|
47
56
|
* - `DEFAULT_DELEGATED_EVENTS` — default event types covered by delegation
|
|
48
57
|
* - `onAction` / `dispatchAction` — subscribe to and dispatch named actions
|
|
49
|
-
* - `dispatchPageId` — read `page-id` attribute and dispatch `'page-ready'`
|
|
50
58
|
* - `registerModifier` / `registerPayloadResolver` — extend the attribute syntax
|
|
59
|
+
*
|
|
60
|
+
* ## Page identity
|
|
61
|
+
*
|
|
62
|
+
* For page-ready signals in SSG/SSR apps, use `@alwatr/page-ready` instead.
|
|
51
63
|
*/
|
|
64
|
+
export type {ActionRecord} from './action-record.js';
|
|
52
65
|
export * from './method.js';
|
|
53
66
|
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';
|
|
@@ -16,8 +17,8 @@ export type {ModifierHandler, PayloadResolver};
|
|
|
16
17
|
* generic annotation needed:
|
|
17
18
|
*
|
|
18
19
|
* ```ts
|
|
19
|
-
* // ActionRecord declares: '
|
|
20
|
-
* onAction('
|
|
20
|
+
* // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
|
|
21
|
+
* onAction('add_to_cart', (item) => {
|
|
21
22
|
* cartService.add(item.productId, item.qty); // fully typed, no `!` needed
|
|
22
23
|
* });
|
|
23
24
|
* ```
|
|
@@ -29,7 +30,7 @@ export type {ModifierHandler, PayloadResolver};
|
|
|
29
30
|
* // src/action-record.ts
|
|
30
31
|
* declare module '@alwatr/action' {
|
|
31
32
|
* interface ActionRecord {
|
|
32
|
-
* '
|
|
33
|
+
* 'open_drawer': string;
|
|
33
34
|
* }
|
|
34
35
|
* }
|
|
35
36
|
* ```
|
|
@@ -45,7 +46,7 @@ export type {ModifierHandler, PayloadResolver};
|
|
|
45
46
|
* ```ts
|
|
46
47
|
* import {onAction} from '@alwatr/action';
|
|
47
48
|
*
|
|
48
|
-
* const sub = onAction('
|
|
49
|
+
* const sub = onAction('page_ready', (pageId) => {
|
|
49
50
|
* router.setPage(pageId); // pageId: string — inferred from ActionRecord
|
|
50
51
|
* });
|
|
51
52
|
*
|
|
@@ -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
|
-
|
|
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
|
/**
|
|
@@ -69,10 +68,10 @@ export function onAction<K extends keyof ActionRecord>(
|
|
|
69
68
|
* automatically typed — passing the wrong type is a **compile error**:
|
|
70
69
|
*
|
|
71
70
|
* ```ts
|
|
72
|
-
* // ActionRecord declares: '
|
|
73
|
-
* dispatchAction('
|
|
74
|
-
* dispatchAction('
|
|
75
|
-
* dispatchAction('
|
|
71
|
+
* // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
|
|
72
|
+
* dispatchAction('add_to_cart', {productId: 42, qty: 1}); // ✅
|
|
73
|
+
* dispatchAction('add_to_cart', 'wrong'); // ❌ compile error
|
|
74
|
+
* dispatchAction('unknown_action', 'x'); // ❌ compile error
|
|
76
75
|
* ```
|
|
77
76
|
*
|
|
78
77
|
* Register new actions by extending `ActionRecord` via declaration merging:
|
|
@@ -89,7 +88,7 @@ export function onAction<K extends keyof ActionRecord>(
|
|
|
89
88
|
*
|
|
90
89
|
* Use `dispatchAction` when triggering an action from code — e.g. after an
|
|
91
90
|
* async operation, from a service layer, or in tests. For DOM-driven actions,
|
|
92
|
-
* use the `on
|
|
91
|
+
* use the `on-<eventType>` HTML attribute with `setupActionDelegation`.
|
|
93
92
|
*
|
|
94
93
|
* @param actionId - A key of `ActionRecord`.
|
|
95
94
|
* @param actionPayload - The payload; type is enforced by `ActionRecord`.
|
|
@@ -98,7 +97,7 @@ export function onAction<K extends keyof ActionRecord>(
|
|
|
98
97
|
* ```ts
|
|
99
98
|
* import {dispatchAction} from '@alwatr/action';
|
|
100
99
|
*
|
|
101
|
-
* dispatchAction('
|
|
100
|
+
* dispatchAction('page_ready', 'home');
|
|
102
101
|
* dispatchAction('navigate', '/dashboard');
|
|
103
102
|
* ```
|
|
104
103
|
*
|
|
@@ -107,23 +106,21 @@ 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
|
-
|
|
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
|
|
114
|
+
internalChannel_.dispatch(actionId, actionPayload);
|
|
118
115
|
}
|
|
119
116
|
|
|
120
117
|
// ─── Extension API ────────────────────────────────────────────────────────────
|
|
121
118
|
|
|
122
119
|
/**
|
|
123
|
-
* Registers a custom modifier that can be used in `on
|
|
120
|
+
* Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.
|
|
124
121
|
*
|
|
125
|
-
* A modifier is a
|
|
126
|
-
* (e.g. `click
|
|
122
|
+
* A modifier is a comma-separated token placed after the `;` separator
|
|
123
|
+
* (e.g. `on-click="action-id; mymod"`). Its handler runs before the payload is
|
|
127
124
|
* resolved and the action is dispatched. Returning `false` cancels the dispatch.
|
|
128
125
|
*
|
|
129
126
|
* Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always
|
|
@@ -132,19 +129,17 @@ export function dispatchAction(actionId: keyof ActionRecord, actionPayload?: unk
|
|
|
132
129
|
* Registering the same name twice logs an accident and overwrites the previous
|
|
133
130
|
* handler — avoid duplicate registrations in production code.
|
|
134
131
|
*
|
|
135
|
-
* @param name - The modifier token (lowercase, no
|
|
136
|
-
* @param handler -
|
|
132
|
+
* @param name - The modifier token (lowercase, no special characters).
|
|
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',
|
|
143
|
-
* return window.confirm('Are you sure?');
|
|
144
|
-
* });
|
|
139
|
+
* registerModifier('confirm', () => window.confirm('Are you sure?'));
|
|
145
140
|
* ```
|
|
146
141
|
* ```html
|
|
147
|
-
* <button on-
|
|
142
|
+
* <button on-click="delete_item:42; confirm">Delete</button>
|
|
148
143
|
* ```
|
|
149
144
|
*/
|
|
150
145
|
export function registerModifier(name: string, handler: ModifierHandler): void {
|
|
@@ -156,10 +151,10 @@ export function registerModifier(name: string, handler: ModifierHandler): void {
|
|
|
156
151
|
}
|
|
157
152
|
|
|
158
153
|
/**
|
|
159
|
-
* Registers a custom payload resolver that can be used in `on
|
|
154
|
+
* Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.
|
|
160
155
|
*
|
|
161
|
-
* A payload resolver is a colon-
|
|
162
|
-
* (e.g. `click
|
|
156
|
+
* A payload resolver is a colon-prefixed token in the attribute value
|
|
157
|
+
* (e.g. `on-click="action-id:$mytoken"`). Its function is called at dispatch time
|
|
163
158
|
* with an `ActionContext` as `this` and the DOM event as the argument.
|
|
164
159
|
* The return value becomes the `actionPayload` passed to `onAction` subscribers.
|
|
165
160
|
*
|
|
@@ -170,18 +165,18 @@ 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 -
|
|
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',
|
|
180
|
-
* return (
|
|
174
|
+
* registerPayloadResolver('$checked', (_event, element) => {
|
|
175
|
+
* return (element as HTMLInputElement).checked;
|
|
181
176
|
* });
|
|
182
177
|
* ```
|
|
183
178
|
* ```html
|
|
184
|
-
* <input type="checkbox" on-
|
|
179
|
+
* <input type="checkbox" on-change="toggle_feature:$checked" />
|
|
185
180
|
* ```
|
|
186
181
|
*/
|
|
187
182
|
export function registerPayloadResolver(name: string, resolver: PayloadResolver): void {
|