@alwatr/page-ready 9.14.0 → 9.20.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 CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  **Lightweight page identity signal for multi page applications routing.**
4
4
 
5
- ## Reads the `page-id` HTML attribute from the document and notifies subscribers using a dedicated O(1) channel signal. Designed for SSG/SSR setups where each generated page has a different `page-id` baked into the HTML
5
+ Reads the `page-id` HTML attribute from the document and notifies subscribers using a dedicated O(1) channel signal. Designed for SSG/SSR setups where each generated page has a different `page-id` baked into the HTML.
6
6
 
7
7
  ---
8
8
 
9
9
  ## Why `@alwatr/page-ready`?
10
10
 
11
- In SSG/SSR apps, the server renders a different HTML page for each route. Each page has a `page-id` attribute on `<body>`. Instead of a full client-side router, you just need to know _which page is currently loaded_ so you can run page-specific initialization logic.
11
+ In SSG/SSR apps, the server renders a different HTML page for each route. Each page has a `page-id` attribute somewhere in the document. Instead of a full client-side router, you just need to know _which page is currently loaded_ so you can run page-specific initialization logic.
12
12
 
13
13
  `@alwatr/page-ready` solves exactly this — nothing more.
14
14
 
@@ -29,7 +29,7 @@ npm i @alwatr/page-ready
29
29
  ### 1. Add `page-id` to your HTML
30
30
 
31
31
  ```html
32
- <!-- Each SSG-generated page has a unique page-id -->
32
+ <!-- Each SSG-generated page has a unique page-id — can be on any element -->
33
33
  <body page-id="home">
34
34
 
35
35
  </body>
@@ -38,15 +38,22 @@ npm i @alwatr/page-ready
38
38
  ### 2. Subscribe and dispatch
39
39
 
40
40
  ```ts
41
- import {onPageReady, dispatchPageReady} from '@alwatr/page-ready';
41
+ import {onPageReady, subscribePageReady, dispatchPageReady} from '@alwatr/page-ready';
42
42
 
43
43
  // Optionally constrain valid page IDs with a type alias
44
44
  type PageId = 'home' | 'about' | 'product-detail';
45
45
 
46
+ // Subscribe to a specific page
46
47
  onPageReady<PageId>('home', () => initHomePage());
47
48
  onPageReady<PageId>('about', () => initAboutPage());
48
49
 
49
- // Call once at bootstrap finds [page-id] in the document and notifies the matching handler
50
+ // Or subscribe to ALL pages and receive the page ID in the handler
51
+ subscribePageReady<PageId>((pageId) => {
52
+ console.log('Page ready:', pageId);
53
+ analytics.trackPageView(pageId);
54
+ });
55
+
56
+ // Call once at bootstrap — finds [page-id] anywhere in the document and notifies subscribers
50
57
  dispatchPageReady();
51
58
  ```
52
59
 
@@ -61,11 +68,13 @@ dispatchPageReady()
61
68
 
62
69
  └─ pageReadyChannel_.dispatch('home')
63
70
 
64
- └─ Map.get('home') → O(1) → invoke only 'home' handlers
71
+ ├─ Map.get('home') → O(1) → invoke only 'home' handlers (onPageReady)
72
+ └─ global subscribers → invoked for every page ID (subscribePageReady)
65
73
  ```
66
74
 
67
- - `dispatchPageReady` finds the element automatically — no argument needed.
68
- - `ChannelSignal` routes in O(1): only handlers for the current page ID are invoked.
75
+ - `dispatchPageReady` scans the entire document for the first `[page-id]` element — no argument needed.
76
+ - `ChannelSignal` routes in O(1): `onPageReady` handlers for non-matching page IDs are never invoked.
77
+ - `subscribePageReady` handlers receive the dispatched page ID and are called on every dispatch.
69
78
 
70
79
  ---
71
80
 
@@ -73,10 +82,10 @@ dispatchPageReady()
73
82
 
74
83
  ### `onPageReady(pageId, handler)`
75
84
 
76
- Subscribes to a specific page becoming ready.
85
+ Subscribes to a **specific** page becoming ready. The handler is only invoked when the dispatched page ID matches `pageId`.
77
86
 
78
87
  ```ts
79
- function onPageReady<T extends string>(pageId: T, handler: () => void): SubscribeResult;
88
+ function onPageReady<T extends string>(pageId: T, handler: () => Awaitable<void>): SubscribeResult;
80
89
  ```
81
90
 
82
91
  Pass a string literal union as the generic to constrain valid page IDs:
@@ -90,15 +99,71 @@ sub.unsubscribe(); // stop listening when no longer needed
90
99
 
91
100
  ---
92
101
 
102
+ ### `subscribePageReady(handler)`
103
+
104
+ Subscribes to **all** page-ready events. The handler is invoked on every `dispatchPageReady()` call, regardless of which page ID is dispatched. The current page ID is passed as the first argument.
105
+
106
+ ```ts
107
+ function subscribePageReady<T extends string>(handler: (pageId: T) => Awaitable<void>): SubscribeResult;
108
+ ```
109
+
110
+ Useful for cross-cutting concerns like analytics, logging, or layout transitions that need to react to every page change:
111
+
112
+ ```ts
113
+ type PageId = 'home' | 'about' | 'product-detail';
114
+
115
+ const sub = subscribePageReady<PageId>((pageId) => {
116
+ analytics.trackPageView(pageId);
117
+ updateActiveNavLink(pageId);
118
+ });
119
+
120
+ sub.unsubscribe(); // stop listening when no longer needed
121
+ ```
122
+
123
+ ---
124
+
93
125
  ### `dispatchPageReady()`
94
126
 
95
- Finds the first `[page-id]` element in the document and notifies matching subscribers.
127
+ Finds the first `[page-id]` element anywhere in the document and notifies all matching subscribers.
96
128
 
97
129
  ```ts
98
130
  function dispatchPageReady(): void;
99
131
  ```
100
132
 
101
- Call once at application bootstrap. Logs an accident if no `[page-id]` element is found.
133
+ Call once at application bootstrap. Logs an accident if no `[page-id]` element is found in the document. An empty attribute value (`page-id=""`) is treated as a valid identifier and dispatched normally.
134
+
135
+ The lookup uses `document.querySelector('[page-id]')`, so the attribute can be placed on any element (`<body>`, `<main>`, `<div>`, etc.).
136
+
137
+ ---
138
+
139
+ ## Choosing Between `onPageReady` and `subscribePageReady`
140
+
141
+ | Use case | API |
142
+ | ------------------------------------------- | -------------------- |
143
+ | Initialize logic for one specific page | `onPageReady` |
144
+ | React to every page change (analytics, nav) | `subscribePageReady` |
145
+ | Multiple pages, each with their own init | `onPageReady` × N |
146
+ | Single handler that branches on the page ID | `subscribePageReady` |
147
+
148
+ ---
149
+
150
+ ---
151
+
152
+ ## 🌊 Part of Alwatr Flux
153
+
154
+ `@alwatr/page-ready` is the **Routing 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.
155
+
156
+ In the Flux architecture, `@alwatr/page-ready` handles **page identity** for Multi-Page Applications (MPA). It reads the `page-id` attribute from the HTML and notifies the application which page is currently active — enabling page-specific initialization without a full client-side router.
157
+
158
+ ```typescript
159
+ // Use @alwatr/flux for the complete architecture
160
+ import {onPageReady, subscribePageReady, dispatchPageReady, setupActionDelegation} from '@alwatr/flux';
161
+
162
+ // Or use @alwatr/page-ready standalone
163
+ import {onPageReady, subscribePageReady, dispatchPageReady} from '@alwatr/page-ready';
164
+ ```
165
+
166
+ → [View the complete Flux documentation](https://github.com/Alwatr/alwatr/tree/next/pkg/flux)
102
167
 
103
168
  ---
104
169
 
package/dist/main.d.ts CHANGED
@@ -1,30 +1,35 @@
1
1
  /**
2
2
  * @alwatr/page-ready — Lightweight page identity signal for MPA routing.
3
3
  *
4
- * Reads the `page-id` attribute from `document.body` and dispatches a named
5
- * signal so any part of the application can react to the current page without
6
- * coupling to a full router.
4
+ * Reads the `page-id` attribute from the first matching element in the document
5
+ * and dispatches a named signal so any part of the application can react to the
6
+ * current page without coupling to a full router.
7
7
  *
8
8
  * ## Usage
9
9
  *
10
10
  * ```html
11
+ * <!-- page-id can be placed on any element, not just <body> -->
11
12
  * <body page-id="home">…</body>
12
13
  * ```
13
14
  *
14
15
  * ```ts
15
- * import {onPageReady, dispatchPageReady} from '@alwatr/page-ready';
16
+ * import {onPageReady, subscribePageReady, dispatchPageReady} from '@alwatr/page-ready';
16
17
  *
17
- * // Subscribe before dispatching so the handler is registered in time.
18
+ * // Subscribe to a specific page before dispatching.
18
19
  * onPageReady('home', () => initHomePage());
19
20
  *
20
- * // Call once at bootstrapreads document.body[page-id] and notifies subscribers.
21
+ * // Or subscribe to ALL pages handler receives the page ID.
22
+ * subscribePageReady((pageId) => analytics.trackPageView(pageId));
23
+ *
24
+ * // Call once at bootstrap — finds [page-id] anywhere in the document and notifies subscribers.
21
25
  * dispatchPageReady();
22
26
  * ```
23
27
  *
24
28
  * ## Public API
25
29
  *
26
- * - `onPageReady(pageId, handler)` — subscribe to a specific page becoming ready
27
- * - `dispatchPageReady(element?)` — read `page-id` attribute and notify subscribers
30
+ * - `onPageReady(pageId, handler)` — subscribe to a **specific** page becoming ready
31
+ * - `subscribePageReady(handler)` — subscribe to **all** page-ready events; handler receives the page ID
32
+ * - `dispatchPageReady()` — find `[page-id]` via `querySelector` and notify subscribers
28
33
  */
29
34
  export * from './page-ready.js';
30
35
  //# sourceMappingURL=main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,cAAc,iBAAiB,CAAC"}
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
- /* 📦 @alwatr/page-ready v9.14.0 */
2
- import{createLogger as i}from"@alwatr/logger";import{createChannelSignal as o}from"@alwatr/signal";var s=i("page-ready"),c=o({name:"page-ready"});function r(b,u){return s.logMethodArgs?.("onPageReady",{pageId:b}),c.on(b,u)}function A(){s.logMethod?.("dispatchPageReady");let b=document.querySelector("[page-id]")?.getAttribute("page-id")?.trim();if(!b){s.accident("dispatchPageReady","page_id_not_found");return}c.dispatch(b)}export{r as onPageReady,A as dispatchPageReady};
1
+ /* 📦 @alwatr/page-ready v9.20.0 */
2
+ import{createLogger as o}from"@alwatr/logger";import{createChannelSignal as c}from"@alwatr/signal";var a=o("page-ready"),i=c({name:"page-ready"});function d(e,t){return a.logMethodArgs?.("onPageReady",{pageId:e}),i.on(e,t)}function b(e){return a.logMethod?.("subscribePageReady"),i.subscribe((t)=>{e(t.name)})}function n(){a.logMethod?.("dispatchPageReady");let e=document.querySelector("[page-id]")?.getAttribute("page-id")?.trim();if(e==null){a.accident("dispatchPageReady","page_id_not_found");return}i.dispatch(e)}export{b as subscribePageReady,d as onPageReady,n as dispatchPageReady};
3
3
 
4
- //# debugId=820E80ECFCFB3C2164756E2164756E21
4
+ //# debugId=192C1C82BA0DF88F64756E2164756E21
5
5
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/page-ready.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * @file page-ready.ts\n *\n * Lightweight page identity signal for MPA routing.\n *\n * ## Design\n *\n * Uses a dedicated `ChannelSignal` keyed by page identifier. This gives O(1)\n * dispatch — only the handler(s) registered for the current page ID are invoked,\n * regardless of how many pages are declared in the application.\n *\n * The signal is intentionally separate from `@alwatr/action`'s action bus:\n * page identity is a routing/lifecycle concern, not a user-interaction action.\n *\n * ## Attribute convention\n *\n * Place `page-id` anywhere in the document — `dispatchPageReady` finds it\n * automatically via `querySelector('[page-id]')`:\n *\n * ```html\n * <body page-id=\"home\">…</body>\n * ```\n *\n * In SSG/SSR setups each generated page has a different `page-id` value baked\n * into the HTML, so `dispatchPageReady()` always reads the correct page without\n * any runtime routing logic.\n */\n\nimport type {Awaitable} from '@alwatr/type-helper';\nimport {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\nimport type {SubscribeResult} from '@alwatr/signal';\n\nconst logger = createLogger('page-ready');\n\n/**\n * Internal channel keyed by page identifier.\n *\n * O(1) dispatch: routes directly to the handler set for the dispatched key,\n * never invoking handlers for other page IDs.\n *\n * @internal\n */\nconst pageReadyChannel_ = createChannelSignal<Record<string, void>>({name: 'page-ready'});\n\n/**\n * Subscribes to a specific page becoming ready.\n *\n * The handler is invoked when `dispatchPageReady()` is called and the\n * `page-id` attribute in the document matches `pageId`.\n *\n * Pass a string literal union as the generic parameter to constrain which\n * page IDs are valid across your application:\n *\n * ```ts\n * type PageId = 'home' | 'about' | 'product-detail';\n *\n * onPageReady<PageId>('home', () => initHomePage());\n * ```\n *\n * @param pageId - The page identifier to listen for (must match the `page-id` attribute value).\n * @param handler - Called with no arguments when the page is ready.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onPageReady} from '@alwatr/page-ready';\n *\n * const sub = onPageReady('home', () => initHomePage());\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onPageReady<T extends string>(pageId: T, handler: () => Awaitable<void>): SubscribeResult {\n logger.logMethodArgs?.('onPageReady', {pageId});\n return pageReadyChannel_.on(pageId, handler);\n}\n\n/**\n * Reads the `page-id` attribute from the first matching element in the document\n * and notifies all `onPageReady` subscribers registered for that page identifier.\n *\n * Finds the element via `document.querySelector('[page-id]')` — no argument\n * needed. Call once at application bootstrap after the DOM is ready.\n *\n * If no element with `page-id` is found, or the attribute value is empty,\n * an accident is logged and nothing is dispatched.\n *\n * @example\n * ```html\n * <body page-id=\"home\">…</body>\n * ```\n * ```ts\n * import {onPageReady, dispatchPageReady} from '@alwatr/page-ready';\n *\n * onPageReady('home', () => initHomePage());\n * dispatchPageReady();\n * ```\n */\nexport function dispatchPageReady(): void {\n logger.logMethod?.('dispatchPageReady');\n\n const pageId = document.querySelector('[page-id]')?.getAttribute('page-id')?.trim();\n\n if (!pageId) {\n logger.accident('dispatchPageReady', 'page_id_not_found');\n return;\n }\n\n pageReadyChannel_.dispatch(pageId);\n}\n"
5
+ "/**\n * @file page-ready.ts\n *\n * Lightweight page identity signal for MPA routing.\n *\n * ## Design\n *\n * Uses a dedicated `ChannelSignal` keyed by page identifier. This gives O(1)\n * dispatch — only the handler(s) registered for the current page ID are invoked,\n * regardless of how many pages are declared in the application.\n *\n * The signal is intentionally separate from `@alwatr/action`'s action bus:\n * page identity is a routing/lifecycle concern, not a user-interaction action.\n *\n * ## Attribute convention\n *\n * Place `page-id` anywhere in the document — `dispatchPageReady` finds it\n * automatically via `querySelector('[page-id]')`:\n *\n * ```html\n * <body page-id=\"home\">…</body>\n * ```\n *\n * In SSG/SSR setups each generated page has a different `page-id` value baked\n * into the HTML, so `dispatchPageReady()` always reads the correct page without\n * any runtime routing logic.\n */\n\nimport type {Awaitable} from '@alwatr/type-helper';\nimport {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\nimport type {SubscribeResult} from '@alwatr/signal';\n\nconst logger = createLogger('page-ready');\n\n/**\n * Internal channel keyed by page identifier.\n *\n * O(1) dispatch: routes directly to the handler set for the dispatched key,\n * never invoking handlers for other page IDs.\n *\n * @internal\n */\nconst pageReadyChannel_ = createChannelSignal<Record<string, void>>({name: 'page-ready'});\n\n/**\n * Subscribes to a specific page becoming ready.\n *\n * The handler is invoked when `dispatchPageReady()` is called and the\n * `page-id` attribute in the document matches `pageId`.\n *\n * Pass a string literal union as the generic parameter to constrain which\n * page IDs are valid across your application:\n *\n * ```ts\n * type PageId = 'home' | 'about' | 'product-detail';\n *\n * onPageReady<PageId>('home', () => initHomePage());\n * ```\n *\n * @param pageId - The page identifier to listen for (must match the `page-id` attribute value).\n * @param handler - Called with no arguments when the page is ready.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onPageReady} from '@alwatr/page-ready';\n *\n * const sub = onPageReady('home', () => initHomePage());\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onPageReady<T extends string>(pageId: T, handler: () => Awaitable<void>): SubscribeResult {\n logger.logMethodArgs?.('onPageReady', {pageId});\n return pageReadyChannel_.on(pageId, handler);\n}\n\n/**\n * Subscribes to **all** page-ready events, regardless of which page ID is dispatched.\n *\n * Unlike `onPageReady` — which targets a single page ID — this handler is invoked\n * every time `dispatchPageReady()` fires, and receives the dispatched page ID as\n * its first argument.\n *\n * Useful for cross-cutting concerns that must react to every page change:\n * analytics tracking, active nav-link updates, layout transitions, etc.\n *\n * Pass a string literal union as the generic parameter to get type-safe page IDs:\n *\n * ```ts\n * type PageId = 'home' | 'about' | 'product-detail';\n *\n * subscribePageReady<PageId>((pageId) => {\n * analytics.trackPageView(pageId);\n * updateActiveNavLink(pageId);\n * });\n * ```\n *\n * @param handler - Called with the dispatched page ID on every `dispatchPageReady()` call.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {subscribePageReady} from '@alwatr/page-ready';\n *\n * const sub = subscribePageReady((pageId) => console.log('Page ready:', pageId));\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function subscribePageReady<T extends string>(handler: (pageId: T) => Awaitable<void>): SubscribeResult {\n logger.logMethod?.('subscribePageReady');\n return pageReadyChannel_.subscribe((message) => {\n handler(message.name as T);\n });\n}\n\n/**\n * Reads the `page-id` attribute from the first matching element in the document\n * and notifies all `onPageReady` subscribers registered for that page identifier.\n *\n * Finds the element via `document.querySelector('[page-id]')` — no argument\n * needed. Call once at application bootstrap after the DOM is ready.\n *\n * If no element with `page-id` is found in the document, an accident is logged\n * and nothing is dispatched. An empty attribute value (`page-id=\"\"`) is treated\n * as a valid identifier and will be dispatched normally.\n *\n * @example\n * ```html\n * <body page-id=\"home\">…</body>\n * ```\n * ```ts\n * import {onPageReady, dispatchPageReady} from '@alwatr/page-ready';\n *\n * onPageReady('home', () => initHomePage());\n * dispatchPageReady();\n * ```\n */\nexport function dispatchPageReady(): void {\n logger.logMethod?.('dispatchPageReady');\n\n const pageId = document.querySelector('[page-id]')?.getAttribute('page-id')?.trim();\n\n if (pageId == null) {\n logger.accident('dispatchPageReady', 'page_id_not_found');\n return;\n }\n\n // An empty string is a valid page identifier — dispatch as-is.\n pageReadyChannel_.dispatch(pageId);\n}\n"
6
6
  ],
7
- "mappings": ";AA6BA,uBAAQ,uBACR,8BAAQ,uBAGR,IAAM,EAAS,EAAa,YAAY,EAUlC,EAAoB,EAA0C,CAAC,KAAM,YAAY,CAAC,EA6BjF,SAAS,CAA6B,CAAC,EAAW,EAAiD,CAExG,OADA,EAAO,gBAAgB,cAAe,CAAC,QAAM,CAAC,EACvC,EAAkB,GAAG,EAAQ,CAAO,EAwBtC,SAAS,CAAiB,EAAS,CACxC,EAAO,YAAY,mBAAmB,EAEtC,IAAM,EAAS,SAAS,cAAc,WAAW,GAAG,aAAa,SAAS,GAAG,KAAK,EAElF,GAAI,CAAC,EAAQ,CACX,EAAO,SAAS,oBAAqB,mBAAmB,EACxD,OAGF,EAAkB,SAAS,CAAM",
8
- "debugId": "820E80ECFCFB3C2164756E2164756E21",
7
+ "mappings": ";AA6BA,uBAAQ,uBACR,8BAAQ,uBAGR,IAAM,EAAS,EAAa,YAAY,EAUlC,EAAoB,EAA0C,CAAC,KAAM,YAAY,CAAC,EA6BjF,SAAS,CAA6B,CAAC,EAAW,EAAiD,CAExG,OADA,EAAO,gBAAgB,cAAe,CAAC,QAAM,CAAC,EACvC,EAAkB,GAAG,EAAQ,CAAO,EAmCtC,SAAS,CAAoC,CAAC,EAA0D,CAE7G,OADA,EAAO,YAAY,oBAAoB,EAChC,EAAkB,UAAU,CAAC,IAAY,CAC9C,EAAQ,EAAQ,IAAS,EAC1B,EAyBI,SAAS,CAAiB,EAAS,CACxC,EAAO,YAAY,mBAAmB,EAEtC,IAAM,EAAS,SAAS,cAAc,WAAW,GAAG,aAAa,SAAS,GAAG,KAAK,EAElF,GAAI,GAAU,KAAM,CAClB,EAAO,SAAS,oBAAqB,mBAAmB,EACxD,OAIF,EAAkB,SAAS,CAAM",
8
+ "debugId": "192C1C82BA0DF88F64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -55,6 +55,39 @@ import type { SubscribeResult } from '@alwatr/signal';
55
55
  * ```
56
56
  */
57
57
  export declare function onPageReady<T extends string>(pageId: T, handler: () => Awaitable<void>): SubscribeResult;
58
+ /**
59
+ * Subscribes to **all** page-ready events, regardless of which page ID is dispatched.
60
+ *
61
+ * Unlike `onPageReady` — which targets a single page ID — this handler is invoked
62
+ * every time `dispatchPageReady()` fires, and receives the dispatched page ID as
63
+ * its first argument.
64
+ *
65
+ * Useful for cross-cutting concerns that must react to every page change:
66
+ * analytics tracking, active nav-link updates, layout transitions, etc.
67
+ *
68
+ * Pass a string literal union as the generic parameter to get type-safe page IDs:
69
+ *
70
+ * ```ts
71
+ * type PageId = 'home' | 'about' | 'product-detail';
72
+ *
73
+ * subscribePageReady<PageId>((pageId) => {
74
+ * analytics.trackPageView(pageId);
75
+ * updateActiveNavLink(pageId);
76
+ * });
77
+ * ```
78
+ *
79
+ * @param handler - Called with the dispatched page ID on every `dispatchPageReady()` call.
80
+ * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import {subscribePageReady} from '@alwatr/page-ready';
85
+ *
86
+ * const sub = subscribePageReady((pageId) => console.log('Page ready:', pageId));
87
+ * sub.unsubscribe(); // stop listening when no longer needed
88
+ * ```
89
+ */
90
+ export declare function subscribePageReady<T extends string>(handler: (pageId: T) => Awaitable<void>): SubscribeResult;
58
91
  /**
59
92
  * Reads the `page-id` attribute from the first matching element in the document
60
93
  * and notifies all `onPageReady` subscribers registered for that page identifier.
@@ -62,8 +95,9 @@ export declare function onPageReady<T extends string>(pageId: T, handler: () =>
62
95
  * Finds the element via `document.querySelector('[page-id]')` — no argument
63
96
  * needed. Call once at application bootstrap after the DOM is ready.
64
97
  *
65
- * If no element with `page-id` is found, or the attribute value is empty,
66
- * an accident is logged and nothing is dispatched.
98
+ * If no element with `page-id` is found in the document, an accident is logged
99
+ * and nothing is dispatched. An empty attribute value (`page-id=""`) is treated
100
+ * as a valid identifier and will be dispatched normally.
67
101
  *
68
102
  * @example
69
103
  * ```html
@@ -1 +1 @@
1
- {"version":3,"file":"page-ready.d.ts","sourceRoot":"","sources":["../src/page-ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAGnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAcpD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,eAAe,CAGxG;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAWxC"}
1
+ {"version":3,"file":"page-ready.d.ts","sourceRoot":"","sources":["../src/page-ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,qBAAqB,CAAC;AAGnD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,gBAAgB,CAAC;AAcpD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,eAAe,CAGxG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,GAAG,eAAe,CAK7G;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,IAAI,IAAI,CAYxC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/page-ready",
3
- "version": "9.14.0",
3
+ "version": "9.20.0",
4
4
  "description": "Lightweight page identity signal for MPA routing — reads page-id attribute and notifies subscribers.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,13 +21,14 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/logger": "9.14.0",
25
- "@alwatr/signal": "9.14.0"
24
+ "@alwatr/logger": "9.16.0",
25
+ "@alwatr/signal": "9.20.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@alwatr/nano-build": "9.14.0",
29
- "@alwatr/standard": "9.14.0",
29
+ "@alwatr/standard": "9.16.0",
30
30
  "@alwatr/type-helper": "9.14.0",
31
+ "@happy-dom/global-registrator": "^20.9.0",
31
32
  "typescript": "^6.0.3"
32
33
  },
33
34
  "scripts": {
@@ -71,5 +72,5 @@
71
72
  "mpa",
72
73
  "typescript"
73
74
  ],
74
- "gitHead": "4e499b23191d4460ea60f34cde8a99b472741f1a"
75
+ "gitHead": "6e47fd80a2da33bb78e12b1d51258f11e2caec72"
75
76
  }
package/src/main.ts CHANGED
@@ -1,29 +1,34 @@
1
1
  /**
2
2
  * @alwatr/page-ready — Lightweight page identity signal for MPA routing.
3
3
  *
4
- * Reads the `page-id` attribute from `document.body` and dispatches a named
5
- * signal so any part of the application can react to the current page without
6
- * coupling to a full router.
4
+ * Reads the `page-id` attribute from the first matching element in the document
5
+ * and dispatches a named signal so any part of the application can react to the
6
+ * current page without coupling to a full router.
7
7
  *
8
8
  * ## Usage
9
9
  *
10
10
  * ```html
11
+ * <!-- page-id can be placed on any element, not just <body> -->
11
12
  * <body page-id="home">…</body>
12
13
  * ```
13
14
  *
14
15
  * ```ts
15
- * import {onPageReady, dispatchPageReady} from '@alwatr/page-ready';
16
+ * import {onPageReady, subscribePageReady, dispatchPageReady} from '@alwatr/page-ready';
16
17
  *
17
- * // Subscribe before dispatching so the handler is registered in time.
18
+ * // Subscribe to a specific page before dispatching.
18
19
  * onPageReady('home', () => initHomePage());
19
20
  *
20
- * // Call once at bootstrapreads document.body[page-id] and notifies subscribers.
21
+ * // Or subscribe to ALL pages handler receives the page ID.
22
+ * subscribePageReady((pageId) => analytics.trackPageView(pageId));
23
+ *
24
+ * // Call once at bootstrap — finds [page-id] anywhere in the document and notifies subscribers.
21
25
  * dispatchPageReady();
22
26
  * ```
23
27
  *
24
28
  * ## Public API
25
29
  *
26
- * - `onPageReady(pageId, handler)` — subscribe to a specific page becoming ready
27
- * - `dispatchPageReady(element?)` — read `page-id` attribute and notify subscribers
30
+ * - `onPageReady(pageId, handler)` — subscribe to a **specific** page becoming ready
31
+ * - `subscribePageReady(handler)` — subscribe to **all** page-ready events; handler receives the page ID
32
+ * - `dispatchPageReady()` — find `[page-id]` via `querySelector` and notify subscribers
28
33
  */
29
34
  export * from './page-ready.js';
package/src/page-ready.ts CHANGED
@@ -75,6 +75,45 @@ export function onPageReady<T extends string>(pageId: T, handler: () => Awaitabl
75
75
  return pageReadyChannel_.on(pageId, handler);
76
76
  }
77
77
 
78
+ /**
79
+ * Subscribes to **all** page-ready events, regardless of which page ID is dispatched.
80
+ *
81
+ * Unlike `onPageReady` — which targets a single page ID — this handler is invoked
82
+ * every time `dispatchPageReady()` fires, and receives the dispatched page ID as
83
+ * its first argument.
84
+ *
85
+ * Useful for cross-cutting concerns that must react to every page change:
86
+ * analytics tracking, active nav-link updates, layout transitions, etc.
87
+ *
88
+ * Pass a string literal union as the generic parameter to get type-safe page IDs:
89
+ *
90
+ * ```ts
91
+ * type PageId = 'home' | 'about' | 'product-detail';
92
+ *
93
+ * subscribePageReady<PageId>((pageId) => {
94
+ * analytics.trackPageView(pageId);
95
+ * updateActiveNavLink(pageId);
96
+ * });
97
+ * ```
98
+ *
99
+ * @param handler - Called with the dispatched page ID on every `dispatchPageReady()` call.
100
+ * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * import {subscribePageReady} from '@alwatr/page-ready';
105
+ *
106
+ * const sub = subscribePageReady((pageId) => console.log('Page ready:', pageId));
107
+ * sub.unsubscribe(); // stop listening when no longer needed
108
+ * ```
109
+ */
110
+ export function subscribePageReady<T extends string>(handler: (pageId: T) => Awaitable<void>): SubscribeResult {
111
+ logger.logMethod?.('subscribePageReady');
112
+ return pageReadyChannel_.subscribe((message) => {
113
+ handler(message.name as T);
114
+ });
115
+ }
116
+
78
117
  /**
79
118
  * Reads the `page-id` attribute from the first matching element in the document
80
119
  * and notifies all `onPageReady` subscribers registered for that page identifier.
@@ -82,8 +121,9 @@ export function onPageReady<T extends string>(pageId: T, handler: () => Awaitabl
82
121
  * Finds the element via `document.querySelector('[page-id]')` — no argument
83
122
  * needed. Call once at application bootstrap after the DOM is ready.
84
123
  *
85
- * If no element with `page-id` is found, or the attribute value is empty,
86
- * an accident is logged and nothing is dispatched.
124
+ * If no element with `page-id` is found in the document, an accident is logged
125
+ * and nothing is dispatched. An empty attribute value (`page-id=""`) is treated
126
+ * as a valid identifier and will be dispatched normally.
87
127
  *
88
128
  * @example
89
129
  * ```html
@@ -101,10 +141,11 @@ export function dispatchPageReady(): void {
101
141
 
102
142
  const pageId = document.querySelector('[page-id]')?.getAttribute('page-id')?.trim();
103
143
 
104
- if (!pageId) {
144
+ if (pageId == null) {
105
145
  logger.accident('dispatchPageReady', 'page_id_not_found');
106
146
  return;
107
147
  }
108
148
 
149
+ // An empty string is a valid page identifier — dispatch as-is.
109
150
  pageReadyChannel_.dispatch(pageId);
110
151
  }