@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/src/registry.ts
CHANGED
|
@@ -1,40 +1,44 @@
|
|
|
1
|
-
import type {ActionDirective} from './directive.js';
|
|
2
|
-
|
|
3
1
|
// ─── Type Definitions ────────────────────────────────────────────────────────
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
|
-
* A modifier handler
|
|
4
|
+
* A modifier handler used in `on-action` attribute syntax.
|
|
5
|
+
*
|
|
6
|
+
* Receives the triggering DOM `event` and the `element` that owns the
|
|
7
|
+
* `on-action` attribute. Return `true` (or any truthy value) to allow the
|
|
8
|
+
* action to proceed, or `false` to cancel the dispatch.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Returning `false` is the only way a modifier can veto a dispatch.
|
|
10
|
+
* Using explicit parameters instead of `this` binding makes handlers
|
|
11
|
+
* compatible with arrow functions and easier to test in isolation.
|
|
11
12
|
*
|
|
12
13
|
* @example
|
|
13
14
|
* ```ts
|
|
14
15
|
* // A modifier that only allows the action when the element is not disabled
|
|
15
|
-
* const notDisabledHandler: ModifierHandler =
|
|
16
|
-
* return !(
|
|
16
|
+
* const notDisabledHandler: ModifierHandler = (_event, element) => {
|
|
17
|
+
* return !(element as HTMLButtonElement).disabled;
|
|
17
18
|
* };
|
|
18
19
|
* ```
|
|
19
20
|
*/
|
|
20
|
-
export type ModifierHandler = (
|
|
21
|
+
export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* A payload resolver
|
|
24
|
+
* A payload resolver used in `on-action` attribute syntax.
|
|
25
|
+
*
|
|
26
|
+
* Receives the triggering DOM `event` and the `element` that owns the
|
|
27
|
+
* `on-action` attribute. The return value becomes the `actionPayload` passed
|
|
28
|
+
* to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
|
|
24
29
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* dispatched action. Use this to compute dynamic payloads from the DOM state.
|
|
30
|
+
* Using explicit parameters instead of `this` binding makes resolvers
|
|
31
|
+
* compatible with arrow functions and easier to test in isolation.
|
|
28
32
|
*
|
|
29
33
|
* @example
|
|
30
34
|
* ```ts
|
|
31
35
|
* // A resolver that returns the element's dataset id
|
|
32
|
-
* const dataIdResolver: PayloadResolver =
|
|
33
|
-
* return (
|
|
36
|
+
* const dataIdResolver: PayloadResolver = (_event, element) => {
|
|
37
|
+
* return (element as HTMLElement).dataset.id ?? null;
|
|
34
38
|
* };
|
|
35
39
|
* ```
|
|
36
40
|
*/
|
|
37
|
-
export type PayloadResolver = (
|
|
41
|
+
export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
|
|
38
42
|
|
|
39
43
|
// ─── Registries ──────────────────────────────────────────────────────────────
|
|
40
44
|
|
|
@@ -77,37 +81,21 @@ modifierRegistry.set('prevent', (event) => {
|
|
|
77
81
|
return true;
|
|
78
82
|
});
|
|
79
83
|
|
|
80
|
-
/**
|
|
81
|
-
* `stop` — calls `event.stopPropagation()` before dispatching.
|
|
82
|
-
*
|
|
83
|
-
* Prevents the event from bubbling further up the DOM tree. Useful when a
|
|
84
|
-
* child element should handle a click without triggering a parent's listener.
|
|
85
|
-
*
|
|
86
|
-
* @example `<button on-action="click.stop->select-item:42">`
|
|
87
|
-
*/
|
|
88
|
-
modifierRegistry.set('stop', (event) => {
|
|
89
|
-
event.stopPropagation();
|
|
90
|
-
return true;
|
|
91
|
-
});
|
|
92
|
-
|
|
93
84
|
/**
|
|
94
85
|
* `validate` — cancels the dispatch if the nearest `<form>` fails validation.
|
|
95
86
|
*
|
|
96
87
|
* Looks for a `<form>` ancestor (or the element itself if it is a form) and
|
|
97
88
|
* calls `checkValidity()`. If the form is invalid the action is not dispatched,
|
|
98
89
|
* allowing native constraint-validation UI to surface errors. If no form is
|
|
99
|
-
* found the dispatch is also cancelled
|
|
90
|
+
* found the dispatch is also cancelled.
|
|
100
91
|
*
|
|
101
92
|
* Pair with `.prevent` on `submit` events to avoid page reloads:
|
|
102
93
|
*
|
|
103
|
-
* @example `<form on-action="submit.prevent.validate->submit-form" novalidate>`
|
|
94
|
+
* @example `<form on-action="submit.prevent.validate->submit-form:$formdata" novalidate>`
|
|
104
95
|
*/
|
|
105
|
-
modifierRegistry.set('validate',
|
|
106
|
-
const form =
|
|
107
|
-
if (!form)
|
|
108
|
-
this.logger_.accident('validate_modifier', 'no_form_found', {element: this.element_});
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
96
|
+
modifierRegistry.set('validate', (_event, element) => {
|
|
97
|
+
const form = element instanceof HTMLFormElement ? element : element.closest('form');
|
|
98
|
+
if (!form) return false;
|
|
111
99
|
return form.checkValidity();
|
|
112
100
|
});
|
|
113
101
|
|
|
@@ -121,8 +109,8 @@ modifierRegistry.set('validate', function () {
|
|
|
121
109
|
*
|
|
122
110
|
* @example `<input on-action="input->search-query:$value" />`
|
|
123
111
|
*/
|
|
124
|
-
payloadRegistry.set('$value',
|
|
125
|
-
return 'value' in
|
|
112
|
+
payloadRegistry.set('$value', (_event, element) => {
|
|
113
|
+
return 'value' in element ? (element as {value: unknown}).value : null;
|
|
126
114
|
});
|
|
127
115
|
|
|
128
116
|
/**
|
|
@@ -132,14 +120,14 @@ payloadRegistry.set('$value', function () {
|
|
|
132
120
|
* Looks for a `<form>` ancestor (or the element itself). Returns `null` when no
|
|
133
121
|
* form is found.
|
|
134
122
|
*
|
|
135
|
-
* @example `<form on-action="submit.prevent.validate->submit-form">`
|
|
123
|
+
* @example `<form on-action="submit.prevent.validate->submit-form:$formdata">`
|
|
136
124
|
* ```ts
|
|
137
|
-
* onAction
|
|
125
|
+
* onAction('submit-form', (data) => {
|
|
138
126
|
* console.log(data); // {username: 'ali', password: '…'}
|
|
139
127
|
* });
|
|
140
128
|
* ```
|
|
141
129
|
*/
|
|
142
|
-
payloadRegistry.set('$formdata',
|
|
143
|
-
const form =
|
|
130
|
+
payloadRegistry.set('$formdata', (_event, element) => {
|
|
131
|
+
const form = element instanceof HTMLFormElement ? element : element.closest('form');
|
|
144
132
|
return form ? Object.fromEntries(new FormData(form).entries()) : null;
|
|
145
133
|
});
|
package/dist/directive.d.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Directive } from '@alwatr/directive';
|
|
2
|
-
/**
|
|
3
|
-
* Directive that bridges a DOM event to a typed action signal.
|
|
4
|
-
*
|
|
5
|
-
* Activated automatically by the `on-action` HTML attribute when
|
|
6
|
-
* `registerActionDirective()` has been called before `bootstrapDirectives()`.
|
|
7
|
-
* You rarely need to reference this class directly.
|
|
8
|
-
*
|
|
9
|
-
* **Attribute syntax**
|
|
10
|
-
* ```
|
|
11
|
-
* on-action="eventType[.modifier…]->actionId[:payload]"
|
|
12
|
-
* ```
|
|
13
|
-
*
|
|
14
|
-
* - `eventType` — any standard DOM event name (e.g. `click`, `input`, `submit`).
|
|
15
|
-
* - `modifier` — dot-chained tokens processed before dispatch
|
|
16
|
-
* (`prevent`, `stop`, `validate`, `once`, `passive`, or custom).
|
|
17
|
-
* - `actionId` — the identifier passed to `onAction` subscribers.
|
|
18
|
-
* - `payload` — an optional literal string or a `$`-prefixed resolver token
|
|
19
|
-
* (e.g. `$value`, `$formdata`, or custom).
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```html
|
|
23
|
-
* <!-- Dispatches 'open-drawer' with payload 'settings' on click -->
|
|
24
|
-
* <button on-action="click->open-drawer:settings">Settings</button>
|
|
25
|
-
*
|
|
26
|
-
* <!-- Dispatches 'search-query' with the live input value on every keystroke -->
|
|
27
|
-
* <input on-action="input->search-query:$value" />
|
|
28
|
-
*
|
|
29
|
-
* <!-- Prevents default, validates, then dispatches 'submit-form' with all field values -->
|
|
30
|
-
* <form on-action="submit.prevent.validate->submit-form:$formdata" novalidate>…</form>
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export declare class ActionDirective extends Directive {
|
|
34
|
-
/**
|
|
35
|
-
* Parsed and validated representation of the `on-action` attribute value.
|
|
36
|
-
*
|
|
37
|
-
* Set during `init_()` after the attribute is successfully parsed against
|
|
38
|
-
* `syntaxRegex`. Remains `undefined` when the attribute value is invalid,
|
|
39
|
-
* which prevents `dispatch_` from running.
|
|
40
|
-
*/
|
|
41
|
-
protected actionContext_?: {
|
|
42
|
-
/** The DOM event type to listen for (e.g. `'click'`, `'input'`). */
|
|
43
|
-
eventType: string;
|
|
44
|
-
/** Set of active modifier names (e.g. `{'prevent', 'once'}`). */
|
|
45
|
-
modifiers: ReadonlySet<string>;
|
|
46
|
-
/** The action identifier dispatched to `onAction` subscribers. */
|
|
47
|
-
actionId: string;
|
|
48
|
-
/** Raw payload token from the attribute (literal string or `$`-resolver key). */
|
|
49
|
-
payload?: string;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Parses the `on-action` attribute, validates modifiers, and attaches the
|
|
53
|
-
* DOM event listener.
|
|
54
|
-
*
|
|
55
|
-
* Called once by `Directive` after one macrotask following element discovery.
|
|
56
|
-
* If the attribute value is malformed or references an unknown modifier,
|
|
57
|
-
* an accident is logged and the directive becomes a no-op.
|
|
58
|
-
*/
|
|
59
|
-
protected init_(): void;
|
|
60
|
-
/**
|
|
61
|
-
* DOM event handler: runs modifiers, resolves the payload, and dispatches
|
|
62
|
-
* the action signal.
|
|
63
|
-
*
|
|
64
|
-
* Execution order:
|
|
65
|
-
* 1. Each modifier in `actionContext_.modifiers` is called in insertion order.
|
|
66
|
-
* If any returns `false` the method returns early — no action is dispatched.
|
|
67
|
-
* 2. The raw payload token is looked up in `payloadRegistry`. If a resolver
|
|
68
|
-
* is found it is called and its return value replaces the token.
|
|
69
|
-
* 3. `dispatchAction` is called with the resolved payload.
|
|
70
|
-
*
|
|
71
|
-
* @param event - The DOM event that triggered this handler.
|
|
72
|
-
*/
|
|
73
|
-
protected dispatch_(event: Event): void;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Registers `ActionDirective` under the `on-action` attribute name.
|
|
77
|
-
*
|
|
78
|
-
* This is a **lazy** registration: calling this function is the only way to
|
|
79
|
-
* opt-in to `on-action` support. If it is never called, the entire directive
|
|
80
|
-
* module (including `ActionDirective`) is tree-shaken from the bundle.
|
|
81
|
-
*
|
|
82
|
-
* Call it once, before `bootstrapDirectives()`, at your application entry point.
|
|
83
|
-
*
|
|
84
|
-
* @example
|
|
85
|
-
* ```ts
|
|
86
|
-
* import {registerActionDirective} from '@alwatr/action';
|
|
87
|
-
* import {bootstrapDirectives} from '@alwatr/directive';
|
|
88
|
-
*
|
|
89
|
-
* registerActionDirective();
|
|
90
|
-
* bootstrapDirectives();
|
|
91
|
-
* ```
|
|
92
|
-
*/
|
|
93
|
-
export declare const registerActionDirective: () => void;
|
|
94
|
-
//# sourceMappingURL=directive.d.ts.map
|
package/dist/directive.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"directive.d.ts","sourceRoot":"","sources":["../src/directive.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,SAAS,EAAC,MAAM,mBAAmB,CAAC;AA4B3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBAAa,eAAgB,SAAQ,SAAS;IAC5C;;;;;;OAMG;IACH,SAAS,CAAC,cAAc,CAAC,EAAE;QACzB,oEAAoE;QACpE,SAAS,EAAE,MAAM,CAAC;QAClB,iEAAiE;QACjE,SAAS,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;QAC/B,kEAAkE;QAClE,QAAQ,EAAE,MAAM,CAAC;QACjB,iFAAiF;QACjF,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IAEF;;;;;;;OAOG;cACgB,KAAK,IAAI,IAAI;IAsDhC;;;;;;;;;;;;OAYG;IACH,SAAS,CAAC,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;CAqBxC;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,uBAAuB,YAA8C,CAAC"}
|
package/dist/page-id.d.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { Directive } from '@alwatr/directive';
|
|
2
|
-
/**
|
|
3
|
-
* Directive that announces the current page identity as an action signal.
|
|
4
|
-
*
|
|
5
|
-
* Activated by the `page-id` HTML attribute. On bootstrap the directive reads
|
|
6
|
-
* the attribute value as the page identifier, dispatches a `'page-ready'`
|
|
7
|
-
* action with that value as the payload, and immediately self-destructs — no
|
|
8
|
-
* persistent listener is registered.
|
|
9
|
-
*
|
|
10
|
-
* Typical placement is on the `<body>` or the top-level page container so that
|
|
11
|
-
* any part of the application can react to route changes by subscribing to the
|
|
12
|
-
* `'page-ready'` action via `onAction`.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```html
|
|
16
|
-
* <!-- Dispatches dispatchAction('page-ready', 'home') on bootstrap -->
|
|
17
|
-
* <body page-id="home">…</body>
|
|
18
|
-
* ```
|
|
19
|
-
*/
|
|
20
|
-
export declare class PageIdDirective extends Directive {
|
|
21
|
-
/**
|
|
22
|
-
* Reads the `page-id` attribute value, dispatches `'page-ready'` with it as
|
|
23
|
-
* the payload, then destroys the directive.
|
|
24
|
-
*
|
|
25
|
-
* Logs an accident and returns early if the attribute value is empty.
|
|
26
|
-
*/
|
|
27
|
-
protected init_(): void;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Registers `PageIdDirective` under the `page-id` attribute name.
|
|
31
|
-
*
|
|
32
|
-
* This is a **lazy** registration: calling this function is the only way to
|
|
33
|
-
* opt-in to `page-id` support. If it is never called, the entire directive
|
|
34
|
-
* module is tree-shaken from the bundle.
|
|
35
|
-
*
|
|
36
|
-
* Call it once, before `bootstrapDirectives()`, at your application entry point.
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```ts
|
|
40
|
-
* import {registerPageIdDirective, onAction} from '@alwatr/action';
|
|
41
|
-
* import {bootstrapDirectives} from '@alwatr/directive';
|
|
42
|
-
*
|
|
43
|
-
* registerPageIdDirective();
|
|
44
|
-
* bootstrapDirectives();
|
|
45
|
-
*
|
|
46
|
-
* // React to every page change
|
|
47
|
-
* onAction('page-ready', (pageId) => {
|
|
48
|
-
* console.log('navigated to:', pageId); // e.g. 'home', 'about', 'product-detail'
|
|
49
|
-
* });
|
|
50
|
-
* ```
|
|
51
|
-
*
|
|
52
|
-
* ```html
|
|
53
|
-
* <body page-id="home">…</body>
|
|
54
|
-
* ```
|
|
55
|
-
*/
|
|
56
|
-
export declare const registerPageIdDirective: () => void;
|
|
57
|
-
//# sourceMappingURL=page-id.d.ts.map
|
package/dist/page-id.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"page-id.d.ts","sourceRoot":"","sources":["../src/page-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAgB,MAAM,mBAAmB,CAAC;AAK3D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAgB,SAAQ,SAAS;IAC5C;;;;;OAKG;cACgB,KAAK,IAAI,IAAI;CAYjC;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,uBAAuB,YAA4C,CAAC"}
|
package/src/directive.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import {lazyDirective, Directive} from '@alwatr/directive';
|
|
2
|
-
import {modifierRegistry, payloadRegistry} from './registry.js';
|
|
3
|
-
import {dispatchAction} from './method.js';
|
|
4
|
-
|
|
5
|
-
// ─── Attribute Syntax Parser ──────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Regex that parses the `on-action` attribute value into its three segments.
|
|
9
|
-
*
|
|
10
|
-
* Full syntax: `eventType[.modifier…]->actionId[:payload]`
|
|
11
|
-
*
|
|
12
|
-
* | Capture group | Matches | Example |
|
|
13
|
-
* | ------------- | ------------------------------------------- | -------------------- |
|
|
14
|
-
* | 1 | Event type + optional dot-chained modifiers | `click.prevent.once` |
|
|
15
|
-
* | 2 | Action identifier | `open-drawer` |
|
|
16
|
-
* | 3 | Optional payload token or literal | `main` / `$value` |
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```
|
|
20
|
-
* 'click.prevent.once->open-drawer:main' → ['click.prevent.once', 'open-drawer', 'main']
|
|
21
|
-
* 'input->search-query:$value' → ['input', 'search-query', '$value']
|
|
22
|
-
* 'submit.prevent->submit-form' → ['submit.prevent', 'submit-form', undefined]
|
|
23
|
-
* ```
|
|
24
|
-
*/
|
|
25
|
-
const syntaxRegex = /^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/;
|
|
26
|
-
|
|
27
|
-
// ─── Directive Class ──────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Directive that bridges a DOM event to a typed action signal.
|
|
31
|
-
*
|
|
32
|
-
* Activated automatically by the `on-action` HTML attribute when
|
|
33
|
-
* `registerActionDirective()` has been called before `bootstrapDirectives()`.
|
|
34
|
-
* You rarely need to reference this class directly.
|
|
35
|
-
*
|
|
36
|
-
* **Attribute syntax**
|
|
37
|
-
* ```
|
|
38
|
-
* on-action="eventType[.modifier…]->actionId[:payload]"
|
|
39
|
-
* ```
|
|
40
|
-
*
|
|
41
|
-
* - `eventType` — any standard DOM event name (e.g. `click`, `input`, `submit`).
|
|
42
|
-
* - `modifier` — dot-chained tokens processed before dispatch
|
|
43
|
-
* (`prevent`, `stop`, `validate`, `once`, `passive`, or custom).
|
|
44
|
-
* - `actionId` — the identifier passed to `onAction` subscribers.
|
|
45
|
-
* - `payload` — an optional literal string or a `$`-prefixed resolver token
|
|
46
|
-
* (e.g. `$value`, `$formdata`, or custom).
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```html
|
|
50
|
-
* <!-- Dispatches 'open-drawer' with payload 'settings' on click -->
|
|
51
|
-
* <button on-action="click->open-drawer:settings">Settings</button>
|
|
52
|
-
*
|
|
53
|
-
* <!-- Dispatches 'search-query' with the live input value on every keystroke -->
|
|
54
|
-
* <input on-action="input->search-query:$value" />
|
|
55
|
-
*
|
|
56
|
-
* <!-- Prevents default, validates, then dispatches 'submit-form' with all field values -->
|
|
57
|
-
* <form on-action="submit.prevent.validate->submit-form:$formdata" novalidate>…</form>
|
|
58
|
-
* ```
|
|
59
|
-
*/
|
|
60
|
-
export class ActionDirective extends Directive {
|
|
61
|
-
/**
|
|
62
|
-
* Parsed and validated representation of the `on-action` attribute value.
|
|
63
|
-
*
|
|
64
|
-
* Set during `init_()` after the attribute is successfully parsed against
|
|
65
|
-
* `syntaxRegex`. Remains `undefined` when the attribute value is invalid,
|
|
66
|
-
* which prevents `dispatch_` from running.
|
|
67
|
-
*/
|
|
68
|
-
protected actionContext_?: {
|
|
69
|
-
/** The DOM event type to listen for (e.g. `'click'`, `'input'`). */
|
|
70
|
-
eventType: string;
|
|
71
|
-
/** Set of active modifier names (e.g. `{'prevent', 'once'}`). */
|
|
72
|
-
modifiers: ReadonlySet<string>;
|
|
73
|
-
/** The action identifier dispatched to `onAction` subscribers. */
|
|
74
|
-
actionId: string;
|
|
75
|
-
/** Raw payload token from the attribute (literal string or `$`-resolver key). */
|
|
76
|
-
payload?: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Parses the `on-action` attribute, validates modifiers, and attaches the
|
|
81
|
-
* DOM event listener.
|
|
82
|
-
*
|
|
83
|
-
* Called once by `Directive` after one macrotask following element discovery.
|
|
84
|
-
* If the attribute value is malformed or references an unknown modifier,
|
|
85
|
-
* an accident is logged and the directive becomes a no-op.
|
|
86
|
-
*/
|
|
87
|
-
protected override init_(): void {
|
|
88
|
-
this.logger_.logMethodArgs?.('init_', {attributeValue: this.attributeValue});
|
|
89
|
-
|
|
90
|
-
const match = this.attributeValue.trim().match(syntaxRegex);
|
|
91
|
-
|
|
92
|
-
if (!match) {
|
|
93
|
-
this.logger_.accident('init_', 'invalid_syntax', {attributeValue: this.attributeValue});
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const [eventType, ...modifierList] = match[1].split('.');
|
|
98
|
-
const actionId = match[2];
|
|
99
|
-
const payload = match[3] as string | undefined;
|
|
100
|
-
|
|
101
|
-
if (!eventType) {
|
|
102
|
-
this.logger_.accident('init_', 'invalid_syntax', {attributeValue: this.attributeValue});
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Validate every modifier token against the registry (built-in native
|
|
107
|
-
// options 'once' and 'passive' are handled separately by the listener).
|
|
108
|
-
const modifiers = new Set<string>();
|
|
109
|
-
for (const modifier of modifierList) {
|
|
110
|
-
if (!modifierRegistry.has(modifier) && modifier !== 'once' && modifier !== 'passive') {
|
|
111
|
-
this.logger_.accident('init_', 'invalid_modifier', {attributeValue: this.attributeValue, modifier});
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
modifiers.add(modifier);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// 'prevent' and 'passive' are mutually exclusive: a passive listener cannot
|
|
118
|
-
// call preventDefault(). Log an accident but continue — 'prevent' wins.
|
|
119
|
-
if (modifiers.has('prevent') && modifiers.has('passive')) {
|
|
120
|
-
this.logger_.accident('init_', 'conflicting_modifiers_prevent_passive', {attributeValue: this.attributeValue});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
this.actionContext_ = {eventType, modifiers, actionId, payload};
|
|
124
|
-
|
|
125
|
-
// Bind once so the same function reference is used for both add and remove.
|
|
126
|
-
const listenerOptions: AddEventListenerOptions = {
|
|
127
|
-
once: modifiers.has('once'),
|
|
128
|
-
// 'passive' is only meaningful when 'prevent' is absent.
|
|
129
|
-
passive: modifiers.has('passive') && !modifiers.has('prevent'),
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const boundDispatch = this.dispatch_.bind(this);
|
|
133
|
-
this.element_.addEventListener(eventType, boundDispatch, listenerOptions);
|
|
134
|
-
// Register cleanup so the listener is removed when the directive is destroyed
|
|
135
|
-
// (e.g. when the element is removed from the DOM via autoDestructDirectives).
|
|
136
|
-
this.addDestroyHook(() => {
|
|
137
|
-
this.element_.removeEventListener(eventType, boundDispatch, listenerOptions);
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* DOM event handler: runs modifiers, resolves the payload, and dispatches
|
|
143
|
-
* the action signal.
|
|
144
|
-
*
|
|
145
|
-
* Execution order:
|
|
146
|
-
* 1. Each modifier in `actionContext_.modifiers` is called in insertion order.
|
|
147
|
-
* If any returns `false` the method returns early — no action is dispatched.
|
|
148
|
-
* 2. The raw payload token is looked up in `payloadRegistry`. If a resolver
|
|
149
|
-
* is found it is called and its return value replaces the token.
|
|
150
|
-
* 3. `dispatchAction` is called with the resolved payload.
|
|
151
|
-
*
|
|
152
|
-
* @param event - The DOM event that triggered this handler.
|
|
153
|
-
*/
|
|
154
|
-
protected dispatch_(event: Event): void {
|
|
155
|
-
this.logger_.logMethodArgs?.('dispatch_', {eventType: event.type, actionId: this.actionContext_?.actionId});
|
|
156
|
-
|
|
157
|
-
const context = this.actionContext_!;
|
|
158
|
-
|
|
159
|
-
// Step 1 — run modifiers; any returning false cancels the dispatch.
|
|
160
|
-
for (const mod of context.modifiers) {
|
|
161
|
-
const handler = modifierRegistry.get(mod);
|
|
162
|
-
if (handler && handler.call(this, event) === false) return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Step 2 — resolve dynamic payload tokens (e.g. '$value', '$formdata').
|
|
166
|
-
let payload: unknown = context.payload;
|
|
167
|
-
if (payload) {
|
|
168
|
-
const resolver = payloadRegistry.get(payload as string);
|
|
169
|
-
if (resolver) payload = resolver.call(this, event);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Step 3 — dispatch the action to all onAction subscribers.
|
|
173
|
-
dispatchAction(context.actionId, payload);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─── Lazy Registration ────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Registers `ActionDirective` under the `on-action` attribute name.
|
|
181
|
-
*
|
|
182
|
-
* This is a **lazy** registration: calling this function is the only way to
|
|
183
|
-
* opt-in to `on-action` support. If it is never called, the entire directive
|
|
184
|
-
* module (including `ActionDirective`) is tree-shaken from the bundle.
|
|
185
|
-
*
|
|
186
|
-
* Call it once, before `bootstrapDirectives()`, at your application entry point.
|
|
187
|
-
*
|
|
188
|
-
* @example
|
|
189
|
-
* ```ts
|
|
190
|
-
* import {registerActionDirective} from '@alwatr/action';
|
|
191
|
-
* import {bootstrapDirectives} from '@alwatr/directive';
|
|
192
|
-
*
|
|
193
|
-
* registerActionDirective();
|
|
194
|
-
* bootstrapDirectives();
|
|
195
|
-
* ```
|
|
196
|
-
*/
|
|
197
|
-
export const registerActionDirective = lazyDirective('on-action', ActionDirective);
|
package/src/page-id.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {Directive, lazyDirective} from '@alwatr/directive';
|
|
2
|
-
import {dispatchAction} from './method.js';
|
|
3
|
-
|
|
4
|
-
// ─── Directive Class ──────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Directive that announces the current page identity as an action signal.
|
|
8
|
-
*
|
|
9
|
-
* Activated by the `page-id` HTML attribute. On bootstrap the directive reads
|
|
10
|
-
* the attribute value as the page identifier, dispatches a `'page-ready'`
|
|
11
|
-
* action with that value as the payload, and immediately self-destructs — no
|
|
12
|
-
* persistent listener is registered.
|
|
13
|
-
*
|
|
14
|
-
* Typical placement is on the `<body>` or the top-level page container so that
|
|
15
|
-
* any part of the application can react to route changes by subscribing to the
|
|
16
|
-
* `'page-ready'` action via `onAction`.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```html
|
|
20
|
-
* <!-- Dispatches dispatchAction('page-ready', 'home') on bootstrap -->
|
|
21
|
-
* <body page-id="home">…</body>
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export class PageIdDirective extends Directive {
|
|
25
|
-
/**
|
|
26
|
-
* Reads the `page-id` attribute value, dispatches `'page-ready'` with it as
|
|
27
|
-
* the payload, then destroys the directive.
|
|
28
|
-
*
|
|
29
|
-
* Logs an accident and returns early if the attribute value is empty.
|
|
30
|
-
*/
|
|
31
|
-
protected override init_(): void {
|
|
32
|
-
const pageId = this.attributeValue.trim();
|
|
33
|
-
this.logger_.logMethodArgs?.('init_', {pageId});
|
|
34
|
-
|
|
35
|
-
if (!pageId) {
|
|
36
|
-
this.logger_.accident('init_', 'empty_page_id');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
dispatchAction('page-ready', pageId);
|
|
41
|
-
this.destroy();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── Lazy Registration ────────────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Registers `PageIdDirective` under the `page-id` attribute name.
|
|
49
|
-
*
|
|
50
|
-
* This is a **lazy** registration: calling this function is the only way to
|
|
51
|
-
* opt-in to `page-id` support. If it is never called, the entire directive
|
|
52
|
-
* module is tree-shaken from the bundle.
|
|
53
|
-
*
|
|
54
|
-
* Call it once, before `bootstrapDirectives()`, at your application entry point.
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* ```ts
|
|
58
|
-
* import {registerPageIdDirective, onAction} from '@alwatr/action';
|
|
59
|
-
* import {bootstrapDirectives} from '@alwatr/directive';
|
|
60
|
-
*
|
|
61
|
-
* registerPageIdDirective();
|
|
62
|
-
* bootstrapDirectives();
|
|
63
|
-
*
|
|
64
|
-
* // React to every page change
|
|
65
|
-
* onAction('page-ready', (pageId) => {
|
|
66
|
-
* console.log('navigated to:', pageId); // e.g. 'home', 'about', 'product-detail'
|
|
67
|
-
* });
|
|
68
|
-
* ```
|
|
69
|
-
*
|
|
70
|
-
* ```html
|
|
71
|
-
* <body page-id="home">…</body>
|
|
72
|
-
* ```
|
|
73
|
-
*/
|
|
74
|
-
export const registerPageIdDirective = lazyDirective('page-id', PageIdDirective);
|