@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.
@@ -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 attached to an `on-action` directive.
4
+ * A modifier handler used in `on-action` attribute syntax.
7
5
  *
8
- * Called with the directive instance as `this` and the triggering DOM `event`.
9
- * Return `true` to allow the action to proceed, or `false` to cancel it.
10
- * Returning `false` is the only way a modifier can veto a dispatch.
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.element_ as HTMLButtonElement).disabled;
14
+ * return !(this.element as HTMLButtonElement).disabled;
17
15
  * };
18
16
  * ```
19
17
  */
20
- export type ModifierHandler = (this: ActionDirective, event: Event) => boolean;
18
+ export type ModifierHandler = (event: Event, element: HTMLElement) => boolean;
21
19
 
22
20
  /**
23
- * A payload resolver attached to an `on-action` directive.
21
+ * A payload resolver used in `on-action` attribute syntax.
24
22
  *
25
- * Called with the directive instance as `this` and the triggering DOM `event`
26
- * at dispatch time. The return value becomes the `actionPayload` of the
27
- * dispatched action. Use this to compute dynamic payloads from the DOM state.
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.element_ as HTMLElement).dataset.id ?? null;
31
+ * return (this.element as HTMLElement).dataset.id ?? null;
34
32
  * };
35
33
  * ```
36
34
  */
37
- export type PayloadResolver = (this: ActionDirective, event: Event) => unknown;
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 and an accident is logged.
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 = this.element_ instanceof HTMLFormElement ? this.element_ : this.element_.closest('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 this.element_ ? (this.element_ as {value: unknown}).value : null;
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 = this.element_ instanceof HTMLFormElement ? this.element_ : this.element_.closest('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
  });
@@ -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
@@ -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
@@ -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);