white-label-router 1.1.1 → 4.0.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
@@ -1,299 +1,246 @@
1
1
  # white-label-router
2
2
 
3
- A simple ES6 JS router based on the history api and pushstate.
3
+ `white-label-router` is a small client-side router built on the browser History API. It maps URL prefixes to functions or view lifecycle objects, intercepts links marked with `data-pushstate`, handles browser back/forward navigation, and can receive navigation requests through a mediator.
4
4
 
5
- Learn more about ES6 classes here:
5
+ ## Requirements
6
6
 
7
- https://babeljs.io/docs/learn-es2015/
7
+ - Node.js `^22.18.0` or `>=24.11.0` for installation and development
8
+ - A browser environment with `window.history`, `window.location`, and standard DOM events at runtime
9
+ - Server fallback configuration that serves the application entry page for client-managed URLs
8
10
 
9
- ## Install
11
+ ## Install and import
10
12
 
11
- Install the node module:
12
-
13
- ```
14
- npm install white-label-router --save
13
+ ```sh
14
+ npm install white-label-router
15
15
  ```
16
16
 
17
- ## Import
18
-
19
- ```
17
+ ```js
20
18
  import Router from 'white-label-router';
21
19
  ```
22
20
 
21
+ ## Complete example
23
22
 
24
- ## Defining Routes
25
-
26
- The most simple way simply executes any function you define:
23
+ ```js
24
+ import Router from 'white-label-router';
27
25
 
28
- ```
29
- const MyRoute = class extends Router {
26
+ class ApplicationRouter extends Router {
30
27
  constructor() {
31
28
  super();
32
- this.routes = {
33
- defaultRoute: (locationData) => {
34
- //here you would put any view specific logic for the defaultRoute
35
- window.console.log('the defaultRoute executed');
36
- },
37
- '/page2': (locationData) => {
38
- //here you would put any view specific logic for the page2 route
39
- window.console.log('the page2 route executed');
40
- }
41
- };
42
- }
43
- };
44
- ```
45
-
46
- or you can also define initialize and destroy functions in your view which will be automatically called as the user navigates your application:
47
29
 
48
- ```
49
- const MyRoute = class extends Router {
50
- constructor() {
51
- super();
30
+ this.scope = document.querySelector('main');
52
31
  this.routes = {
53
- defaultRoute: {
54
- view: {
55
- initialize: (locationData) => {
56
- window.console.log('the defaultRoute initialized');
57
- },
58
- destroy: (locationData) => {
59
- window.console.log('the defaultRoute has been destroyed');
60
- }
61
- }
32
+ '/products': (scope, location) => {
33
+ scope.textContent = `Product: ${location.data.url[0] || 'all'}`;
62
34
  },
63
- '/page2': {
35
+ '/account': {
36
+ title: 'Your account',
37
+ secure: () => Boolean(window.currentUser),
64
38
  view: {
65
- initialize: (locationData) => {
66
- window.console.log('the page2 initialized');
39
+ initialize: (scope, location) => {
40
+ scope.textContent = 'Account';
41
+ console.log(location.data.query);
67
42
  },
68
- destroy: (locationData) => {
69
- window.console.log('the page2 has been destroyed');
43
+ destroy: () => {
44
+ console.log('Leaving account');
70
45
  }
71
46
  }
47
+ },
48
+ defaultRoute: () => {
49
+ console.log('No configured route matched.');
72
50
  }
73
51
  };
74
52
  }
75
- };
53
+ }
54
+
55
+ const router = new ApplicationRouter();
56
+ router.initialize();
76
57
  ```
77
58
 
78
- You have some additional options when using the view object approach. They include the ability to set the page title for a route and also do security check before a route's view is initialized at the router level:
59
+ At startup, `initialize()` reads the current path and query string, registers document click and browser `popstate` listeners, and navigates to the matching route.
60
+
61
+ ## Define routes
79
62
 
63
+ A route may be a function:
64
+
65
+ ```js
66
+ this.routes = {
67
+ '/help': (scope, location) => {
68
+ scope.textContent = `Help page: ${location.url}`;
69
+ }
70
+ };
80
71
  ```
81
- const MyRoute = class extends Router {
82
- constructor() {
83
- super();
84
- this.routes = {
85
- defaultRoute: {
86
- title: 'Home',
87
- view: {
88
- initialize: (locationData) => {
89
- window.console.log('the defaultRoute initialized');
90
- },
91
- destroy: (locationData) => {
92
- window.console.log('the defaultRoute has been destroyed');
93
- }
94
- }
72
+
73
+ Or it may describe a title, security check, and view lifecycle:
74
+
75
+ ```js
76
+ this.routes = {
77
+ '/orders': {
78
+ title: 'Orders',
79
+ secure(scope, location) {
80
+ return Boolean(window.currentUser);
81
+ },
82
+ view: {
83
+ initialize(scope, location) {
84
+ scope.textContent = 'Order history';
95
85
  },
96
- '/page2': {
97
- title: 'Page 2',
98
- view: {
99
- initialize: (locationData) => {
100
- window.console.log('the page2 initialized');
101
- },
102
- destroy: (locationData) => {
103
- window.console.log('the page2 has been destroyed');
104
- }
105
- },
106
- secure: this.isSecure
86
+ destroy(scope, nextLocation) {
87
+ scope.replaceChildren();
107
88
  }
108
- };
109
- }
110
- isSecure (locationData) => {
111
- if (someCookieIsValid) {
112
- return true;
113
- } else {
114
- return false;
115
89
  }
116
90
  }
117
91
  };
118
92
  ```
119
93
 
120
- NOTE: routes are matched by checking if the beginning of the url path name string matches a route. For the example above the route '/page2' would be executed for both path names '/page2' and '/page23/232'.
94
+ The `secure` function must return exactly `true` to allow navigation. When navigation succeeds, the previous route's `destroy()` runs before the next route's `initialize()`.
121
95
 
122
- ## Instantiate
96
+ ### Route matching behavior
123
97
 
124
- ```
125
- const myRouter = new MyRouter();
98
+ Routes match complete path boundaries in object insertion order. For example, `/products` matches `/products` and `/products/42`, but it does not match `/products-old`. Put more specific routes before broader ones:
126
99
 
127
- myRouter.initialize();
100
+ ```js
101
+ this.routes = {
102
+ '/products/sale': saleRoute,
103
+ '/products': productsRoute,
104
+ defaultRoute
105
+ };
128
106
  ```
129
107
 
130
- ## HTML Linking To Routes
131
-
132
- you can make any anchor tigger a specified route by simply setting the href attribute to your route path and adding the attribute 'data-pushstate'.
108
+ If no prefix matches, `defaultRoute` is used when present.
133
109
 
134
- ```
135
- <a href="/page2" data-pushstate>take me to page2</a>
136
- ```
110
+ ## Navigate from HTML
137
111
 
138
- ## Navigate
112
+ The router delegates one document-level click listener. Add `data-pushstate` to an anchor to use client-side navigation:
139
113
 
140
- you can also trigger navigation to a route with the navigate method
141
-
142
- ```
143
- myRouter.navigate('/page2');
114
+ ```html
115
+ <a href="/products/42" data-pushstate>
116
+ <span>View product 42</span>
117
+ </a>
144
118
  ```
145
119
 
146
- ## Mediator
147
-
148
- Optionally you can use a mediator, such as White Label Mediator, with the router. To do so you would like this:
120
+ Clicks on nested elements such as the `span` are resolved to the enclosing anchor. Links without `data-pushstate` keep their normal browser behavior.
149
121
 
150
- ```
151
- const MyRoute = class extends Router {
152
- constructor() {
153
- super();
122
+ Modified clicks, non-left clicks, downloads, links targeting another browsing context, and cross-origin URLs also keep their normal browser behavior.
154
123
 
155
- this.mediator = myMediator;
124
+ ## Navigate from JavaScript
156
125
 
157
- this.routes = {
158
- defaultRoute: (locationData) => {
159
- //here you would put any view specific logic for the defaultRoute
160
- window.console.log('the defaultRoute executed');
161
- },
162
- '/page2': (locationData) => {
163
- //here you would put any view specific logic for the page2 route
164
- window.console.log('the page2 route executed');
165
- }
166
- };
167
-
168
- }
169
- };
170
- ```
171
-
172
- Then anywhere else in the application you want to trigger the router to navigate you could do so like this:
173
-
174
- ```
175
- myMediator.emit('router:navigate', {
176
- url: '/authentication',
177
- message: 'Session expired, please login again.'
126
+ ```js
127
+ router.navigate('/products/42', {
128
+ source: 'featured-products'
178
129
  });
179
130
  ```
180
131
 
181
- ## Passing Data
132
+ The second argument becomes `location.data.mediator`. Unless the call represents a browser `popstate`, successful navigation adds the URL to browser history.
182
133
 
183
- The router passes in a location data object to the route that is being executed. Here is an example object passed:
134
+ ## Location data
184
135
 
185
- ```
136
+ Every route receives the configured `scope` and a location object:
137
+
138
+ ```js
186
139
  {
187
- url: ['/user'],
140
+ url: '/products/42?color=blue',
188
141
  data: {
189
- url: [],
190
- mediator: {},
191
- query: {}
142
+ url: ['42'],
143
+ mediator: {
144
+ source: 'featured-products'
145
+ },
146
+ query: {
147
+ color: 'blue'
148
+ }
192
149
  }
193
150
  }
194
151
  ```
195
152
 
196
- 'data.url' is an array of values parsed from the url being requested. For Example if the url was '/user/fred' and we defined a route for '/user' then data object passed into the '/user' route would be the following:
153
+ - `url` is the complete route URL.
154
+ - `data.url` contains path segments after the matched route prefix.
155
+ - `data.mediator` contains the object passed to `navigate()` or received from the mediator.
156
+ - `data.query` contains parsed query values.
197
157
 
198
- ```
199
- {
200
- url: ['/user/fred'],
201
- data: {
202
- url: ['fred'],
203
- mediator: {},
204
- query: {}
205
- }
206
- }
207
- ```
158
+ Path segments and query values are URI-decoded with browser-native URL APIs. Application code must still validate values before using them.
208
159
 
209
- 'data.query' is an object of values parsed from the url query string. For Example if the url was '/user?name=fred' and we defined a route for '/user' then data object passed into the '/user' route would be the following:
160
+ ## Version 3 migration note
210
161
 
211
- ```
212
- {
213
- url: ['/user?name=fred'],
214
- data: {
215
- url: [],
216
- mediator: {},
217
- query: {
218
- name: 'fred'
219
- }
220
- }
221
- }
222
- ```
162
+ Version 3 changes route matching from unrestricted string prefixes to path boundaries. A route such as `/page2` no longer matches `/page23`. Define `/page23` explicitly if the application previously depended on that behavior.
223
163
 
224
- 'data.mediator' is an object of values parsed through the optional mediator. For Example if this was emitted through the mediator:
164
+ ## Mediator navigation
225
165
 
226
- ```
227
- myMediator.emit('router:navigate', {
228
- url: '/authentication',
229
- message: 'Session expired, please login again.'
166
+ Assign an EventEmitter-compatible mediator before initialization:
167
+
168
+ ```js
169
+ import Mediator from 'white-label-mediator';
170
+
171
+ const mediator = new Mediator();
172
+ const router = new ApplicationRouter();
173
+ router.mediator = mediator;
174
+ router.initialize();
175
+
176
+ mediator.emit('router:navigate', {
177
+ url: '/account',
178
+ reason: 'Session refreshed'
230
179
  });
231
180
  ```
232
181
 
233
- Then the location data passed in to the '/authentication' route would be the following:
182
+ The complete emitted object is passed to the route as `location.data.mediator`. `destroy()` removes the document, window, and mediator listeners. Repeated listener initialization is idempotent and does not add duplicate listeners.
234
183
 
235
- ```
236
- {
237
- url: ['/authentication'],
238
- data: {
239
- url: [],
240
- mediator: {
241
- url: '/authentication',
242
- message: 'Session expired, please login again.'
243
- }
244
- }
245
- }
246
- ```
184
+ ## Lifecycle
247
185
 
248
- ## Let's look at an example:
186
+ | Method | Behavior |
187
+ | --- | --- |
188
+ | `initialize()` | Reads the browser URL, adds listeners, and runs the initial route. |
189
+ | `navigate(url, data, isPopState)` | Matches and runs a route, updates history, and returns the router; returns `false` when blocked or invalid. |
190
+ | `addListeners()` | Adds browser and optional mediator listeners once. |
191
+ | `removeListeners()` | Removes listeners previously added by this router. |
192
+ | `destroy()` | Calls `removeListeners()` and returns the router. |
249
193
 
250
- import the module
194
+ Call `destroy()` when the router is no longer used:
251
195
 
252
- ```
253
- import Router from 'white-label-router';
196
+ ```js
197
+ router.destroy();
254
198
  ```
255
199
 
256
- extend the router
200
+ ## Development
257
201
 
202
+ ```sh
203
+ npm ci
204
+ npm run build
205
+ npm run typecheck
206
+ npm test
207
+ npm run coverage
208
+ npm run audit
258
209
  ```
259
- const myRouter = class extends Router {
260
- // this is the constructor. This executed whenever the view is instantiated.
261
- constructor() {
262
- // always do this
263
- super();
264
- this.routes = {
265
- defaultRoute: (locationData) => {
266
- //here you would put any view specific logic for the defaultRoute
267
- window.console.log('the defaultRoute executed');
268
- },
269
- '/page2': (locationData) => {
270
- //here you would put any view specific logic for the page2 route
271
- window.console.log('the page2 route executed');
272
- }
273
- };
274
- }
275
- };
276
210
 
277
- ```
211
+ The npm package publishes the compiled `dist` file and this README.
278
212
 
279
- instantiate your router
213
+ ## TypeScript development and version 4.0.0 migration
280
214
 
281
- ```
282
- const myRouter = new MyRoute();
283
- ```
215
+ Implementation code now uses strict TypeScript. Builds emit JavaScript, source maps with embedded source, and `.d.ts` declarations into `dist`. JavaScript callers can still use the package without compiling TypeScript themselves. JSDoc comments describe parameters, return values, lifecycle behavior, and validation at the implementation, and are retained in declarations.
284
216
 
285
- initialize your router
217
+ ```ts
218
+ import Router from 'white-label-router';
286
219
 
287
- ```
288
- myRouter.initialize();
220
+ const router = new Router();
221
+ const greeting: Router.Route = (scope, location) => {
222
+ console.log(location.data.query.name); // /hello?name=Ada
223
+ };
224
+ router.routes['/hello'] = greeting;
225
+ router.initialize();
289
226
  ```
290
227
 
291
- navigate to '/page2'
228
+ `Router.Route` covers function routes and objects with a guard or view lifecycle. `Router.Location` describes decoded URL data, `Router.Navigation` describes mediator payloads, and `Router.Handler` describes a route callback. Browser DOM types are required when compiling a consuming application.
292
229
 
230
+ This is a major release because the distribution is now CommonJS emitted by TypeScript, replacing the previous UMD wrapper. CommonJS `require` and the documented ESM imports remain supported. Direct AMD loading or browser script tags that depended on UMD globals must migrate to a browser bundler. Edit `src/*.ts`, then run `npm run build`; do not edit generated `dist` files. The obsolete Babel build dependencies have been removed.
231
+
232
+ ### Verification and coverage
233
+
234
+ ```sh
235
+ npm ci --ignore-scripts
236
+ npm run typecheck
237
+ npm test
238
+ npm run coverage
239
+ npm pack --dry-run
293
240
  ```
294
- myRouter.navigate('/page2');
295
- ```
296
241
 
297
- In the example above we have set up two routes. The first route 'defaultRoute' is a catch all route. If no other routes match the specified url path this is the route that will be executed. In this example the defaultRoute would be executed for 'http://example.com' or 'http://example.com/home', but not 'http://example.com/page2'.
242
+ `npm test` builds the code, checks TypeScript consumer examples against the emitted declarations, and runs the tests. `npm run coverage` additionally enforces **100% statements, branches, functions, and lines for each implementation file**. Unexecuted implementation files count toward the result; declaration-only files contain no executable code and are excluded. Reports are written to `coverage`, including `lcov.info` for coverage viewers. CI runs the same gate and checks committed build output for drift.
243
+
244
+ Tests exercise the compiled JavaScript interface used by downstream callers. Coverage is an execution metric, not proof that all possible inputs or external integrations are correct.
298
245
 
299
- The second defined route 'page2' will only be executed when the specified url path starts with '/page2', or for example 'http://example.com/page2'.
246
+ To undo this migration, revert its commit and run `npm ci` from the restored lockfile. No npm release, database migration, or production deployment is performed by these development changes.
@@ -0,0 +1,111 @@
1
+ /** Navigation payload, including caller-defined metadata. */
2
+ interface NavigationData {
3
+ url?: string;
4
+ [key: string]: unknown;
5
+ }
6
+ /** URL-derived values supplied to route handlers. */
7
+ interface LocationData {
8
+ url: string;
9
+ data: {
10
+ url: string[];
11
+ mediator: NavigationData | undefined;
12
+ query: Record<string, string>;
13
+ };
14
+ }
15
+ type RouteHandler = (scope: Element | null, location: LocationData) => unknown;
16
+ /** An object route can own a function or a view lifecycle. */
17
+ interface RouteObject {
18
+ title?: string;
19
+ secure?: RouteHandler;
20
+ view?: RouteHandler | {
21
+ initialize?: RouteHandler;
22
+ destroy?: RouteHandler;
23
+ };
24
+ }
25
+ type Route = RouteHandler | RouteObject;
26
+ /** Minimal event-bus contract required by the router. */
27
+ interface NavigationMediator {
28
+ on(event: string, callback: (data?: NavigationData) => unknown): unknown;
29
+ removeListener?(event: string, callback: (data?: NavigationData) => unknown): unknown;
30
+ }
31
+ /** History API router with ordered path-boundary matching. */
32
+ declare class Router {
33
+ routes: Record<string, Route>;
34
+ url: string;
35
+ route: string | null;
36
+ scope: Element | null;
37
+ previousRoute: string | null;
38
+ pageTitle: string | null;
39
+ mediator: NavigationMediator | false;
40
+ boundPushStateClick: (event: MouseEvent) => unknown;
41
+ boundPopState: (event: PopStateEvent) => unknown;
42
+ boundMediatorNavigate: (data?: NavigationData) => unknown;
43
+ listenersInitialized: boolean;
44
+ locationData: LocationData;
45
+ /**
46
+ * Create an instance with its own state and listener references.
47
+ */
48
+ constructor();
49
+ /**
50
+ * Start this instance and return it for lifecycle chaining.
51
+ * @returns This instance for chaining.
52
+ */
53
+ initialize(): this;
54
+ /**
55
+ * Register stable document, history, and optional mediator handlers once.
56
+ * @returns This instance for chaining.
57
+ */
58
+ addListeners(): this;
59
+ /**
60
+ * Release owned state and listeners so the instance can leave the application lifecycle.
61
+ * @returns This instance after cleanup.
62
+ */
63
+ destroy(): this;
64
+ /**
65
+ * Release listeners owned by this instance; subclasses may extend the lifecycle hook.
66
+ * @returns This instance for chaining.
67
+ */
68
+ removeListeners(): this;
69
+ /**
70
+ * Intercept eligible same-origin push-state links while preserving normal browser actions.
71
+ * @param e - Browser event being handled.
72
+ * @returns True when the browser should handle the event, or this router after interception.
73
+ */
74
+ eventPushStateClick(e: MouseEvent): true | this;
75
+ /**
76
+ * Restore browser history state without adding a new history entry.
77
+ * @param e - Browser event being handled.
78
+ * @returns This router after restoring the history state.
79
+ */
80
+ eventPopState(e: PopStateEvent): this;
81
+ /**
82
+ * Decode URL query parameters, keeping the last value for duplicate keys.
83
+ * @param queryString - Encoded URL query portion.
84
+ * @returns A record of decoded query names and their final values.
85
+ */
86
+ parseQueryString(queryString: string): {
87
+ [k: string]: string;
88
+ };
89
+ /**
90
+ * Build decoded path, query, and mediator data for the selected route.
91
+ * @param mediatorData - Optional caller metadata passed to the route.
92
+ * @returns No value; locationData is updated in place.
93
+ */
94
+ setLocationData(mediatorData?: NavigationData): void;
95
+ /**
96
+ * Select a route, enforce its guard, transition view lifecycles, and update history and title.
97
+ * @param url - URL or optional adapter argument.
98
+ * @param mediatorData - Optional caller metadata passed to the route.
99
+ * @param isPopState - Whether navigation came from browser history.
100
+ * @returns False when a guard or route rejects navigation; otherwise this router.
101
+ */
102
+ navigate(url?: string | false, mediatorData?: NavigationData, isPopState?: boolean): false | this;
103
+ }
104
+ /** Public route configuration and navigation payload types. */
105
+ declare namespace Router {
106
+ type Handler = RouteHandler;
107
+ type Route = RouteHandler | RouteObject;
108
+ type Location = LocationData;
109
+ type Navigation = NavigationData;
110
+ }
111
+ export = Router;