@alwatr/action 9.12.0 → 9.13.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 +252 -137
- package/dist/action-record.d.ts +66 -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 +14 -29
- package/dist/lib.d.ts.map +1 -1
- package/dist/main.d.ts +49 -9
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +8 -8
- package/dist/method.d.ts +67 -53
- package/dist/method.d.ts.map +1 -1
- package/dist/page-ready.d.ts +3 -0
- package/dist/page-ready.d.ts.map +1 -0
- package/dist/registry.d.ts +12 -13
- package/dist/registry.d.ts.map +1 -1
- package/package.json +3 -5
- package/src/action-record.ts +66 -0
- package/src/delegate.ts +315 -0
- package/src/lib.ts +15 -31
- package/src/main.ts +49 -9
- package/src/method.ts +80 -61
- package/src/page-ready.ts +31 -0
- package/src/registry.ts +22 -40
- 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,95 @@ 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
|
+
### Global Event Delegation
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
## Attribute Syntax
|
|
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`.
|
|
33
25
|
|
|
34
26
|
```
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
User clicks a button
|
|
28
|
+
│
|
|
29
|
+
▼
|
|
30
|
+
document.body capture listener (1 listener total)
|
|
31
|
+
│
|
|
32
|
+
└─ closest('[on-action]') → finds element
|
|
33
|
+
parse attribute → 'click->add-to-cart:42'
|
|
34
|
+
run modifiers → none
|
|
35
|
+
resolve payload → '42'
|
|
36
|
+
dispatchAction('add-to-cart', '42')
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
ChannelSignal.dispatch('add-to-cart', '42') [O(1)]
|
|
40
|
+
│
|
|
41
|
+
└─ Map.get('add-to-cart') → invoke only matching handlers
|
|
37
42
|
```
|
|
38
43
|
|
|
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` |
|
|
44
|
+
### Complexity
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
| Metric | Per-element listeners | Global delegation |
|
|
47
|
+
| --------------- | --------------------- | -------------------- |
|
|
48
|
+
| Boot time | O(N elements) | **O(1)** |
|
|
49
|
+
| Memory | O(N listeners) | **O(1)** |
|
|
50
|
+
| Dynamic content | Requires re-bootstrap | **Works out-of-box** |
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
| `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
|
|
52
|
+
### Action Bus
|
|
53
|
+
|
|
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.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
---
|
|
53
57
|
|
|
54
|
-
|
|
58
|
+
## Installation
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
| `.passive` | Marks the listener as passive (cannot be combined with `.prevent`) |
|
|
62
|
-
| `.validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
|
|
60
|
+
```bash
|
|
61
|
+
bun add @alwatr/action
|
|
62
|
+
# or
|
|
63
|
+
npm i @alwatr/action
|
|
64
|
+
```
|
|
63
65
|
|
|
64
66
|
---
|
|
65
67
|
|
|
66
68
|
## Quick Start
|
|
67
69
|
|
|
68
|
-
### 1. Register
|
|
70
|
+
### 1. Register your action types
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
import {bootstrapDirectives} from '@alwatr/directive';
|
|
72
|
-
import {registerActionDirective} from '@alwatr/action';
|
|
72
|
+
Create a declaration file in your package to extend `ActionRecord`. This gives you full type safety and IDE autocomplete across the entire app:
|
|
73
73
|
|
|
74
|
-
|
|
75
|
-
|
|
74
|
+
```ts
|
|
75
|
+
// src/action-record.ts
|
|
76
|
+
declare module '@alwatr/action' {
|
|
77
|
+
interface ActionRecord {
|
|
78
|
+
'open-drawer': string;
|
|
79
|
+
'search-query': string;
|
|
80
|
+
'add-to-cart': {productId: number; qty: number};
|
|
81
|
+
'logout': void;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
76
84
|
```
|
|
77
85
|
|
|
78
|
-
|
|
86
|
+
Passing an action name not declared in `ActionRecord` is a **compile error** — there is no string fallback.
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
import {onAction} from '@alwatr/action';
|
|
88
|
+
### 2. Bootstrap delegation
|
|
82
89
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
});
|
|
90
|
+
```ts
|
|
91
|
+
import {setupActionDelegation, onAction} from '@alwatr/action';
|
|
92
|
+
import './action-record.js'; // ensure the declaration is loaded
|
|
87
93
|
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
94
|
+
// One call — the entire page is covered, including future dynamic content.
|
|
95
|
+
setupActionDelegation();
|
|
96
|
+
|
|
97
|
+
// Payload types are inferred automatically from ActionRecord — no generics needed.
|
|
98
|
+
onAction('open-drawer', (panel) => openDrawer(panel)); // panel: string
|
|
99
|
+
onAction('search-query', (query) => performSearch(query)); // query: string
|
|
100
|
+
onAction('add-to-cart', (item) => {
|
|
101
|
+
cartService.add(item.productId, item.qty); // fully typed, no `!`
|
|
91
102
|
});
|
|
92
103
|
```
|
|
93
104
|
|
|
@@ -97,9 +108,6 @@ onAction('search-query', (query) => {
|
|
|
97
108
|
<!-- Dispatches 'open-drawer' with payload 'main' on click -->
|
|
98
109
|
<button on-action="click->open-drawer:main">Open Drawer</button>
|
|
99
110
|
|
|
100
|
-
<!-- Dispatches 'open-drawer' with payload 'settings' on click -->
|
|
101
|
-
<button on-action="click->open-drawer:settings">Settings</button>
|
|
102
|
-
|
|
103
111
|
<!-- Dispatches 'search-query' with the input's live value -->
|
|
104
112
|
<input
|
|
105
113
|
type="search"
|
|
@@ -107,99 +115,175 @@ onAction('search-query', (query) => {
|
|
|
107
115
|
placeholder="Search…"
|
|
108
116
|
/>
|
|
109
117
|
|
|
110
|
-
<!-- Prevents default
|
|
118
|
+
<!-- Prevents default, validates, then dispatches all field values -->
|
|
111
119
|
<form
|
|
112
120
|
on-action="submit.prevent.validate->submit-form:$formdata"
|
|
113
121
|
novalidate
|
|
114
122
|
>
|
|
115
|
-
|
|
123
|
+
<input
|
|
124
|
+
name="username"
|
|
125
|
+
required
|
|
126
|
+
/>
|
|
127
|
+
<button type="submit">Save</button>
|
|
116
128
|
</form>
|
|
117
129
|
```
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## Programmatic Dispatch
|
|
131
|
+
### 3. Programmatic dispatch
|
|
122
132
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
133
|
+
```ts
|
|
126
134
|
import {dispatchAction} from '@alwatr/action';
|
|
127
135
|
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
// Trigger actions from code — after async ops, from service layers, etc.
|
|
137
|
+
await uploadFile(file);
|
|
138
|
+
dispatchAction('upload-complete', fileId);
|
|
139
|
+
|
|
140
|
+
dispatchAction('navigate', '/dashboard');
|
|
130
141
|
dispatchAction<{code: number}>('show-error', {code: 404});
|
|
131
142
|
```
|
|
132
143
|
|
|
133
144
|
---
|
|
134
145
|
|
|
146
|
+
## Attribute Syntax
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
on-action="eventType[.modifier…]->actionId[:payload]"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
| Segment | Description | Example |
|
|
153
|
+
| ----------- | --------------------------------------------------------- | ----------------------------- |
|
|
154
|
+
| `eventType` | Any standard DOM event name | `click`, `input`, `submit` |
|
|
155
|
+
| `modifier` | Optional dot-chained tokens processed before dispatch | `.prevent`, `.validate` |
|
|
156
|
+
| `actionId` | Identifier your handler subscribes to | `open-drawer`, `search-query` |
|
|
157
|
+
| `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
|
|
158
|
+
|
|
159
|
+
### Built-in modifiers
|
|
160
|
+
|
|
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.
|
|
170
|
+
|
|
171
|
+
### Built-in payload resolvers
|
|
172
|
+
|
|
173
|
+
| Token | Resolves to |
|
|
174
|
+
| ------------ | -------------------------------------------------------------- |
|
|
175
|
+
| `:$value` | `element.value` (for `<input>`, `<select>`, `<textarea>`) |
|
|
176
|
+
| `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
135
180
|
## API Reference
|
|
136
181
|
|
|
137
|
-
### `
|
|
182
|
+
### `ActionRecord` (interface)
|
|
138
183
|
|
|
139
|
-
|
|
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`.
|
|
140
185
|
|
|
141
|
-
```
|
|
142
|
-
|
|
186
|
+
```ts
|
|
187
|
+
// src/action-record.ts
|
|
188
|
+
declare module '@alwatr/action' {
|
|
189
|
+
interface ActionRecord {
|
|
190
|
+
'open-drawer': string;
|
|
191
|
+
'add-to-cart': {productId: number; qty: number};
|
|
192
|
+
'logout': void;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
143
195
|
```
|
|
144
196
|
|
|
145
|
-
|
|
146
|
-
| ---------- | ----------------------- | ----------------------------------- |
|
|
147
|
-
| `actionId` | `string` | The action identifier to listen for |
|
|
148
|
-
| `handler` | `(payload?: T) => void` | Called with the resolved payload |
|
|
197
|
+
Once declared:
|
|
149
198
|
|
|
150
|
-
|
|
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**
|
|
151
202
|
|
|
152
|
-
|
|
153
|
-
const sub = onAction('open-drawer', (payload) => {
|
|
154
|
-
/* … */
|
|
155
|
-
});
|
|
203
|
+
---
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
|
|
205
|
+
### `setupActionDelegation(eventTypes?)`
|
|
206
|
+
|
|
207
|
+
Registers global event delegation on `document.body`. Call once at bootstrap.
|
|
208
|
+
Subsequent calls with the same event types are no-ops (idempotent).
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
function setupActionDelegation(eventTypes?: readonly string[]): void;
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Defaults to `DEFAULT_DELEGATED_EVENTS`: `['click', 'submit', 'input', 'change']`.
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';
|
|
218
|
+
|
|
219
|
+
// Default events
|
|
220
|
+
setupActionDelegation();
|
|
221
|
+
|
|
222
|
+
// Add extra event types
|
|
223
|
+
setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);
|
|
159
224
|
```
|
|
160
225
|
|
|
161
226
|
---
|
|
162
227
|
|
|
163
|
-
### `
|
|
228
|
+
### `teardownActionDelegation()`
|
|
164
229
|
|
|
165
|
-
|
|
230
|
+
Removes all delegation listeners. Useful in tests or micro-frontend teardown.
|
|
166
231
|
|
|
167
|
-
```
|
|
168
|
-
function
|
|
232
|
+
```ts
|
|
233
|
+
function teardownActionDelegation(): void;
|
|
169
234
|
```
|
|
170
235
|
|
|
171
236
|
---
|
|
172
237
|
|
|
173
|
-
### `
|
|
238
|
+
### `onAction(actionId, handler)`
|
|
174
239
|
|
|
175
|
-
|
|
176
|
-
If never called, the entire directive module is tree-shaken from the bundle.
|
|
240
|
+
Subscribes to a named action. Uses `ChannelSignal.on()` for O(1) routing.
|
|
177
241
|
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
```ts
|
|
243
|
+
function onAction<T = string>(actionId: string, handler: (payload?: T) => void): SubscribeResult;
|
|
244
|
+
```
|
|
181
245
|
|
|
182
|
-
|
|
183
|
-
|
|
246
|
+
```ts
|
|
247
|
+
const sub = onAction('open-drawer', (panel) => openDrawer(panel));
|
|
248
|
+
|
|
249
|
+
// Unsubscribe when no longer needed (prevents memory leaks)
|
|
250
|
+
sub.unsubscribe();
|
|
184
251
|
```
|
|
185
252
|
|
|
186
253
|
---
|
|
187
254
|
|
|
188
|
-
### `
|
|
255
|
+
### `dispatchAction(actionId, payload?)`
|
|
256
|
+
|
|
257
|
+
Dispatches a named action to all matching `onAction` subscribers.
|
|
189
258
|
|
|
190
|
-
|
|
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.
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
function dispatchPageId(element?: HTMLElement): void;
|
|
272
|
+
```
|
|
191
273
|
|
|
192
274
|
```html
|
|
193
|
-
<body page-id="home"
|
|
275
|
+
<body page-id="home">
|
|
276
|
+
…
|
|
277
|
+
</body>
|
|
194
278
|
```
|
|
195
279
|
|
|
196
|
-
```
|
|
197
|
-
import {
|
|
280
|
+
```ts
|
|
281
|
+
import {dispatchPageId, onAction} from '@alwatr/action';
|
|
198
282
|
|
|
199
|
-
|
|
283
|
+
dispatchPageId(); // → dispatchAction('page-ready', 'home')
|
|
200
284
|
|
|
201
285
|
onAction('page-ready', (pageId) => {
|
|
202
|
-
console.log('
|
|
286
|
+
console.log('navigated to:', pageId); // 'home'
|
|
203
287
|
});
|
|
204
288
|
```
|
|
205
289
|
|
|
@@ -207,9 +291,10 @@ onAction('page-ready', (pageId) => {
|
|
|
207
291
|
|
|
208
292
|
### `registerModifier(name, handler)`
|
|
209
293
|
|
|
210
|
-
Registers a custom modifier
|
|
294
|
+
Registers a custom modifier. Return `false` to cancel the dispatch.
|
|
295
|
+
Works with both delegation and programmatic dispatch.
|
|
211
296
|
|
|
212
|
-
```
|
|
297
|
+
```ts
|
|
213
298
|
import {registerModifier} from '@alwatr/action';
|
|
214
299
|
|
|
215
300
|
registerModifier('confirm', function () {
|
|
@@ -221,17 +306,26 @@ registerModifier('confirm', function () {
|
|
|
221
306
|
<button on-action="click.confirm->delete-item:42">Delete</button>
|
|
222
307
|
```
|
|
223
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
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
224
317
|
---
|
|
225
318
|
|
|
226
319
|
### `registerPayloadResolver(name, resolver)`
|
|
227
320
|
|
|
228
|
-
Registers a custom payload resolver
|
|
321
|
+
Registers a custom payload resolver. The return value becomes the action payload.
|
|
322
|
+
Works with both delegation and programmatic dispatch.
|
|
229
323
|
|
|
230
|
-
```
|
|
324
|
+
```ts
|
|
231
325
|
import {registerPayloadResolver} from '@alwatr/action';
|
|
232
326
|
|
|
233
327
|
registerPayloadResolver('$checked', function () {
|
|
234
|
-
return (this.
|
|
328
|
+
return (this.element as HTMLInputElement).checked;
|
|
235
329
|
});
|
|
236
330
|
```
|
|
237
331
|
|
|
@@ -244,14 +338,6 @@ registerPayloadResolver('$checked', function () {
|
|
|
244
338
|
|
|
245
339
|
---
|
|
246
340
|
|
|
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
341
|
## Unidirectional Data Flow
|
|
256
342
|
|
|
257
343
|
```
|
|
@@ -259,13 +345,15 @@ You rarely need to interact with this class directly — use `registerActionDire
|
|
|
259
345
|
│ UI Layer │
|
|
260
346
|
│ <button on-action="click->add-to-cart:42">Add</button> │
|
|
261
347
|
└────────────────────────┬────────────────────────────────┘
|
|
262
|
-
│ DOM event
|
|
348
|
+
│ DOM event bubbles to body
|
|
263
349
|
▼
|
|
264
350
|
┌─────────────────────────────────────────────────────────┐
|
|
265
|
-
│
|
|
266
|
-
│
|
|
351
|
+
│ 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)] │
|
|
267
355
|
└────────────────────────┬────────────────────────────────┘
|
|
268
|
-
│ action signal
|
|
356
|
+
│ action signal (O(1) routing)
|
|
269
357
|
▼
|
|
270
358
|
┌─────────────────────────────────────────────────────────┐
|
|
271
359
|
│ Business Logic Layer │
|
|
@@ -275,45 +363,72 @@ You rarely need to interact with this class directly — use `registerActionDire
|
|
|
275
363
|
▼
|
|
276
364
|
┌─────────────────────────────────────────────────────────┐
|
|
277
365
|
│ State Layer (@alwatr/signal) │
|
|
278
|
-
│ cartSignal.
|
|
366
|
+
│ cartSignal.set(newCartState) │
|
|
279
367
|
└────────────────────────┬────────────────────────────────┘
|
|
280
368
|
│ state flows down to UI
|
|
281
369
|
▼
|
|
282
|
-
UI
|
|
370
|
+
UI re-renders
|
|
283
371
|
```
|
|
284
372
|
|
|
285
373
|
---
|
|
286
374
|
|
|
287
|
-
##
|
|
375
|
+
## Migration from Previous Versions
|
|
376
|
+
|
|
377
|
+
### `registerActionDirective` / `registerPageIdDirective` removed
|
|
378
|
+
|
|
379
|
+
The directive-based approach has been replaced by global delegation.
|
|
380
|
+
|
|
381
|
+
**Before:**
|
|
288
382
|
|
|
383
|
+
```ts
|
|
384
|
+
import {registerActionDirective, registerPageIdDirective} from '@alwatr/action';
|
|
385
|
+
import {bootstrapDirectives} from '@alwatr/directive';
|
|
386
|
+
|
|
387
|
+
registerActionDirective();
|
|
388
|
+
registerPageIdDirective();
|
|
389
|
+
bootstrapDirectives();
|
|
289
390
|
```
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
├─ parse attributeValue with syntaxRegex
|
|
299
|
-
├─ if invalid → log accident, return
|
|
300
|
-
└─ addEventListener(eventType, dispatch_)
|
|
301
|
-
addDestroyHook(removeEventListener)
|
|
391
|
+
|
|
392
|
+
**After:**
|
|
393
|
+
|
|
394
|
+
```ts
|
|
395
|
+
import {setupActionDelegation, dispatchPageId} from '@alwatr/action';
|
|
396
|
+
|
|
397
|
+
setupActionDelegation();
|
|
398
|
+
dispatchPageId();
|
|
302
399
|
```
|
|
303
400
|
|
|
304
|
-
|
|
401
|
+
### `ActionDirective` / `PageIdDirective` removed
|
|
402
|
+
|
|
403
|
+
These classes are no longer exported. Use `setupActionDelegation()` and
|
|
404
|
+
`dispatchPageId()` instead.
|
|
305
405
|
|
|
306
|
-
|
|
406
|
+
### `ModifierHandler` / `PayloadResolver` context changed
|
|
307
407
|
|
|
308
|
-
|
|
408
|
+
The `this` context in custom modifier and resolver functions changed from
|
|
409
|
+
`ActionDirective` to `ActionContext`:
|
|
309
410
|
|
|
310
|
-
|
|
311
|
-
import {autoDestructDirectives} from '@alwatr/directive';
|
|
411
|
+
**Before:**
|
|
312
412
|
|
|
313
|
-
|
|
314
|
-
|
|
413
|
+
```ts
|
|
414
|
+
registerModifier('not-disabled', function () {
|
|
415
|
+
return !(this.element_ as HTMLButtonElement).disabled; // this.element_
|
|
416
|
+
});
|
|
315
417
|
```
|
|
316
418
|
|
|
419
|
+
**After:**
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
registerModifier('not-disabled', function () {
|
|
423
|
+
return !(this.element as HTMLButtonElement).disabled; // this.element (no underscore)
|
|
424
|
+
});
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### `ActionSignalPayload` removed
|
|
428
|
+
|
|
429
|
+
This type was an implementation detail of the old `EventSignal`-based bus and
|
|
430
|
+
is no longer needed. Use `onAction` and `dispatchAction` directly.
|
|
431
|
+
|
|
317
432
|
---
|
|
318
433
|
|
|
319
434
|
## Contributing
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
* 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.
|
|
38
|
+
*
|
|
39
|
+
* @example — registering actions in a feature package
|
|
40
|
+
* ```ts
|
|
41
|
+
* // pkg/my-feature/src/action-record.ts
|
|
42
|
+
* declare module '@alwatr/action' {
|
|
43
|
+
* interface ActionRecord {
|
|
44
|
+
* 'open-drawer': string;
|
|
45
|
+
* 'add-to-cart': {productId: number; qty: number};
|
|
46
|
+
* 'logout': void;
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
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
|
+
}
|
|
66
|
+
//# 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;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;OAWG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB"}
|