@alwatr/action 9.14.0 → 9.17.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 +335 -95
- package/dist/action-record.d.ts +4 -4
- package/dist/action.d.ts +93 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/delegate.d.ts +33 -10
- package/dist/delegate.d.ts.map +1 -1
- package/dist/lib.d.ts +8 -6
- package/dist/lib.d.ts.map +1 -1
- package/dist/main.d.ts +47 -8
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +6 -6
- package/dist/method.d.ts +74 -57
- package/dist/method.d.ts.map +1 -1
- package/dist/registry.d.ts +28 -16
- package/dist/registry.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/action-record.ts +4 -4
- package/src/action.ts +97 -0
- package/src/delegate.ts +131 -86
- package/src/lib.ts +9 -6
- package/src/main.ts +47 -8
- package/src/method.ts +84 -66
- package/src/registry.ts +55 -24
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Declarative DOM action-dispatch — the Action layer for Unidirectional Data Flow.**
|
|
4
4
|
|
|
5
|
-
`@alwatr/action` bridges HTML `on
|
|
5
|
+
`@alwatr/action` bridges HTML `on-<eventType>` 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.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
|
|
24
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
25
|
|
|
26
|
+
Every message on the bus is a full **`Action<K>`** object (Alwatr Flux Standard Action — AFSA) rather than a bare payload. This means every handler receives `type`, `payload`, `context`, and `meta` in one unified structure.
|
|
27
|
+
|
|
26
28
|
### Global Event Delegation
|
|
27
29
|
|
|
28
|
-
A single capture-phase listener on `document.body` handles all `on
|
|
30
|
+
A single capture-phase listener on `document.body` handles all `on-<eventType>` elements. When an event fires, the handler walks up from `event.target` using `closest('[on-click]')` (or the matching attribute), resolves the nearest `[action-context]` ancestor, parses the attribute value, runs modifiers, resolves the payload, and dispatches the full `Action` object.
|
|
29
31
|
|
|
30
32
|
```
|
|
31
33
|
User clicks a button
|
|
@@ -33,13 +35,18 @@ User clicks a button
|
|
|
33
35
|
▼
|
|
34
36
|
document.body capture listener (1 listener per event type)
|
|
35
37
|
│
|
|
36
|
-
└─ closest('[on-
|
|
37
|
-
|
|
38
|
+
└─ closest('[on-click]') → finds element
|
|
39
|
+
closest('[action-context]') → resolves context (e.g. 'product-list')
|
|
40
|
+
parse attribute → 'add_to_cart:42'
|
|
38
41
|
run modifiers → none
|
|
39
42
|
resolve payload → '42'
|
|
40
|
-
internalChannel_.dispatch('
|
|
43
|
+
internalChannel_.dispatch('add_to_cart', {
|
|
44
|
+
type: 'add_to_cart',
|
|
45
|
+
payload: '42',
|
|
46
|
+
context: 'product-list',
|
|
47
|
+
})
|
|
41
48
|
│
|
|
42
|
-
└─ Map.get('
|
|
49
|
+
└─ Map.get('add_to_cart') → O(1) → invoke only matching handlers
|
|
43
50
|
```
|
|
44
51
|
|
|
45
52
|
### Complexity
|
|
@@ -53,7 +60,7 @@ document.body capture listener (1 listener per event type)
|
|
|
53
60
|
|
|
54
61
|
### `once` modifier
|
|
55
62
|
|
|
56
|
-
In delegation mode, `once` is implemented by removing the `on
|
|
63
|
+
In delegation mode, `once` is implemented by removing the `on-<eventType>` 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.
|
|
57
64
|
|
|
58
65
|
---
|
|
59
66
|
|
|
@@ -77,10 +84,10 @@ Extend `ActionRecord` via declaration merging. This gives you full type safety a
|
|
|
77
84
|
// src/action-record.ts
|
|
78
85
|
declare module '@alwatr/action' {
|
|
79
86
|
interface ActionRecord {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
open_drawer: string;
|
|
88
|
+
search_query: string;
|
|
89
|
+
add_to_cart: {productId: number; qty: number};
|
|
90
|
+
logout: void;
|
|
84
91
|
}
|
|
85
92
|
}
|
|
86
93
|
```
|
|
@@ -93,29 +100,33 @@ import './action-record.js'; // ensure the declaration is loaded
|
|
|
93
100
|
|
|
94
101
|
setupActionDelegation();
|
|
95
102
|
|
|
96
|
-
//
|
|
97
|
-
onAction('
|
|
98
|
-
onAction('
|
|
99
|
-
cartService.add(
|
|
103
|
+
// The handler receives the full Action<K> object — payload, context, and meta in one place.
|
|
104
|
+
onAction('open_drawer', (action) => openDrawer(action.payload)); // action.payload: string
|
|
105
|
+
onAction('add_to_cart', (action) => {
|
|
106
|
+
cartService.add(action.payload.productId, action.payload.qty); // fully typed
|
|
107
|
+
console.log(action.context); // e.g. 'product-list' — from nearest [action-context] ancestor
|
|
100
108
|
});
|
|
101
109
|
```
|
|
102
110
|
|
|
103
111
|
### 3. Add attributes to HTML
|
|
104
112
|
|
|
105
113
|
```html
|
|
106
|
-
<!-- Dispatches '
|
|
107
|
-
<button on-
|
|
114
|
+
<!-- Dispatches 'close_drawer' on click — no payload -->
|
|
115
|
+
<button on-click="close_drawer">Close</button>
|
|
116
|
+
|
|
117
|
+
<!-- Dispatches 'open_drawer' with payload 'main' on click -->
|
|
118
|
+
<button on-click="open_drawer:main">Open Drawer</button>
|
|
108
119
|
|
|
109
|
-
<!-- Dispatches '
|
|
120
|
+
<!-- Dispatches 'search_query' with the input's live value -->
|
|
110
121
|
<input
|
|
111
122
|
type="search"
|
|
112
|
-
on-
|
|
123
|
+
on-input="search_query:$value"
|
|
113
124
|
placeholder="Search…"
|
|
114
125
|
/>
|
|
115
126
|
|
|
116
127
|
<!-- Prevents default, validates, then dispatches all field values -->
|
|
117
128
|
<form
|
|
118
|
-
on-
|
|
129
|
+
on-submit="submit_form:$formdata; prevent,validate"
|
|
119
130
|
novalidate
|
|
120
131
|
>
|
|
121
132
|
<input
|
|
@@ -126,18 +137,57 @@ onAction('add-to-cart', (item) => {
|
|
|
126
137
|
</form>
|
|
127
138
|
|
|
128
139
|
<!-- Fires only once — attribute is removed after first click -->
|
|
129
|
-
<button on-
|
|
140
|
+
<button on-click="welcome_dismissed; once">Got it</button>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 4. Context scoping with `action-context`
|
|
144
|
+
|
|
145
|
+
Wrap a group of elements in an `[action-context]` container to scope their actions. The delegation handler automatically resolves the nearest ancestor and attaches its value to `action.context`. This lets the same action type serve multiple independent UI regions without creating separate action names.
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<!-- Two sliders on the same page, both dispatching 'slider:change' -->
|
|
149
|
+
<section action-context="volume">
|
|
150
|
+
<input
|
|
151
|
+
type="range"
|
|
152
|
+
on-input="slider:change:$value"
|
|
153
|
+
/>
|
|
154
|
+
</section>
|
|
155
|
+
|
|
156
|
+
<section action-context="brightness">
|
|
157
|
+
<input
|
|
158
|
+
type="range"
|
|
159
|
+
on-input="slider:change:$value"
|
|
160
|
+
/>
|
|
161
|
+
</section>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
onAction('slider:change', (action) => {
|
|
166
|
+
if (action.context === 'volume') audioService.setVolume(Number(action.payload));
|
|
167
|
+
if (action.context === 'brightness') displayService.setBrightness(Number(action.payload));
|
|
168
|
+
});
|
|
130
169
|
```
|
|
131
170
|
|
|
132
|
-
|
|
171
|
+
Context is `undefined` when no `[action-context]` ancestor exists — programmatic dispatches also have no context by default.
|
|
172
|
+
|
|
173
|
+
### 5. Programmatic dispatch
|
|
133
174
|
|
|
134
175
|
```ts
|
|
135
176
|
import {dispatchAction} from '@alwatr/action';
|
|
136
177
|
|
|
178
|
+
// dispatchAction takes a full Action object
|
|
137
179
|
await uploadFile(file);
|
|
138
|
-
dispatchAction('
|
|
180
|
+
dispatchAction({type: 'upload_complete', payload: fileId});
|
|
139
181
|
|
|
140
|
-
dispatchAction('navigate', '/dashboard');
|
|
182
|
+
dispatchAction({type: 'navigate', payload: '/dashboard'});
|
|
183
|
+
|
|
184
|
+
// With explicit context and meta
|
|
185
|
+
dispatchAction({
|
|
186
|
+
type: 'slider:change',
|
|
187
|
+
payload: 75,
|
|
188
|
+
context: 'volume',
|
|
189
|
+
meta: {source: 'keyboard'},
|
|
190
|
+
});
|
|
141
191
|
```
|
|
142
192
|
|
|
143
193
|
---
|
|
@@ -145,23 +195,23 @@ dispatchAction('navigate', '/dashboard');
|
|
|
145
195
|
## Attribute Syntax
|
|
146
196
|
|
|
147
197
|
```
|
|
148
|
-
on
|
|
198
|
+
on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"
|
|
149
199
|
```
|
|
150
200
|
|
|
151
|
-
| Segment
|
|
152
|
-
|
|
|
153
|
-
| `eventType`
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
|
|
|
201
|
+
| Segment | Description | Example |
|
|
202
|
+
| ------------- | ----------------------------------------------------------- | ----------------------------- |
|
|
203
|
+
| `eventType` | Any standard DOM event name — encoded in the attribute name | `on-click`, `on-submit` |
|
|
204
|
+
| `actionId` | Identifier your handler subscribes to | `open_drawer`, `search_query` |
|
|
205
|
+
| `:payload` | Optional literal string, or a `$`-prefixed resolver token | `:main`, `:$value` |
|
|
206
|
+
| `; modifiers` | Optional comma-separated modifier list after a semicolon | `; prevent,validate` |
|
|
157
207
|
|
|
158
208
|
### Built-in modifiers
|
|
159
209
|
|
|
160
|
-
| Modifier
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
210
|
+
| Modifier | Behavior |
|
|
211
|
+
| ---------- | ------------------------------------------------------------------------------------- |
|
|
212
|
+
| `prevent` | Calls `event.preventDefault()` |
|
|
213
|
+
| `once` | Removes the `on-<eventType>` attribute after first fire — action dispatches only once |
|
|
214
|
+
| `validate` | Cancels dispatch if the nearest `<form>` fails `checkValidity()` |
|
|
165
215
|
|
|
166
216
|
### Built-in payload resolvers
|
|
167
217
|
|
|
@@ -169,11 +219,81 @@ on-action="eventType[.modifier…]->actionId[:payload]"
|
|
|
169
219
|
| ------------ | -------------------------------------------------------------- |
|
|
170
220
|
| `:$value` | `element.value` (for `<input>`, `<select>`, `<textarea>`) |
|
|
171
221
|
| `:$formdata` | `Object.fromEntries(new FormData(form))` from nearest `<form>` |
|
|
222
|
+
| `:$checked` | `(element as HTMLInputElement).checked` for checkboxes/radios |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## The Action Object (AFSA)
|
|
227
|
+
|
|
228
|
+
Every action flowing through the bus — whether triggered from HTML attributes or dispatched programmatically — is a single **`Action<K>`** object:
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
interface Action<K extends keyof ActionRecord> {
|
|
232
|
+
/** Action identifier — must be a key of ActionRecord. */
|
|
233
|
+
type: K;
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* DOM context from the nearest [action-context] ancestor.
|
|
237
|
+
* undefined for programmatic dispatches or when no ancestor exists.
|
|
238
|
+
*/
|
|
239
|
+
context?: string;
|
|
240
|
+
|
|
241
|
+
/** Business payload — type is inferred from ActionRecord[K]. */
|
|
242
|
+
payload: ActionRecord[K];
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Open-ended metadata bag for cross-cutting concerns.
|
|
246
|
+
* Modifiers may write to this before the action reaches subscribers.
|
|
247
|
+
*/
|
|
248
|
+
meta?: Record<string, unknown>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Modifiers in the delegation pipeline receive the mutable `action` object and can enrich `meta` before the action reaches subscribers:
|
|
253
|
+
|
|
254
|
+
```ts
|
|
255
|
+
import {registerModifier} from '@alwatr/action';
|
|
256
|
+
|
|
257
|
+
// A modifier that stamps a trace ID into meta
|
|
258
|
+
registerModifier('trace', (_event, _element, action) => {
|
|
259
|
+
action.meta ??= {};
|
|
260
|
+
action.meta['traceId'] = crypto.randomUUID();
|
|
261
|
+
return true;
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
```html
|
|
266
|
+
<button on-click="submit_order:42; trace">Place Order</button>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
onAction('submit_order', (action) => {
|
|
271
|
+
console.log(action.meta?.['traceId']); // e.g. 'a1b2-c3d4-…'
|
|
272
|
+
});
|
|
273
|
+
```
|
|
172
274
|
|
|
173
275
|
---
|
|
174
276
|
|
|
175
277
|
## API Reference
|
|
176
278
|
|
|
279
|
+
### `Action<K>` (interface)
|
|
280
|
+
|
|
281
|
+
The Alwatr Flux Standard Action object. Every dispatch and every handler callback uses this structure.
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
import type {Action} from '@alwatr/action';
|
|
285
|
+
|
|
286
|
+
// Reading fields in a handler
|
|
287
|
+
onAction('add_to_cart', (action: Action<'add_to_cart'>) => {
|
|
288
|
+
console.log(action.type); // 'add_to_cart'
|
|
289
|
+
console.log(action.payload); // {productId: number; qty: number}
|
|
290
|
+
console.log(action.context); // string | undefined
|
|
291
|
+
console.log(action.meta); // Record<string, unknown> | undefined
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
177
297
|
### `ActionRecord` (interface)
|
|
178
298
|
|
|
179
299
|
The global action type registry. Extend via declaration merging to register typed actions.
|
|
@@ -181,8 +301,8 @@ The global action type registry. Extend via declaration merging to register type
|
|
|
181
301
|
```ts
|
|
182
302
|
declare module '@alwatr/action' {
|
|
183
303
|
interface ActionRecord {
|
|
184
|
-
|
|
185
|
-
|
|
304
|
+
open_drawer: string;
|
|
305
|
+
logout: void;
|
|
186
306
|
}
|
|
187
307
|
}
|
|
188
308
|
```
|
|
@@ -217,34 +337,46 @@ function teardownActionDelegation(): void;
|
|
|
217
337
|
|
|
218
338
|
---
|
|
219
339
|
|
|
220
|
-
### `onAction(
|
|
340
|
+
### `onAction(type, handler)`
|
|
221
341
|
|
|
222
|
-
Subscribes to a named action. O(1) routing via `ChannelSignal`.
|
|
342
|
+
Subscribes to a named action. O(1) routing via `ChannelSignal`. The handler receives the full `Action<K>` object.
|
|
223
343
|
|
|
224
344
|
```ts
|
|
225
|
-
function onAction<K extends keyof ActionRecord>(
|
|
226
|
-
actionId: K,
|
|
227
|
-
handler: (payload: ActionRecord[K]) => void,
|
|
228
|
-
): SubscribeResult;
|
|
345
|
+
function onAction<K extends keyof ActionRecord>(type: K, handler: (action: Action<K>) => void): SubscribeResult;
|
|
229
346
|
```
|
|
230
347
|
|
|
231
348
|
```ts
|
|
232
|
-
const sub = onAction('
|
|
349
|
+
const sub = onAction('open_drawer', (action) => {
|
|
350
|
+
openDrawer(action.payload); // payload: string
|
|
351
|
+
console.log(action.context); // e.g. 'sidebar' or undefined
|
|
352
|
+
});
|
|
233
353
|
sub.unsubscribe(); // prevent memory leaks
|
|
234
354
|
```
|
|
235
355
|
|
|
236
356
|
---
|
|
237
357
|
|
|
238
|
-
### `dispatchAction(
|
|
358
|
+
### `dispatchAction(action)`
|
|
239
359
|
|
|
240
360
|
Dispatches a named action. Payload type is enforced by `ActionRecord`.
|
|
241
361
|
|
|
362
|
+
```ts
|
|
363
|
+
function dispatchAction<K extends keyof ActionRecord>(action: Action<K>): void;
|
|
364
|
+
```
|
|
365
|
+
|
|
242
366
|
```ts
|
|
243
367
|
// With payload
|
|
244
|
-
dispatchAction('
|
|
368
|
+
dispatchAction({type: 'open_drawer', payload: 'settings'});
|
|
245
369
|
|
|
246
|
-
// Void payload
|
|
247
|
-
dispatchAction('logout');
|
|
370
|
+
// Void payload
|
|
371
|
+
dispatchAction({type: 'logout', payload: undefined});
|
|
372
|
+
|
|
373
|
+
// With context and meta
|
|
374
|
+
dispatchAction({
|
|
375
|
+
type: 'open_drawer',
|
|
376
|
+
payload: 'settings',
|
|
377
|
+
context: 'header',
|
|
378
|
+
meta: {triggeredBy: 'keyboard'},
|
|
379
|
+
});
|
|
248
380
|
```
|
|
249
381
|
|
|
250
382
|
---
|
|
@@ -253,20 +385,28 @@ dispatchAction('logout');
|
|
|
253
385
|
|
|
254
386
|
Registers a custom modifier. Return `false` to cancel the dispatch.
|
|
255
387
|
|
|
256
|
-
Handler signature: `(event: Event, element: HTMLElement) => boolean`
|
|
388
|
+
Handler signature: `(event: Event, element: HTMLElement, action: Action) => boolean`
|
|
389
|
+
|
|
390
|
+
The handler receives the mutable `action` object and may write to `action.meta`.
|
|
257
391
|
|
|
258
392
|
```ts
|
|
259
393
|
import {registerModifier} from '@alwatr/action';
|
|
260
394
|
|
|
261
|
-
|
|
262
|
-
registerModifier('not-disabled', (_event, element) => {
|
|
395
|
+
registerModifier('not_disabled', (_event, element) => {
|
|
263
396
|
return !(element as HTMLButtonElement).disabled;
|
|
264
397
|
});
|
|
398
|
+
|
|
399
|
+
// A modifier that enriches meta before dispatch
|
|
400
|
+
registerModifier('timestamp', (_event, _element, action) => {
|
|
401
|
+
action.meta ??= {};
|
|
402
|
+
action.meta['ts'] = Date.now();
|
|
403
|
+
return true;
|
|
404
|
+
});
|
|
265
405
|
```
|
|
266
406
|
|
|
267
407
|
```html
|
|
268
408
|
<button
|
|
269
|
-
on-
|
|
409
|
+
on-click="select_item:$data_id; not_disabled,timestamp"
|
|
270
410
|
data-id="42"
|
|
271
411
|
>
|
|
272
412
|
Select
|
|
@@ -284,11 +424,7 @@ Handler signature: `(event: Event, element: HTMLElement) => unknown`
|
|
|
284
424
|
```ts
|
|
285
425
|
import {registerPayloadResolver} from '@alwatr/action';
|
|
286
426
|
|
|
287
|
-
registerPayloadResolver('$
|
|
288
|
-
return (element as HTMLInputElement).checked;
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
registerPayloadResolver('$data-id', (_event, element) => {
|
|
427
|
+
registerPayloadResolver('$data_id', (_event, element) => {
|
|
292
428
|
return (element as HTMLElement).dataset.id ?? null;
|
|
293
429
|
});
|
|
294
430
|
```
|
|
@@ -296,10 +432,10 @@ registerPayloadResolver('$data-id', (_event, element) => {
|
|
|
296
432
|
```html
|
|
297
433
|
<input
|
|
298
434
|
type="checkbox"
|
|
299
|
-
on-
|
|
435
|
+
on-change="toggle_feature:$checked"
|
|
300
436
|
/>
|
|
301
437
|
<li
|
|
302
|
-
on-
|
|
438
|
+
on-click="select_item:$data_id"
|
|
303
439
|
data-id="42"
|
|
304
440
|
>
|
|
305
441
|
Item
|
|
@@ -311,33 +447,41 @@ registerPayloadResolver('$data-id', (_event, element) => {
|
|
|
311
447
|
## Unidirectional Data Flow
|
|
312
448
|
|
|
313
449
|
```
|
|
314
|
-
|
|
315
|
-
│
|
|
316
|
-
│ <
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
│
|
|
324
|
-
│
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
│
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
│
|
|
336
|
-
│
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
450
|
+
┌────────────────────────────────────────────────────────────┐
|
|
451
|
+
│ UI Layer │
|
|
452
|
+
│ <section action-context="cart"> │
|
|
453
|
+
│ <button on-click="add_to_cart:42">Add</button> │
|
|
454
|
+
│ </section> │
|
|
455
|
+
└─────────────────────────┬──────────────────────────────────┘
|
|
456
|
+
│ DOM event bubbles to body
|
|
457
|
+
▼
|
|
458
|
+
┌────────────────────────────────────────────────────────────┐
|
|
459
|
+
│ Action Layer (@alwatr/action) │
|
|
460
|
+
│ document.body capture listener (1 per event type) │
|
|
461
|
+
│ → closest('[on-click]') → parse attribute │
|
|
462
|
+
│ → closest('[action-context]') → context = 'cart' │
|
|
463
|
+
│ → run modifiers (may enrich action.meta) │
|
|
464
|
+
│ → resolve payload → '42' │
|
|
465
|
+
│ → dispatch Action {type, payload, context, meta} [O(1)] │
|
|
466
|
+
└─────────────────────────┬──────────────────────────────────┘
|
|
467
|
+
│ O(1) routing via ChannelSignal
|
|
468
|
+
▼
|
|
469
|
+
┌────────────────────────────────────────────────────────────┐
|
|
470
|
+
│ Business Logic Layer │
|
|
471
|
+
│ onAction('add_to_cart', (action) => { │
|
|
472
|
+
│ cartService.add(action.payload); │
|
|
473
|
+
│ // action.context === 'cart' │
|
|
474
|
+
│ }) │
|
|
475
|
+
└─────────────────────────┬──────────────────────────────────┘
|
|
476
|
+
│ state update
|
|
477
|
+
▼
|
|
478
|
+
┌────────────────────────────────────────────────────────────┐
|
|
479
|
+
│ State Layer (@alwatr/signal) │
|
|
480
|
+
│ cartSignal.set(newCartState) │
|
|
481
|
+
└─────────────────────────┬──────────────────────────────────┘
|
|
482
|
+
│ state flows down to UI
|
|
483
|
+
▼
|
|
484
|
+
UI re-renders
|
|
341
485
|
```
|
|
342
486
|
|
|
343
487
|
---
|
|
@@ -353,30 +497,94 @@ concern, not a user-interaction action.
|
|
|
353
497
|
|
|
354
498
|
## Migration from Previous Versions
|
|
355
499
|
|
|
356
|
-
### `
|
|
500
|
+
### `dispatchAction` API changed
|
|
357
501
|
|
|
358
|
-
|
|
502
|
+
`dispatchAction` now takes a single `Action` object instead of two positional arguments.
|
|
359
503
|
|
|
360
504
|
**Before:**
|
|
361
505
|
|
|
362
506
|
```ts
|
|
363
|
-
|
|
364
|
-
|
|
507
|
+
dispatchAction('open_drawer', 'settings');
|
|
508
|
+
dispatchAction('logout');
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
**After:**
|
|
512
|
+
|
|
513
|
+
```ts
|
|
514
|
+
dispatchAction({type: 'open_drawer', payload: 'settings'});
|
|
515
|
+
dispatchAction({type: 'logout', payload: undefined});
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### `onAction` handler signature changed
|
|
519
|
+
|
|
520
|
+
Handlers now receive the full `Action<K>` object instead of just the payload.
|
|
521
|
+
|
|
522
|
+
**Before:**
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
onAction('add_to_cart', (item) => {
|
|
526
|
+
cartService.add(item.productId, item.qty);
|
|
365
527
|
});
|
|
366
528
|
```
|
|
367
529
|
|
|
368
530
|
**After:**
|
|
369
531
|
|
|
370
532
|
```ts
|
|
371
|
-
|
|
533
|
+
onAction('add_to_cart', (action) => {
|
|
534
|
+
cartService.add(action.payload.productId, action.payload.qty);
|
|
535
|
+
// action.context is now also available
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### `registerModifier` handler signature changed
|
|
540
|
+
|
|
541
|
+
Modifier handlers now receive a third `action` argument for `meta` enrichment.
|
|
542
|
+
|
|
543
|
+
**Before:**
|
|
544
|
+
|
|
545
|
+
```ts
|
|
546
|
+
registerModifier('not_disabled', (_event, element) => {
|
|
372
547
|
return !(element as HTMLButtonElement).disabled;
|
|
373
548
|
});
|
|
374
549
|
```
|
|
375
550
|
|
|
376
|
-
|
|
551
|
+
**After (backward-compatible — third arg is optional to use):**
|
|
377
552
|
|
|
378
|
-
|
|
379
|
-
|
|
553
|
+
```ts
|
|
554
|
+
registerModifier('not_disabled', (_event, element, _action) => {
|
|
555
|
+
return !(element as HTMLButtonElement).disabled;
|
|
556
|
+
});
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Attribute syntax changed
|
|
560
|
+
|
|
561
|
+
The event type is now encoded in the **attribute name** instead of the value, and modifiers are listed after a semicolon instead of dot-chained before the arrow.
|
|
562
|
+
|
|
563
|
+
**Before:**
|
|
564
|
+
|
|
565
|
+
```html
|
|
566
|
+
<button on-action="click->open_drawer:main">Open</button>
|
|
567
|
+
<form
|
|
568
|
+
on-action="submit.prevent.validate->submit_form:$formdata"
|
|
569
|
+
novalidate
|
|
570
|
+
>
|
|
571
|
+
…
|
|
572
|
+
</form>
|
|
573
|
+
<button on-action="click.once->welcome_dismissed">Got it</button>
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**After:**
|
|
577
|
+
|
|
578
|
+
```html
|
|
579
|
+
<button on-click="open_drawer:main">Open</button>
|
|
580
|
+
<form
|
|
581
|
+
on-submit="submit_form:$formdata; prevent,validate"
|
|
582
|
+
novalidate
|
|
583
|
+
>
|
|
584
|
+
…
|
|
585
|
+
</form>
|
|
586
|
+
<button on-click="welcome_dismissed; once">Got it</button>
|
|
587
|
+
```
|
|
380
588
|
|
|
381
589
|
### `page-ready` moved to `@alwatr/page-ready`
|
|
382
590
|
|
|
@@ -384,6 +592,38 @@ Behavior is equivalent for typical use cases.
|
|
|
384
592
|
|
|
385
593
|
---
|
|
386
594
|
|
|
595
|
+
## 🌊 Part of Alwatr Flux
|
|
596
|
+
|
|
597
|
+
`@alwatr/action` is the **Action Layer** of the [Alwatr Flux](https://github.com/Alwatr/alwatr/tree/next/pkg/flux) architecture — a complete Unidirectional Data Flow system for building scalable Progressive Web Applications.
|
|
598
|
+
|
|
599
|
+
```
|
|
600
|
+
View (HTML on-<event> attributes + action-context)
|
|
601
|
+
↓
|
|
602
|
+
Action Layer (@alwatr/action) — global delegation, O(1) routing, AFSA objects
|
|
603
|
+
↓
|
|
604
|
+
Controller (business logic via onAction — receives full Action object)
|
|
605
|
+
↓
|
|
606
|
+
State Layer (@alwatr/signal) — fine-grained reactivity
|
|
607
|
+
↓
|
|
608
|
+
View (re-render only affected nodes)
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
`@alwatr/action` is the bridge between the **View** and **Controller** layers. It captures user intent from HTML attributes and routes it to the right handler — without any coupling between the UI and business logic.
|
|
612
|
+
|
|
613
|
+
**The full Flux bundle** (`@alwatr/flux`) includes actions, signals, directives, page-ready, and storage — everything you need to build a complete reactive application from a single import.
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// Use @alwatr/flux for the complete architecture
|
|
617
|
+
import {setupActionDelegation, onAction, createStateSignal} from '@alwatr/flux';
|
|
618
|
+
|
|
619
|
+
// Or use @alwatr/action standalone for just the action bus
|
|
620
|
+
import {setupActionDelegation, onAction, dispatchAction} from '@alwatr/action';
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
→ [View the complete Flux documentation](https://github.com/Alwatr/alwatr/tree/next/pkg/flux)
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
387
627
|
## Contributing
|
|
388
628
|
|
|
389
629
|
Contributions are welcome! Please read our [contribution guidelines](https://github.com/Alwatr/.github/blob/next/CONTRIBUTING.md) before submitting a pull request.
|
package/dist/action-record.d.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
|
* }
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
* // pkg/my-feature/src/action-record.ts
|
|
43
43
|
* declare module '@alwatr/action' {
|
|
44
44
|
* interface ActionRecord {
|
|
45
|
-
* '
|
|
46
|
-
* '
|
|
45
|
+
* 'open_drawer': string;
|
|
46
|
+
* 'add_to_cart': {productId: number; qty: number};
|
|
47
47
|
* 'logout': void;
|
|
48
48
|
* }
|
|
49
49
|
* }
|