@alwatr/action 9.11.2 → 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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {createLogger} from '@alwatr/logger';
|
|
2
|
+
import {createChannelSignal} from '@alwatr/signal';
|
|
3
|
+
|
|
4
|
+
const logger = createLogger('page-ready');
|
|
5
|
+
|
|
6
|
+
const pageReadyChannel_ = createChannelSignal<Record<string, undefined>>({
|
|
7
|
+
name: 'page-ready',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export function onPageReady<T extends string>(pageId: T, handler: () => void) {
|
|
11
|
+
logger.logMethodArgs?.('onPageReady', {pageId});
|
|
12
|
+
pageReadyChannel_.on(pageId, handler);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function dispatchPageReady(): void {
|
|
16
|
+
logger.logMethod?.('dispatchPageReady');
|
|
17
|
+
const element = document.querySelector('[page-id]');
|
|
18
|
+
if (!element) {
|
|
19
|
+
logger.incident?.('dispatchPageReady', 'element_not_found');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pageId = element.getAttribute('page-id')?.trim();
|
|
24
|
+
|
|
25
|
+
if (!pageId) {
|
|
26
|
+
logger.accident('dispatchPageReady', 'empty_page_id', {element});
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
pageReadyChannel_.dispatch(pageId);
|
|
31
|
+
}
|
package/src/registry.ts
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
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.
|
|
7
5
|
*
|
|
8
|
-
* Called with
|
|
9
|
-
* Return `true` to allow the action to proceed,
|
|
10
|
-
*
|
|
6
|
+
* Called with an `ActionContext` as `this` and the triggering DOM `event`.
|
|
7
|
+
* Return `true` (or any truthy value) to allow the action to proceed,
|
|
8
|
+
* or `false` to cancel the dispatch.
|
|
11
9
|
*
|
|
12
10
|
* @example
|
|
13
11
|
* ```ts
|
|
14
12
|
* // A modifier that only allows the action when the element is not disabled
|
|
15
13
|
* const notDisabledHandler: ModifierHandler = function () {
|
|
16
|
-
* return !(this.
|
|
14
|
+
* return !(this.element as HTMLButtonElement).disabled;
|
|
17
15
|
* };
|
|
18
16
|
* ```
|
|
19
17
|
*/
|
|
20
|
-
export type ModifierHandler = (
|
|
18
|
+
export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
|
-
* A payload resolver
|
|
21
|
+
* A payload resolver used in `on-action` attribute syntax.
|
|
24
22
|
*
|
|
25
|
-
* Called with
|
|
26
|
-
* at dispatch time. The return value becomes the `actionPayload`
|
|
27
|
-
*
|
|
23
|
+
* Called with an `ActionContext` as `this` and the triggering DOM `event`
|
|
24
|
+
* at dispatch time. The return value becomes the `actionPayload` passed to
|
|
25
|
+
* `onAction` subscribers. Use this to compute dynamic payloads from DOM state.
|
|
28
26
|
*
|
|
29
27
|
* @example
|
|
30
28
|
* ```ts
|
|
31
29
|
* // A resolver that returns the element's dataset id
|
|
32
30
|
* const dataIdResolver: PayloadResolver = function () {
|
|
33
|
-
* return (this.
|
|
31
|
+
* return (this.element as HTMLElement).dataset.id ?? null;
|
|
34
32
|
* };
|
|
35
33
|
* ```
|
|
36
34
|
*/
|
|
37
|
-
export type PayloadResolver = (
|
|
35
|
+
export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
|
|
38
36
|
|
|
39
37
|
// ─── Registries ──────────────────────────────────────────────────────────────
|
|
40
38
|
|
|
@@ -77,37 +75,21 @@ modifierRegistry.set('prevent', (event) => {
|
|
|
77
75
|
return true;
|
|
78
76
|
});
|
|
79
77
|
|
|
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
78
|
/**
|
|
94
79
|
* `validate` — cancels the dispatch if the nearest `<form>` fails validation.
|
|
95
80
|
*
|
|
96
81
|
* Looks for a `<form>` ancestor (or the element itself if it is a form) and
|
|
97
82
|
* calls `checkValidity()`. If the form is invalid the action is not dispatched,
|
|
98
83
|
* allowing native constraint-validation UI to surface errors. If no form is
|
|
99
|
-
* found the dispatch is also cancelled
|
|
84
|
+
* found the dispatch is also cancelled.
|
|
100
85
|
*
|
|
101
86
|
* Pair with `.prevent` on `submit` events to avoid page reloads:
|
|
102
87
|
*
|
|
103
|
-
* @example `<form on-action="submit.prevent.validate->submit-form" novalidate>`
|
|
88
|
+
* @example `<form on-action="submit.prevent.validate->submit-form:$formdata" novalidate>`
|
|
104
89
|
*/
|
|
105
|
-
modifierRegistry.set('validate', function () {
|
|
106
|
-
const form =
|
|
107
|
-
if (!form)
|
|
108
|
-
this.logger_.accident('validate_modifier', 'no_form_found', {element: this.element_});
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
90
|
+
modifierRegistry.set('validate', function (_, element) {
|
|
91
|
+
const form = element instanceof HTMLFormElement ? element : element.closest('form');
|
|
92
|
+
if (!form) return false;
|
|
111
93
|
return form.checkValidity();
|
|
112
94
|
});
|
|
113
95
|
|
|
@@ -121,8 +103,8 @@ modifierRegistry.set('validate', function () {
|
|
|
121
103
|
*
|
|
122
104
|
* @example `<input on-action="input->search-query:$value" />`
|
|
123
105
|
*/
|
|
124
|
-
payloadRegistry.set('$value', function () {
|
|
125
|
-
return 'value' in
|
|
106
|
+
payloadRegistry.set('$value', function (_, element) {
|
|
107
|
+
return 'value' in element ? (element as {value: unknown}).value : null;
|
|
126
108
|
});
|
|
127
109
|
|
|
128
110
|
/**
|
|
@@ -132,14 +114,14 @@ payloadRegistry.set('$value', function () {
|
|
|
132
114
|
* Looks for a `<form>` ancestor (or the element itself). Returns `null` when no
|
|
133
115
|
* form is found.
|
|
134
116
|
*
|
|
135
|
-
* @example `<form on-action="submit.prevent.validate->submit-form">`
|
|
117
|
+
* @example `<form on-action="submit.prevent.validate->submit-form:$formdata">`
|
|
136
118
|
* ```ts
|
|
137
119
|
* onAction<Record<string, FormDataEntryValue>>('submit-form', (data) => {
|
|
138
120
|
* console.log(data); // {username: 'ali', password: '…'}
|
|
139
121
|
* });
|
|
140
122
|
* ```
|
|
141
123
|
*/
|
|
142
|
-
payloadRegistry.set('$formdata', function () {
|
|
143
|
-
const form =
|
|
124
|
+
payloadRegistry.set('$formdata', function (_, element) {
|
|
125
|
+
const form = element instanceof HTMLFormElement ? element : element.closest('form');
|
|
144
126
|
return form ? Object.fromEntries(new FormData(form).entries()) : null;
|
|
145
127
|
});
|
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);
|