@alwatr/action 9.12.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 +215 -147
- package/dist/action-record.d.ts +54 -0
- package/dist/action-record.d.ts.map +1 -0
- package/dist/delegate.d.ts +103 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/lib.d.ts +8 -29
- package/dist/lib.d.ts.map +1 -1
- package/dist/main.d.ts +48 -9
- 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 +70 -57
- package/dist/method.d.ts.map +1 -1
- package/dist/registry.d.ts +20 -15
- package/dist/registry.d.ts.map +1 -1
- package/package.json +7 -9
- package/src/action-record.ts +54 -0
- package/src/delegate.ts +315 -0
- package/src/lib.ts +9 -31
- package/src/main.ts +48 -9
- package/src/method.ts +79 -65
- package/src/registry.ts +31 -43
- package/dist/directive.d.ts +0 -94
- package/dist/directive.d.ts.map +0 -1
- package/dist/page-id.d.ts +0 -57
- package/dist/page-id.d.ts.map +0 -1
- package/src/directive.ts +0 -197
- package/src/page-id.ts +0 -74
package/README.md
CHANGED
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Declarative DOM action-dispatch — the Action layer for Unidirectional Data Flow.**
|
|
4
4
|
|
|
5
|
-
`@alwatr/action` bridges HTML attributes to typed signal handlers
|
|
6
|
-
|
|
7
|
-
This package serves as the **Action layer** in a Unidirectional Data Flow (UDF) architecture: UI elements declare their intent via `on-action` attributes, actions flow upward to business logic via `dispatchAction`, and state flows back down to the UI through signals.
|
|
5
|
+
`@alwatr/action` bridges HTML `on-action` attributes to typed signal handlers using **global event delegation**. One listener on `document.body` covers every element on the page — including elements added dynamically after bootstrap — with O(1) initialization cost regardless of how many elements exist.
|
|
8
6
|
|
|
9
7
|
---
|
|
10
8
|
|
|
@@ -12,82 +10,93 @@ This package serves as the **Action layer** in a Unidirectional Data Flow (UDF)
|
|
|
12
10
|
|
|
13
11
|
| Approach | Problem |
|
|
14
12
|
| -------------------------------------- | -------------------------------------------------------- |
|
|
15
|
-
| Inline `addEventListener` everywhere |
|
|
13
|
+
| Inline `addEventListener` everywhere | O(N) boot cost, scattered, breaks on dynamic content |
|
|
16
14
|
| Framework event bindings (React, Vue…) | Requires full framework buy-in |
|
|
17
15
|
| Custom events + `dispatchEvent` | Verbose, no typed payload, no central subscription point |
|
|
18
|
-
| **`@alwatr/action`** | ✅
|
|
16
|
+
| **`@alwatr/action`** | ✅ O(1) boot, declarative, typed, zero-coupling |
|
|
19
17
|
|
|
20
18
|
---
|
|
21
19
|
|
|
22
|
-
##
|
|
20
|
+
## How It Works
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
bun add @alwatr/action
|
|
26
|
-
# or
|
|
27
|
-
npm i @alwatr/action
|
|
28
|
-
```
|
|
22
|
+
### Action Bus
|
|
29
23
|
|
|
30
|
-
|
|
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.
|
|
31
25
|
|
|
32
|
-
|
|
26
|
+
### Global Event Delegation
|
|
27
|
+
|
|
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.
|
|
33
29
|
|
|
34
30
|
```
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
User clicks a button
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
document.body capture listener (1 listener per event type)
|
|
35
|
+
│
|
|
36
|
+
└─ closest('[on-action^=click]') → finds element
|
|
37
|
+
parse attribute → 'click->add-to-cart:42'
|
|
38
|
+
run modifiers → none
|
|
39
|
+
resolve payload → '42'
|
|
40
|
+
internalChannel_.dispatch('add-to-cart', '42')
|
|
41
|
+
│
|
|
42
|
+
└─ Map.get('add-to-cart') → O(1) → invoke only matching handlers
|
|
37
43
|
```
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
| ----------- | ------------------------------------------------------------ | ----------------------------- |
|
|
41
|
-
| `eventType` | Any standard DOM event name | `click`, `input`, `submit` |
|
|
42
|
-
| `actionId` | Identifier your handler subscribes to | `open-drawer`, `search-query` |
|
|
43
|
-
| `:payload` | Optional literal string, or `$value` to read `element.value` | `:main`, `:$value` |
|
|
45
|
+
### Complexity
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
| Metric | Per-element listeners | Global delegation |
|
|
48
|
+
| --------------- | --------------------- | -------------------- |
|
|
49
|
+
| Boot time | O(N elements) | **O(1)** |
|
|
50
|
+
| Memory | O(N listeners) | **O(1)** |
|
|
51
|
+
| Dynamic content | Requires re-bootstrap | **Works out-of-box** |
|
|
52
|
+
| `once` modifier | Native option | Remove attribute |
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
| ------------ | -------------------------------------------------------------- |
|
|
49
|
-
| `:$value` | `element.value` (for `<input>`, `<select>`, `<textarea>`) |
|
|
50
|
-
| `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
|
|
54
|
+
### `once` modifier
|
|
51
55
|
|
|
52
|
-
|
|
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 reuse — if the element is re-rendered with the attribute, it fires again.
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
---
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bun add @alwatr/action
|
|
64
|
+
# or
|
|
65
|
+
npm i @alwatr/action
|
|
66
|
+
```
|
|
63
67
|
|
|
64
68
|
---
|
|
65
69
|
|
|
66
70
|
## Quick Start
|
|
67
71
|
|
|
68
|
-
### 1. Register
|
|
72
|
+
### 1. Register your action types
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
import {bootstrapDirectives} from '@alwatr/directive';
|
|
72
|
-
import {registerActionDirective} from '@alwatr/action';
|
|
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
|
-
|
|
75
|
-
|
|
76
|
+
```ts
|
|
77
|
+
// src/action-record.ts
|
|
78
|
+
declare module '@alwatr/action' {
|
|
79
|
+
interface ActionRecord {
|
|
80
|
+
'open-drawer': string;
|
|
81
|
+
'search-query': string;
|
|
82
|
+
'add-to-cart': {productId: number; qty: number};
|
|
83
|
+
'logout': void;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
### 2.
|
|
88
|
+
### 2. Bootstrap delegation
|
|
79
89
|
|
|
80
|
-
```
|
|
81
|
-
import {onAction} from '@alwatr/action';
|
|
90
|
+
```ts
|
|
91
|
+
import {setupActionDelegation, onAction} from '@alwatr/action';
|
|
92
|
+
import './action-record.js'; // ensure the declaration is loaded
|
|
82
93
|
|
|
83
|
-
|
|
84
|
-
onAction('open-drawer', (payload) => {
|
|
85
|
-
openDrawer(payload); // payload === 'main'
|
|
86
|
-
});
|
|
94
|
+
setupActionDelegation();
|
|
87
95
|
|
|
88
|
-
//
|
|
89
|
-
onAction('
|
|
90
|
-
|
|
96
|
+
// Payload types are inferred from ActionRecord — no generics needed.
|
|
97
|
+
onAction('open-drawer', (panel) => openDrawer(panel)); // panel: string
|
|
98
|
+
onAction('add-to-cart', (item) => {
|
|
99
|
+
cartService.add(item.productId, item.qty); // fully typed
|
|
91
100
|
});
|
|
92
101
|
```
|
|
93
102
|
|
|
@@ -97,9 +106,6 @@ onAction('search-query', (query) => {
|
|
|
97
106
|
<!-- Dispatches 'open-drawer' with payload 'main' on click -->
|
|
98
107
|
<button on-action="click->open-drawer:main">Open Drawer</button>
|
|
99
108
|
|
|
100
|
-
<!-- Dispatches 'open-drawer' with payload 'settings' on click -->
|
|
101
|
-
<button on-action="click->open-drawer:settings">Settings</button>
|
|
102
|
-
|
|
103
109
|
<!-- Dispatches 'search-query' with the input's live value -->
|
|
104
110
|
<input
|
|
105
111
|
type="search"
|
|
@@ -107,131 +113,183 @@ onAction('search-query', (query) => {
|
|
|
107
113
|
placeholder="Search…"
|
|
108
114
|
/>
|
|
109
115
|
|
|
110
|
-
<!-- Prevents default
|
|
116
|
+
<!-- Prevents default, validates, then dispatches all field values -->
|
|
111
117
|
<form
|
|
112
118
|
on-action="submit.prevent.validate->submit-form:$formdata"
|
|
113
119
|
novalidate
|
|
114
120
|
>
|
|
115
|
-
|
|
121
|
+
<input
|
|
122
|
+
name="username"
|
|
123
|
+
required
|
|
124
|
+
/>
|
|
125
|
+
<button type="submit">Save</button>
|
|
116
126
|
</form>
|
|
127
|
+
|
|
128
|
+
<!-- Fires only once — attribute is removed after first click -->
|
|
129
|
+
<button on-action="click.once->welcome-dismissed">Got it</button>
|
|
117
130
|
```
|
|
118
131
|
|
|
119
|
-
|
|
132
|
+
### 4. Programmatic dispatch
|
|
120
133
|
|
|
121
|
-
|
|
134
|
+
```ts
|
|
135
|
+
import {dispatchAction} from '@alwatr/action';
|
|
122
136
|
|
|
123
|
-
|
|
137
|
+
await uploadFile(file);
|
|
138
|
+
dispatchAction('upload-complete', fileId);
|
|
124
139
|
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
dispatchAction('navigate', '/dashboard');
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
## Attribute Syntax
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
on-action="eventType[.modifier…]->actionId[:payload]"
|
|
131
149
|
```
|
|
132
150
|
|
|
151
|
+
| Segment | Description | Example |
|
|
152
|
+
| ----------- | --------------------------------------------------------- | ----------------------------- |
|
|
153
|
+
| `eventType` | Any standard DOM event name | `click`, `input`, `submit` |
|
|
154
|
+
| `modifier` | Optional dot-chained tokens processed before dispatch | `.prevent`, `.validate` |
|
|
155
|
+
| `actionId` | Identifier your handler subscribes to | `open-drawer`, `search-query` |
|
|
156
|
+
| `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
|
|
157
|
+
|
|
158
|
+
### Built-in modifiers
|
|
159
|
+
|
|
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()` |
|
|
165
|
+
|
|
166
|
+
### Built-in payload resolvers
|
|
167
|
+
|
|
168
|
+
| Token | Resolves to |
|
|
169
|
+
| ------------ | -------------------------------------------------------------- |
|
|
170
|
+
| `:$value` | `element.value` (for `<input>`, `<select>`, `<textarea>`) |
|
|
171
|
+
| `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
|
|
172
|
+
|
|
133
173
|
---
|
|
134
174
|
|
|
135
175
|
## API Reference
|
|
136
176
|
|
|
137
|
-
### `
|
|
177
|
+
### `ActionRecord` (interface)
|
|
138
178
|
|
|
139
|
-
|
|
179
|
+
The global action type registry. Extend via declaration merging to register typed actions.
|
|
140
180
|
|
|
141
|
-
```
|
|
142
|
-
|
|
181
|
+
```ts
|
|
182
|
+
declare module '@alwatr/action' {
|
|
183
|
+
interface ActionRecord {
|
|
184
|
+
'open-drawer': string;
|
|
185
|
+
'logout': void;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
143
188
|
```
|
|
144
189
|
|
|
145
|
-
|
|
146
|
-
| ---------- | ----------------------- | ----------------------------------- |
|
|
147
|
-
| `actionId` | `string` | The action identifier to listen for |
|
|
148
|
-
| `handler` | `(payload?: T) => void` | Called with the resolved payload |
|
|
190
|
+
---
|
|
149
191
|
|
|
150
|
-
|
|
192
|
+
### `setupActionDelegation(eventTypes?)`
|
|
151
193
|
|
|
152
|
-
|
|
153
|
-
const sub = onAction('open-drawer', (payload) => {
|
|
154
|
-
/* … */
|
|
155
|
-
});
|
|
194
|
+
Registers global event delegation on `document.body`. Call once at bootstrap. Idempotent.
|
|
156
195
|
|
|
157
|
-
|
|
158
|
-
|
|
196
|
+
```ts
|
|
197
|
+
function setupActionDelegation(eventTypes?: readonly string[]): void;
|
|
159
198
|
```
|
|
160
199
|
|
|
161
|
-
|
|
200
|
+
Defaults to `DEFAULT_DELEGATED_EVENTS`: `['click', 'submit', 'input', 'change']`.
|
|
162
201
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
Dispatches a named action signal. Any `onAction` subscriber with a matching `actionId` will be invoked.
|
|
202
|
+
```ts
|
|
203
|
+
import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
function dispatchAction<T = string>(actionId: string, actionPayload?: T): void;
|
|
205
|
+
setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown']);
|
|
169
206
|
```
|
|
170
207
|
|
|
171
208
|
---
|
|
172
209
|
|
|
173
|
-
### `
|
|
174
|
-
|
|
175
|
-
Lazy registration for `ActionDirective`. Call once before `bootstrapDirectives()`.
|
|
176
|
-
If never called, the entire directive module is tree-shaken from the bundle.
|
|
210
|
+
### `teardownActionDelegation()`
|
|
177
211
|
|
|
178
|
-
|
|
179
|
-
import {registerActionDirective} from '@alwatr/action';
|
|
180
|
-
import {bootstrapDirectives} from '@alwatr/directive';
|
|
212
|
+
Removes all delegation listeners and clears the descriptor cache. Useful in tests or micro-frontend teardown.
|
|
181
213
|
|
|
182
|
-
|
|
183
|
-
|
|
214
|
+
```ts
|
|
215
|
+
function teardownActionDelegation(): void;
|
|
184
216
|
```
|
|
185
217
|
|
|
186
218
|
---
|
|
187
219
|
|
|
188
|
-
### `
|
|
220
|
+
### `onAction(actionId, handler)`
|
|
189
221
|
|
|
190
|
-
|
|
222
|
+
Subscribes to a named action. O(1) routing via `ChannelSignal`.
|
|
191
223
|
|
|
192
|
-
```
|
|
193
|
-
<
|
|
224
|
+
```ts
|
|
225
|
+
function onAction<K extends keyof ActionRecord>(
|
|
226
|
+
actionId: K,
|
|
227
|
+
handler: (payload: ActionRecord[K]) => void,
|
|
228
|
+
): SubscribeResult;
|
|
194
229
|
```
|
|
195
230
|
|
|
196
|
-
```
|
|
197
|
-
|
|
231
|
+
```ts
|
|
232
|
+
const sub = onAction('open-drawer', (panel) => openDrawer(panel));
|
|
233
|
+
sub.unsubscribe(); // prevent memory leaks
|
|
234
|
+
```
|
|
198
235
|
|
|
199
|
-
|
|
236
|
+
---
|
|
200
237
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
### `dispatchAction(actionId, payload?)`
|
|
239
|
+
|
|
240
|
+
Dispatches a named action. Payload type is enforced by `ActionRecord`.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// With payload
|
|
244
|
+
dispatchAction('open-drawer', 'settings');
|
|
245
|
+
|
|
246
|
+
// Void payload — no second argument
|
|
247
|
+
dispatchAction('logout');
|
|
204
248
|
```
|
|
205
249
|
|
|
206
250
|
---
|
|
207
251
|
|
|
208
252
|
### `registerModifier(name, handler)`
|
|
209
253
|
|
|
210
|
-
Registers a custom modifier
|
|
254
|
+
Registers a custom modifier. Return `false` to cancel the dispatch.
|
|
255
|
+
|
|
256
|
+
Handler signature: `(event: Event, element: HTMLElement) => boolean`
|
|
211
257
|
|
|
212
|
-
```
|
|
258
|
+
```ts
|
|
213
259
|
import {registerModifier} from '@alwatr/action';
|
|
214
260
|
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
// Arrow function — no `this` binding needed
|
|
262
|
+
registerModifier('not-disabled', (_event, element) => {
|
|
263
|
+
return !(element as HTMLButtonElement).disabled;
|
|
217
264
|
});
|
|
218
265
|
```
|
|
219
266
|
|
|
220
267
|
```html
|
|
221
|
-
<button
|
|
268
|
+
<button
|
|
269
|
+
on-action="click.not-disabled->select-item:$data-id"
|
|
270
|
+
data-id="42"
|
|
271
|
+
>
|
|
272
|
+
Select
|
|
273
|
+
</button>
|
|
222
274
|
```
|
|
223
275
|
|
|
224
276
|
---
|
|
225
277
|
|
|
226
278
|
### `registerPayloadResolver(name, resolver)`
|
|
227
279
|
|
|
228
|
-
Registers a custom payload resolver
|
|
280
|
+
Registers a custom payload resolver.
|
|
281
|
+
|
|
282
|
+
Handler signature: `(event: Event, element: HTMLElement) => unknown`
|
|
229
283
|
|
|
230
|
-
```
|
|
284
|
+
```ts
|
|
231
285
|
import {registerPayloadResolver} from '@alwatr/action';
|
|
232
286
|
|
|
233
|
-
registerPayloadResolver('$checked',
|
|
234
|
-
return (
|
|
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;
|
|
235
293
|
});
|
|
236
294
|
```
|
|
237
295
|
|
|
@@ -240,18 +298,16 @@ registerPayloadResolver('$checked', function () {
|
|
|
240
298
|
type="checkbox"
|
|
241
299
|
on-action="change->toggle-feature:$checked"
|
|
242
300
|
/>
|
|
301
|
+
<li
|
|
302
|
+
on-action="click->select-item:$data-id"
|
|
303
|
+
data-id="42"
|
|
304
|
+
>
|
|
305
|
+
Item
|
|
306
|
+
</li>
|
|
243
307
|
```
|
|
244
308
|
|
|
245
309
|
---
|
|
246
310
|
|
|
247
|
-
### `ActionDirective`
|
|
248
|
-
|
|
249
|
-
The directive class registered under the `on-action` attribute. Extends `Directive` from `@alwatr/directive`.
|
|
250
|
-
|
|
251
|
-
You rarely need to interact with this class directly — use `registerActionDirective()` to register it.
|
|
252
|
-
|
|
253
|
-
---
|
|
254
|
-
|
|
255
311
|
## Unidirectional Data Flow
|
|
256
312
|
|
|
257
313
|
```
|
|
@@ -259,13 +315,15 @@ You rarely need to interact with this class directly — use `registerActionDire
|
|
|
259
315
|
│ UI Layer │
|
|
260
316
|
│ <button on-action="click->add-to-cart:42">Add</button> │
|
|
261
317
|
└────────────────────────┬────────────────────────────────┘
|
|
262
|
-
│ DOM event
|
|
318
|
+
│ DOM event bubbles to body
|
|
263
319
|
▼
|
|
264
320
|
┌─────────────────────────────────────────────────────────┐
|
|
265
|
-
│
|
|
266
|
-
│
|
|
321
|
+
│ Action Layer (@alwatr/action) │
|
|
322
|
+
│ document.body capture listener (1 per event type) │
|
|
323
|
+
│ → closest('[on-action]') → parse → modifiers │
|
|
324
|
+
│ → internalChannel_.dispatch('add-to-cart', '42') [O(1)]│
|
|
267
325
|
└────────────────────────┬────────────────────────────────┘
|
|
268
|
-
│
|
|
326
|
+
│ O(1) routing via ChannelSignal
|
|
269
327
|
▼
|
|
270
328
|
┌─────────────────────────────────────────────────────────┐
|
|
271
329
|
│ Business Logic Layer │
|
|
@@ -275,45 +333,55 @@ You rarely need to interact with this class directly — use `registerActionDire
|
|
|
275
333
|
▼
|
|
276
334
|
┌─────────────────────────────────────────────────────────┐
|
|
277
335
|
│ State Layer (@alwatr/signal) │
|
|
278
|
-
│ cartSignal.
|
|
336
|
+
│ cartSignal.set(newCartState) │
|
|
279
337
|
└────────────────────────┬────────────────────────────────┘
|
|
280
338
|
│ state flows down to UI
|
|
281
339
|
▼
|
|
282
|
-
UI
|
|
340
|
+
UI re-renders
|
|
283
341
|
```
|
|
284
342
|
|
|
285
343
|
---
|
|
286
344
|
|
|
287
|
-
##
|
|
345
|
+
## Page Identity
|
|
288
346
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
│
|
|
294
|
-
└─ new ActionDirective(element, 'on-action')
|
|
295
|
-
│
|
|
296
|
-
└─ after one macrotask → init_()
|
|
297
|
-
│
|
|
298
|
-
├─ parse attributeValue with syntaxRegex
|
|
299
|
-
├─ if invalid → log accident, return
|
|
300
|
-
└─ addEventListener(eventType, dispatch_)
|
|
301
|
-
addDestroyHook(removeEventListener)
|
|
302
|
-
```
|
|
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.
|
|
303
351
|
|
|
304
352
|
---
|
|
305
353
|
|
|
306
|
-
##
|
|
354
|
+
## Migration from Previous Versions
|
|
307
355
|
|
|
308
|
-
|
|
356
|
+
### `ActionContext` removed
|
|
309
357
|
|
|
310
|
-
|
|
311
|
-
import {autoDestructDirectives} from '@alwatr/directive';
|
|
358
|
+
The `this` context in modifier and resolver handlers changed to explicit parameters:
|
|
312
359
|
|
|
313
|
-
|
|
314
|
-
|
|
360
|
+
**Before:**
|
|
361
|
+
|
|
362
|
+
```ts
|
|
363
|
+
registerModifier('not-disabled', function () {
|
|
364
|
+
return !(this.element as HTMLButtonElement).disabled;
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**After:**
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
registerModifier('not-disabled', (_event, element) => {
|
|
372
|
+
return !(element as HTMLButtonElement).disabled;
|
|
373
|
+
});
|
|
315
374
|
```
|
|
316
375
|
|
|
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`
|
|
382
|
+
|
|
383
|
+
`dispatchPageId` / `onPageReady` are no longer part of this package.
|
|
384
|
+
|
|
317
385
|
---
|
|
318
386
|
|
|
319
387
|
## Contributing
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file action-record.ts
|
|
3
|
+
*
|
|
4
|
+
* Global action type registry via TypeScript declaration merging.
|
|
5
|
+
*
|
|
6
|
+
* ## How it works
|
|
7
|
+
*
|
|
8
|
+
* `ActionRecord` is an open interface — any package in the monorepo (or any
|
|
9
|
+
* consumer application) can extend it with their own action names and payload
|
|
10
|
+
* types using declaration merging, without modifying this file:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // In your package: src/action-record.ts
|
|
14
|
+
* declare module '@alwatr/action' {
|
|
15
|
+
* interface ActionRecord {
|
|
16
|
+
* 'open-drawer': string;
|
|
17
|
+
* 'add-to-cart': {productId: number; qty: number};
|
|
18
|
+
* 'logout': void;
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Once declared, `onAction` and `dispatchAction` become fully type-safe for
|
|
24
|
+
* those action names — the compiler enforces the correct payload type at every
|
|
25
|
+
* call site and provides autocomplete for action identifiers.
|
|
26
|
+
*
|
|
27
|
+
* Only actions declared in `ActionRecord` are accepted. Passing an unknown
|
|
28
|
+
* action name is a **compile error** — there is no string fallback.
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Global registry mapping action identifiers to their payload types.
|
|
32
|
+
*
|
|
33
|
+
* Extend this interface via declaration merging to register your application's
|
|
34
|
+
* actions and gain full type safety in `onAction` and `dispatchAction`.
|
|
35
|
+
*
|
|
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.
|
|
39
|
+
*
|
|
40
|
+
* @example — registering actions in a feature package
|
|
41
|
+
* ```ts
|
|
42
|
+
* // pkg/my-feature/src/action-record.ts
|
|
43
|
+
* declare module '@alwatr/action' {
|
|
44
|
+
* interface ActionRecord {
|
|
45
|
+
* 'open-drawer': string;
|
|
46
|
+
* 'add-to-cart': {productId: number; qty: number};
|
|
47
|
+
* 'logout': void;
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export interface ActionRecord {
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=action-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|