@lwrjs/router 0.12.0-alpha.2 → 0.12.0-alpha.20

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.
Files changed (50) hide show
  1. package/README.md +777 -0
  2. package/build/bundle/prod/lwr/navigation/navigation.js +1 -1
  3. package/build/bundle/prod/lwr/router/router.js +1 -1
  4. package/build/bundle/prod/lwr/routerContainer/routerContainer.js +1 -1
  5. package/build/cjs/modules/lwr/domRouter/domRouter.cjs +20 -10
  6. package/build/cjs/modules/lwr/historyRouter/historyRouter.cjs +3 -3
  7. package/build/cjs/modules/lwr/navigation/navigationApi.cjs +4 -4
  8. package/build/cjs/modules/lwr/navigation/navigationMixin.cjs +4 -4
  9. package/build/cjs/modules/lwr/router/router.cjs +14 -13
  10. package/build/cjs/modules/lwr/routerUtils/routeUtils.cjs +21 -8
  11. package/build/cjs/modules/lwr/routerUtils/routerUtils.cjs +1 -0
  12. package/build/cjs/modules/lwr/serverRouter/serverRouter.cjs +10 -10
  13. package/build/cjs/services/index.cjs +0 -1
  14. package/build/cjs/services/module-provider/index.cjs +13 -36
  15. package/build/cjs/services/module-provider/utils.cjs +18 -8
  16. package/build/es/modules/lwr/contextUtils/navigationApiStore.d.ts +3 -2
  17. package/build/es/modules/lwr/domRouter/domRouter.d.ts +14 -8
  18. package/build/es/modules/lwr/domRouter/domRouter.js +25 -10
  19. package/build/es/modules/lwr/historyRouter/historyRouter.d.ts +3 -2
  20. package/build/es/modules/lwr/historyRouter/historyRouter.js +4 -4
  21. package/build/es/modules/lwr/navigation/navigationApi.d.ts +9 -8
  22. package/build/es/modules/lwr/navigation/navigationApi.js +10 -10
  23. package/build/es/modules/lwr/navigation/navigationMixin.js +4 -4
  24. package/build/es/modules/lwr/router/router.d.ts +1 -1
  25. package/build/es/modules/lwr/router/router.js +15 -14
  26. package/build/es/modules/lwr/routerUtils/routeUtils.d.ts +7 -5
  27. package/build/es/modules/lwr/routerUtils/routeUtils.js +22 -8
  28. package/build/es/modules/lwr/routerUtils/routerUtils.d.ts +1 -1
  29. package/build/es/modules/lwr/routerUtils/routerUtils.js +1 -1
  30. package/build/es/modules/lwr/routerUtils/types.d.ts +17 -13
  31. package/build/es/modules/lwr/serverRouter/serverRouter.d.ts +4 -3
  32. package/build/es/modules/lwr/serverRouter/serverRouter.js +11 -11
  33. package/build/es/services/index.d.ts +3 -1
  34. package/build/es/services/index.js +0 -1
  35. package/build/es/services/module-provider/index.d.ts +6 -7
  36. package/build/es/services/module-provider/index.js +15 -47
  37. package/build/es/services/module-provider/utils.d.ts +2 -2
  38. package/build/es/services/module-provider/utils.js +24 -8
  39. package/build/modules/lwr/domRouter/domRouter.js +27 -10
  40. package/build/modules/lwr/historyRouter/historyRouter.js +4 -4
  41. package/build/modules/lwr/navigation/navigationApi.js +10 -10
  42. package/build/modules/lwr/navigation/navigationMixin.js +4 -4
  43. package/build/modules/lwr/outlet/outlet.js-meta.xml +3 -0
  44. package/build/modules/lwr/router/router.js +15 -14
  45. package/build/modules/lwr/routerContainer/routerContainer.js-meta.xml +3 -0
  46. package/build/modules/lwr/routerUtils/routeUtils.js +23 -8
  47. package/build/modules/lwr/routerUtils/routerUtils.js +1 -1
  48. package/build/modules/lwr/serverRouter/serverRouter.js +11 -11
  49. package/package.json +9 -9
  50. package/pageObjects/outlet.d.cts +18 -0
package/README.md ADDED
@@ -0,0 +1,777 @@
1
+ # LWR Routing & Navigation
2
+
3
+ - [Introduction](#introduction)
4
+ - [Router](#router)
5
+ - [Locations](#locations)
6
+ - [Route Definitions](#route-definitions)
7
+ - [Route Matching](#route-matching)
8
+ - [Route Handlers](#route-handlers)
9
+ - [Generated Routers](#generated-routers)
10
+ - [Configuration](#configuration)
11
+ - [Router JSON](#router-json)
12
+ - [Usage](#usage)
13
+ - [Server-side Rendering](#server-side-rendering)
14
+ - [Router Container](#router-container)
15
+ - [Nesting Router Containers](#nesting-router-containers)
16
+ - [Outlet](#outlet)
17
+ - [Multiple Outlets](#multiple-outlets)
18
+ - [Navigation Wires](#navigation-wires)
19
+ - [`CurrentPageReference`](#currentpagereference)
20
+ - [`CurrentView`](#currentview)
21
+ - [`NavigationContext`](#navigationcontext)
22
+ - [Navigation APIs](#navigation-apis)
23
+ - [`navigate()`](#navigate)
24
+ - [`generateUrl()`](#generateurl)
25
+ - [Lightning Navigation](#lightning-navigation)
26
+ - [`NavigationMixin`](#navigationmixin)
27
+
28
+ ## Introduction
29
+
30
+ The `@lwrjs/router` package provides modules for client-side routing (`lwr/router`) and navigation (`lwr/navigation`), which export APIs to create a router, navigate, generate URLs and subscribe to navigation events. Client-side routing enables the creation of a Single Page Application (SPA).
31
+
32
+ LWR routers can be customized with configuration and hooks. They can also be nested, to create a hierarchy in an application.
33
+
34
+ > See the RFC on LWR routing APIs [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0003-router-api-baseline.md).
35
+
36
+ ## Router
37
+
38
+ A router is a piece of code that manages client-side navigation changes. All navigation events flow through a router for processing. Use the `createRouter(config: RouterConfig)` API to initialize a LWR router:
39
+
40
+ ```ts
41
+ import { createRouter } from 'lwr/router';
42
+ createRouter({
43
+ routes: [
44
+ /* see Route Definitions section */
45
+ ],
46
+ basePath: '/my-site',
47
+ i18n: {
48
+ locale: 'es',
49
+ defaultLocale: 'en-US'
50
+ }
51
+ caseSensitive: true,
52
+ });
53
+ ```
54
+
55
+ It takes a configuration object as an argument:
56
+
57
+ ```ts
58
+ type RouterConfig = {
59
+ routes?: RouteDefinition[]; // see Route Definitions section, default = []
60
+ basePath?: string; // a path prefix applied to all URIs, default = ''
61
+ i18n?: I18NRouterConfig; // a i18n config that may effect the path prefix applied to all URIs, default = {locale: 'en-US', defaultLocale: 'en-US'}
62
+ caseSensitive?: boolean; // true if URIs should be processed case sensitively, default = false
63
+ };
64
+ ```
65
+
66
+ ### Locations
67
+
68
+ The router processes incoming navigation events (i.e. location changes), which enter the router as **page references**. A page reference is a location in JSON form, passed to the router via the [`navigate()` API](#navigate)
69
+
70
+ ```ts
71
+ interface PageReference {
72
+ type: string;
73
+ attributes: { [key: string]: string | null };
74
+ state: { [key: string]: string | null };
75
+ }
76
+ ```
77
+
78
+ The router uses its [route definitions](#route-definitions) to determine if a location is valid. If so, the navigation event is accepted and a user will see updated content in their browser.
79
+
80
+ ### Route Definitions
81
+
82
+ The most important part of the `RouterConfig` is the array of route definitions. The router uses these to verify and process incoming [location](#locations) changes. A location is only valid if it can be matched to a `RouteDefinition`. The application will fail to navigate given an invalid location. Each `RouteDefinition` has this shape:
83
+
84
+ ```ts
85
+ interface RouteDefinition<TMetadata = Record<string, any>> {
86
+ id: string;
87
+ uri: string;
88
+ page: Partial<PageReference>;
89
+ handler: () => Promise<{ default: RouteHandler }>;
90
+ patterns?: { [paramName: string]: string };
91
+ exact?: boolean;
92
+ metadata?: TMetadata;
93
+ }
94
+ ```
95
+
96
+ containing the following properties:
97
+
98
+ - `id`: each `RouteDefinition` must have a unique identifier
99
+ - `uri`: a string pattern for URIs which match this `RouteDefinition`; the grammar is fully defined [in an RFC](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0006-route-binding-and-serialization.md#uri-grammar-syntax) and includes these characters:
100
+ - `/`: path separator
101
+ - `:parameter`: captures a variable from a path or query parameter; must be alpha-numeric (i.e. [a-zA-Z0-9])
102
+ - `?`: denotes the beginning of the query string
103
+ - `&`: query parameter separator
104
+ - `page`: shape for page references which match this `RouteDefinition`; the usage is detailed [in an RFC](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0006-route-binding-and-serialization.md#pagereference-binding) and allows:
105
+ - `:parameter` bindings: map a path or query parameter from the `uri` to an `attributes` or `state` property
106
+ - literal bindings: hard-code the `type`, an `attribute`, or `state` property to a literal value
107
+ - `handler`: a `Promise` to a module which is called when a `RouteDefinition` is matched by a location; see the [Route Handlers section](#route-handlers)
108
+ - `patterns` (optional): a regular expression which a parameter must match in order to be valid; described in an RFC [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0006-route-binding-and-serialization.md#parameter-validation-patterns)
109
+ - `exact` (optional, default = `true`): see the [Nesting Router Containers section](#nesting-router-containers)
110
+ - `metadata` (optional): developer-defined metadata attached to `RouteDefinition`; see the RFC [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0000-route-definition-meta.md) and a recipe [here](https://github.com/salesforce-experience-platform-emu/lwr-recipes/tree/main/packages/routing-extended-metadata)
111
+
112
+ > _Important_: The `routes` array seen in [LWR app configuration](./config.md) is for **server-side** routes (see RFC [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0000-lwr-app-config.md#application-routes)), and is unrelated to the `@lwrjs/router` package.
113
+
114
+ ### Route Matching
115
+
116
+ Here is an example `RouteDefinition` for a recipe:
117
+
118
+ ```js
119
+ // RouteDefinition for a page in a recipe website
120
+ {
121
+ id: 'recipe',
122
+ uri: '/recipes/:category/:recipeId?units=:units&yummy=yes',
123
+ patterns: {
124
+ recipeId: '[0-9]{3}', // "recipeId" must be a 3 digit number
125
+ },
126
+ page: {
127
+ type: 'recipe_page', // matching page references must be of type "recipe_page"
128
+ attributes: {
129
+ recipeId: ':recipeId', // straightforward attribute binding
130
+ cat: ':category', // bind the "category" path parameter to the "cat" attribute
131
+ units: ':units', // bind the "units" query parameter to an attribute
132
+ },
133
+ state: {
134
+ code: 'abc123', // hard-coded state literal
135
+ },
136
+ },
137
+ handler: () => import('my/recipeHandler'),
138
+ }
139
+ ```
140
+
141
+ This URI and page reference match the recipe `RouteDefinition`:
142
+
143
+ ```js
144
+ // URI -> https://www.somewhere.com/recipes/desserts/010?units=metric&yummy=yes&extra=foo (extra query params are allowed)
145
+
146
+ // page reference:
147
+ {
148
+ "type": "recipe_page", // type matches
149
+ "attributes": {
150
+ // all bound attributes have values
151
+ "recipeId": "010", // the "recipeId" matches the pattern
152
+ "cat": "desserts",
153
+ "units": "metric"
154
+ },
155
+ "state": {
156
+ "code": "abc123", // the state literal matches
157
+ "extra": "foo" // extra state properties are allowed
158
+ }
159
+ }
160
+ ```
161
+
162
+ This URI and page reference **do not** match:
163
+
164
+ ```js
165
+ // URI -> https://www.somewhere.com/r/desserts/abc (parameters and literals are missing or malformed)
166
+
167
+ // page reference:
168
+ {
169
+ "type": "awful_page", // type DOES NOT match
170
+ "attributes": {
171
+ "recipeId": "lol", // the "recipeId" DOES NOT match the pattern
172
+ "extra": "bad" // extra attributes ARE NOT allowed
173
+ // the "cat" and "units" attributes are missing
174
+ },
175
+ "state": {
176
+ "code": "fail" // the state literal IS NOT equal
177
+ }
178
+ }
179
+ ```
180
+
181
+ > See more `RouteDefinition` examples with [matching](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0006-route-binding-and-serialization.md#positively-matching-pagereferences) and [non-matching](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0006-route-binding-and-serialization.md#failing-matching-pagereferences) page references in the RFC.
182
+
183
+ ### Route Handlers
184
+
185
+ When the router [matches](#route-matching) an incoming [location](#locations) to a [`RouteDefinition`](#route-definitions), it accesses its `RouteDefinition.handler` to determine the associated "view". A view is the component to display when the application navigates to a location.
186
+
187
+ ```ts
188
+ // types related to the RouteHandler
189
+ interface RouteDefinition {
190
+ handler: () => Promise<{ default: RouteHandler }>; // `export default` a RouteHandler module
191
+ // other properties...
192
+ }
193
+ interface RouteHandler {
194
+ new (callback: (routeDestination: RouteDestination) => void): void; // denotes a Class
195
+ dispose(): void;
196
+ update(routeInfo: RouteInstance): void;
197
+ }
198
+ interface RouteDestination {
199
+ // provided by `RouteHandler.update()` via a callback
200
+ viewset: ViewSet;
201
+ }
202
+ interface ViewSet {
203
+ [namedView: string]: (() => Promise<Module>) | ViewInfo;
204
+ }
205
+ interface ViewInfo {
206
+ module: () => Promise<Module>;
207
+ specifier: string;
208
+ }
209
+ interface RouteInstance {
210
+ // location information passed to `RouteHandler.update()`
211
+ id: string; // RouteDefinition.id
212
+ attributes: { [key: string]: string | null };
213
+ state: { [key: string]: string | null };
214
+ pageReference: PageReference;
215
+ }
216
+ ```
217
+
218
+ > _Note_: Modules are always provided via promises. This allows the module code to be lazily loaded, which improves performance of the application.
219
+
220
+ Given information on the current location (i.e. a `RouteInstance`), the job of the `RouteHandler` module is to provide a set of views via a callback from its `update()` function:
221
+
222
+ ```ts
223
+ // "my/recipeHandler" RouteHandler module
224
+ import type { Module, RouteHandlerCallback } from 'lwr/router';
225
+
226
+ export default class RecipeHandler {
227
+ callback: RouteHandlerCallback;
228
+
229
+ constructor(callback: RouteHandlerCallback) {
230
+ this.callback = callback; // Important: maintain a reference to the callback
231
+ }
232
+
233
+ dispose(): void {
234
+ // perform cleanup tasks
235
+ }
236
+
237
+ update(routeInfo: RouteInstance): void {
238
+ // called every time a RouteDefinition with this handler matches a location during processing
239
+ const {
240
+ attributes: { cat }, // location information
241
+ } = routeInfo;
242
+ const category = cat || 'entree'; // cat may be null
243
+ const viewSpecifier = `my/${category}Recipe`; // e.g. "my/dessertRecipe"
244
+ this.callback({
245
+ viewset: {
246
+ // return view component info based on the recipe's category
247
+ default: {
248
+ module: (): Promise<Module> => import(viewSpecifier),
249
+ specifier: viewSpecifier,
250
+ },
251
+ },
252
+ });
253
+ }
254
+ }
255
+ ```
256
+
257
+ > See the `RouteHandler` RFC [here](https://rfcs.lwc.dev/rfcs/lwr/0002-route-handler) and some example handlers [here](https://github.com/salesforce-experience-platform-emu/lwr-recipes/tree/main/packages/simple-routing/src/modules/example).
258
+
259
+ ### Generated Routers
260
+
261
+ The Router Module Provider can generate a router based on a static JSON file. A generated router consumes its [configuration](#router) from a portable JSON file rather than a JavaScript module. Static configuration can be easier to author and to maintain. This approach is most helpful for straightforward use cases.
262
+
263
+ #### Configuration
264
+
265
+ The Router Module Provider is not a default module provider, so it must be added to the project configuration. Learn more in [Configure a LWR Project](https://github.com/salesforce-experience-platform-emu/lwr-recipes/blob/main/doc/config.md#providers).
266
+
267
+ Add `"@lwrjs/router/module-provider" as a dependency in `package.json`.
268
+
269
+ ```json
270
+ // package.json
271
+ {
272
+ "dependencies": {
273
+ "@lwrjs/router/module-provider": "0.7.1"
274
+ }
275
+ }
276
+ ```
277
+
278
+ Register the Router Module Provider in `lwr.config.json`.
279
+
280
+ ```json
281
+ // lwr.config.json with the Router Module Provider and the LWR default module providers
282
+ {
283
+ "moduleProviders": [
284
+ "@lwrjs/router/module-provider",
285
+ "@lwrjs/app-service/moduleProvider",
286
+ "@lwrjs/lwc-module-provider",
287
+ "@lwrjs/npm-module-provider"
288
+ ]
289
+ }
290
+ ```
291
+
292
+ When registering the module provider, optionally configure the directory location of the router JSON files.
293
+
294
+ ```json
295
+ // lwr.config.json
296
+ {
297
+ "moduleProviders": [
298
+ ["@lwrjs/router/module-provider", { "routesDir": "$rootDir/config/router" }],
299
+ "@lwrjs/app-service/moduleProvider",
300
+ "@lwrjs/lwc-module-provider",
301
+ "@lwrjs/npm-module-provider"
302
+ ]
303
+ }
304
+ ```
305
+
306
+ If a configuration is not specified when registering the Router Module Provider, it uses this default configuration.
307
+
308
+ ```json
309
+ {
310
+ "routesDir": "$rootDir/src/routes"
311
+ }
312
+ ```
313
+
314
+ To automatically register **client-side** routes with the **server**, specify them as "sub routes" by pointing to their [Route JSON file](#router-json).
315
+
316
+ ```json
317
+ // lwr.config.json
318
+ {
319
+ "routes": [
320
+ {
321
+ "id": "spa",
322
+ "path": "/site",
323
+ "rootComponent": "my/spa",
324
+ "subRoutes": "$rootDir/src/routes/client.json"
325
+ }
326
+ ]
327
+ }
328
+ ```
329
+
330
+ If the client-side routes contain these `uri`s:
331
+
332
+ - "/"
333
+ - "/about"
334
+ - "/:id"
335
+ Then the LWR server will automatically register these `path`s:
336
+ - "_/site_"
337
+ - "_/site_/about"
338
+ - "_/site_/:id"
339
+
340
+ This allows users to do a full page refresh on a client-side route without getting a 404.
341
+
342
+ #### Router JSON
343
+
344
+ The Router Module Provider generates a router module based on JSON configuration: `LwrRouterConfig`.
345
+
346
+ ```ts
347
+ interface LwrRouterConfig {
348
+ basePath?: string;
349
+ caseSensitive?: boolean;
350
+ routes: LwrConfigRouteDefinition[];
351
+ }
352
+
353
+ interface LwrConfigRouteDefinition<TMetadata = Record<string, any>> {
354
+ // These properties are the same as in RouteDefinition
355
+ id: string;
356
+ uri: string;
357
+ page?: Partial<PageReference>;
358
+ patterns?: { [paramName: string]: string };
359
+ exact?: boolean;
360
+ metadata?: TMetadata;
361
+ // These properties are different than RouteDefinition
362
+ // A Route Definition must have 1 or the other, but not both
363
+ handler?: string; // a STRING reference to the handler class
364
+ component?: string; // a STRING reference to a page component
365
+ }
366
+ ```
367
+
368
+ The `LwrRouterConfig` contains the same properties which are passed to [`createRouter()`](#router). The `LwrConfigRouteDefinition` contains the same properties as [`RouteDefinition`](#route-definitions), except for:
369
+
370
+ - `handler`: A **string** reference to the [`RouteHandler`](#route-handlers) class specifier, rather than a function.
371
+ - `component`: A **string** reference to the view component specifier. This is a shortcut so the view component can be specified directly, without authoring a `RouteHandler`. A `LwrConfigRouteDefinition` must contain a `handler` or a `component`, but not both.
372
+
373
+ > Note: `LwrConfigRouteDefinition` is pure JSON, which is why it cannot contain any functions like `RouteDefinition` does.
374
+
375
+ Here is a Router config example.
376
+
377
+ ```json
378
+ // src/routes/website.json
379
+ {
380
+ "routes": [
381
+ {
382
+ "id": "home",
383
+ "uri": "/",
384
+ "component": "examples/home",
385
+ "page": {
386
+ "type": "home"
387
+ },
388
+ "metadata": {
389
+ "title": "Home"
390
+ }
391
+ },
392
+ {
393
+ "id": "namedPage",
394
+ "uri": "/:pageName",
395
+ "handler": "examples/namedPageHandler",
396
+ "page": {
397
+ "type": "namedPage",
398
+ "attributes": {
399
+ "pageName": ":pageName"
400
+ }
401
+ }
402
+ }
403
+ ]
404
+ }
405
+ ```
406
+
407
+ #### Usage
408
+
409
+ Import and use a router generated using the JSON above.
410
+
411
+ ```js
412
+ // src/modules/my/app/app.js
413
+ import { LightningElement } from 'lwc';
414
+ import { createRouter } from '@lwrjs/router/website'; // "website" refers to src/routes/website.json
415
+
416
+ export default class MyApp extends LightningElement {
417
+ router = createRouter();
418
+ }
419
+ ```
420
+
421
+ ```html
422
+ <!-- src/modules/my/app/app.html -->
423
+ <template>
424
+ <lwr-router-container router="{router}">
425
+ <lwr-outlet></lwr-outlet>
426
+ </lwr-router-container>
427
+ </template>
428
+ ```
429
+
430
+ The generated router module specifier is: `@lwrjs/router/<name of the JSON config file>`. It provides a `createRouter()` function that is identical to the [static `createRouter()` function](#router), except that it does not take a `routes` array, since the routes are configured in the JSON file instead. If `basePath` or `caseSensitive` is specified in both the JSON file and the `createRouter()` call, then the latter takes precedence.
431
+
432
+ ### Server-side Rendering
433
+
434
+ See [this documentation](../lwc-ssr/README.md#routing) to learn about routing during server-side rendering.
435
+
436
+ ## Router Container
437
+
438
+ In order to use a [router](#router) in an application, it must be attached to the DOM. This is done with a router container, provided by the `lwr-router-container` component.
439
+
440
+ A router container provides "navigation context", meaning that it is responsible for [processing](#route-matching) all navigation [wires](#navigation-wires) and [events](#navigation-apis) from its descendants in the DOM (e.g. `my-nav` and `lwr-outlet` in the example code below).
441
+
442
+ ```html
443
+ <!-- my/app/app.html -->
444
+ <template>
445
+ <lwr-router-container
446
+ router="{router}"
447
+ onhandlenavigation="{handleNavigation}"
448
+ onprenavigate="{preNavigate}"
449
+ onpostnavigate="{postNavigate}"
450
+ onerrornavigate="{errorNavigate}"
451
+ >
452
+ <my-nav></my-nav>
453
+ <lwr-outlet><!-- See the Outlet section below --></lwr-outlet>
454
+ </lwr-router-container>
455
+ </template>
456
+ ```
457
+
458
+ ```ts
459
+ // my/app/app.ts
460
+ import { LightningElement } from 'lwc';
461
+ import { createRouter } from 'lwr/router';
462
+ import { ROUTE_DEFINITIONS } from './routeDefinitions';
463
+
464
+ export default class MyApp extends LightningElement {
465
+ router = createRouter({ routes: ROUTE_DEFINITIONS });
466
+ approvedCategories = ['apps', 'entrees', 'sides', 'desserts'];
467
+
468
+ handleNavigation(e: CustomEvent): void {
469
+ console.log('navigate() called with page reference:', e.detail);
470
+ }
471
+
472
+ preNavigate(e: CustomEvent): void {
473
+ const {
474
+ next: {
475
+ route: { pageReference },
476
+ },
477
+ } = e.detail;
478
+ const {
479
+ attributes: { cat },
480
+ } = pageReference;
481
+ console.log('navigation event incoming with page reference:', pageReference);
482
+ if (!this.approvedCategories.includes(cat)) {
483
+ // REJECT unapproved recipe categories
484
+ e.preventDefault();
485
+ }
486
+ }
487
+
488
+ postNavigate(e: CustomEvent): void {
489
+ const {
490
+ route: { pageReference },
491
+ } = e.detail;
492
+ console.log('navigated to page reference:', pageReference);
493
+ }
494
+
495
+ errorNavigate(e: CustomEvent): void {
496
+ const { code, message } = e.detail;
497
+ console.error(`navigation error -> ${code}: ${message}`);
498
+ }
499
+ }
500
+ ```
501
+
502
+ A router container requires a [router](#router), and fires these [events](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/events_handling):
503
+
504
+ - `onhandlenavigation`: dispatched when [`navigate(pageRef)`](#navigate) is called; `event.preventDefault()` **cancels** the navigation event; `event.detail` is the `PageReference`
505
+ - `onprenavigate`: dispatched when a navigation event is received and a `RouteDefinition` match is found; `event.preventDefault()` **cancels** the navigation event; `event.detail` is a `RouteChange`
506
+ - `onpostnavigate`: dispatched when a navigation event has completed; `event.detail` is a `DomRoutingMatch` for the current location
507
+ - `onerrornavigate`: dispatched when there is an error processing a navigation event (e.g. no `RouteDefinition` match, `prenavigate` cancelation); `event.detail` is a `MessageObject`
508
+
509
+ ```ts
510
+ // router container event payload types
511
+ interface DomRoutingMatch {
512
+ url: string; // e.g. "/recipes/desserts/010?units=metric&yummy=yes"
513
+ route: RouteInstance;
514
+ routeDefinition: RouteDefinition;
515
+ }
516
+ interface RouteChange {
517
+ current?: DomRoutingMatch; // the current location info
518
+ next: DomRoutingMatch; // location info for the incoming nav event
519
+ }
520
+ interface MessageObject {
521
+ code: string | number;
522
+ message: string;
523
+ level: number; // Fatal = 0, Error = 1, Warning = 2, Log = 3
524
+ }
525
+ ```
526
+
527
+ > See a simple routing recipe [here](https://github.com/salesforce-experience-platform-emu/lwr-recipes/tree/main/packages/simple-routing).
528
+
529
+ ### Nesting Router Containers
530
+
531
+ A router container can have up to 1 child router. Each router is responsible for processing the navigation events from its descendants. Every [`RouteDefinition`](#route-definitions) resolving to a view component which includes a child router must set `exact` to `false`:
532
+
533
+ ```js
534
+ // parent RouteDefinition for a page which includes a child router
535
+ {
536
+ id: 'root',
537
+ uri: '/parent/path',
538
+ exact: false, // allow the parent and child router to resolve a URI together (i.e. "/parent/path/child/path&params)
539
+ page: { type: 'home' },
540
+ handler: () => import('my/someHandler'), // resolves a view containing a child router container
541
+ }
542
+ ```
543
+
544
+ > See a nested routing recipe [here](https://github.com/salesforce-experience-platform-emu/lwr-recipes/tree/main/packages/nested-routing).
545
+
546
+ ## Outlet
547
+
548
+ It is the router's job to [resolve view components](#route-handlers) for a given location, and it is the outlet's job to _display_ those view components:
549
+
550
+ ```html
551
+ <!-- my/app/app.html -->
552
+ <template>
553
+ <lwr-router-container>
554
+ <lwr-outlet refocus-off onviewchange="{onViewChange}" onviewerror="{onViewError}">
555
+ <div slot="error">View component cannot display</div>
556
+ </lwr-outlet>
557
+ </lwr-router-container>
558
+ </template>
559
+ ```
560
+
561
+ The outlet uses the [`CurrentView` wire](#currentview) to get the current view component, then displays it in the DOM. It has:
562
+
563
+ - [properties](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_components_data_binding):
564
+ - `view-name`: the key of the `ViewSet` entry to display; the default value is `"default"`
565
+ - `refocus-off` boolean: if present, the outlet will **not** put the browser focus on the view component when it loads; refocusing is on by default as an accessibility feature
566
+ - [events](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/events_handling):
567
+
568
+ - `onviewchange` event: dispatched whenever the view component changes; `event.detail` is the view component class:
569
+
570
+ ```ts
571
+ type Constructor<T = object> = new (...args: any[]) => T;
572
+ interface Constructable<T = object> {
573
+ constructor: Constructor<T>;
574
+ }
575
+ interface ViewChangePayload {
576
+ detail: Constructable;
577
+ }
578
+ ```
579
+
580
+ - `onviewerror` event: dispatched whenever the view component fails to mount; `event.detail` is the error and stack:
581
+
582
+ ```ts
583
+ interface ViewErrorPayload {
584
+ detail: {
585
+ error: Error;
586
+ stack: string;
587
+ };
588
+ }
589
+ ```
590
+
591
+ - [slots](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_components_slots):
592
+ - "error": The contents of the error slot are shown whenever the view component fails to mount
593
+
594
+ > See the RFC for the outlet [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0003-router-api-baseline.md#lwroutlet) and the `viewchange` and `viewerror` events [here](https://github.com/salesforce-emu/lwr-rfcs/blob/master/text/0000-router-viewChange-event.md).
595
+
596
+ ### Multiple Outlets
597
+
598
+ A [`RouteHandler`](#route-handlers) may return multiple views:
599
+
600
+ ```ts
601
+ import type { Module, RouteHandlerCallback } from 'lwr/router';
602
+
603
+ export default class HomeHandler {
604
+ callback: RouteHandlerCallback;
605
+
606
+ constructor(callback: RouteHandlerCallback) {
607
+ this.callback = callback;
608
+ }
609
+
610
+ dispose(): void {}
611
+
612
+ update(): void {
613
+ this.callback({
614
+ viewset: {
615
+ // return multiple views
616
+ default: (): Promise<Module> => import('my/home'),
617
+ nav: (): Promise<Module> => import('my/homeNav'),
618
+ footer: (): Promise<Module> => import('my/homeInfo'),
619
+ },
620
+ });
621
+ }
622
+ }
623
+ ```
624
+
625
+ Multiple outlets can be used to display all the current view components by setting different `view-names`:
626
+
627
+ ```html
628
+ <!-- my/app/app.html -->
629
+ <template>
630
+ <lwr-router-container>
631
+ <lwr-outlet view-name="nav"></lwr-outlet>
632
+ <lwr-outlet><!-- default view --></lwr-outlet>
633
+ <lwr-outlet view-name="footer"></lwr-outlet>
634
+ </lwr-router-container>
635
+ </template>
636
+ ```
637
+
638
+ ## Navigation Wires
639
+
640
+ The `lwr/navigation` module provides [wire adapters](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.data_wire_service_about) from which components can receive information about navigation events.
641
+
642
+ ### `CurrentPageReference`
643
+
644
+ Get the current page reference from the [router container](#router-container):
645
+
646
+ ```js
647
+ import { LightningElement, wire } from 'lwc';
648
+ import { CurrentPageReference } from 'lwr/navigation';
649
+
650
+ export default class Example extends LightningElement {
651
+ // Subscribe to page reference updates
652
+ @wire(CurrentPageReference)
653
+ printPageName(pageRef) {
654
+ console.log(`Page name: ${pageRef ? pageRef.attributes.name : ''}`);
655
+ }
656
+ }
657
+ ```
658
+
659
+ > _Note_: This wire is also available in [Lightning Experience](https://developer.salesforce.com/docs/component-library/bundle/lightning-navigation/documentation).
660
+
661
+ ### `CurrentView`
662
+
663
+ Get a reference to the current view component. The `viewName` configuration property is optional, and falls back to `"default"` if unspecified.
664
+
665
+ ```js
666
+ import { LightningElement, wire } from 'lwc';
667
+ import { CurrentView } from 'lwr/navigation';
668
+
669
+ export default class MyFooter extends LightningElement {
670
+ // Subscribe to view component updates
671
+ @wire(CurrentView, { viewName: 'footer' })
672
+ viewCtor;
673
+ }
674
+ ```
675
+
676
+ ### `NavigationContext`
677
+
678
+ Get a reference to a component's navigation context (i.e. its closest ancestor [router container](#router-container)), for use with the [navigation APIs](#navigation-apis):
679
+
680
+ ```ts
681
+ import { LightningElement, wire } from 'lwc';
682
+ import { NavigationContext } from 'lwr/navigation';
683
+ import type { ContextId } from 'lwr/navigation';
684
+
685
+ export default class Example extends LightningElement {
686
+ @wire(NavigationContext as any)
687
+ navContext?: ContextId;
688
+ }
689
+ ```
690
+
691
+ ## Navigation APIs
692
+
693
+ ### `navigate()`
694
+
695
+ Navigate programmatically:
696
+
697
+ ```ts
698
+ import { LightningElement, api, wire } from 'lwc';
699
+ import { NavigationContext, navigate } from 'lwr/navigation';
700
+ import type { ContextId } from 'lwr/navigation';
701
+
702
+ export default class AboutLink extends LightningElement {
703
+ @wire(NavigationContext as any)
704
+ navContext?: ContextId;
705
+
706
+ handleClick(event: Event): void {
707
+ event.preventDefault();
708
+ if (this.navContext) {
709
+ navigate(this.navContext, {
710
+ type: 'named_page',
711
+ attributes: { name: 'about' },
712
+ });
713
+ }
714
+ }
715
+ }
716
+ ```
717
+
718
+ ### `generateUrl()`
719
+
720
+ Generate a URL for a page reference:
721
+
722
+ ```ts
723
+ import { LightningElement, api, wire } from 'lwc';
724
+ import { NavigationContext, generateUrl } from 'lwr/navigation';
725
+ import type { ContextId, PageReference } from 'lwr/navigation';
726
+
727
+ export default class UrlGenerator extends LightningElement {
728
+ @api pageReference?: PageReference;
729
+
730
+ @wire(NavigationContext as any)
731
+ navContext?: ContextId;
732
+
733
+ connectedCallback(): void {
734
+ if (this.pageReference && this.navContext) {
735
+ const url = generateUrl(this.navContext, this.pageReference);
736
+ console.log(`"${url}" is the URL for this page reference:`, this.pageReference);
737
+ }
738
+ }
739
+ }
740
+ ```
741
+
742
+ ## Lightning Navigation
743
+
744
+ The `@lwrjs/router` package provides an implementation of [`lightning/navigation`](http://component-library-dev.herokuapp.com/docs/component-library/bundle/lightning-navigation/documentation), which defines the `NavigationMixin` and the `CurrentPageReference` wire adapter. This allows a component to be written once and plugged in anywhere that supports the `lightning/navigation` contracts.
745
+
746
+ The `lightning/navigation` module is an [alias](https://github.com/salesforce/lwc-rfcs/blob/master/text/0020-module-resolution.md#aliasmodulerecord) for the `lwr/navigation` module, so it includes the same [wires](#navigation-wires) and [APIs](#navigation-apis), along with the [`NavigationMixin`](#navigationmixin).
747
+
748
+ > **Important** Pick either `lightning/navigation` or `lwr/navigation` to use throughout your app. Otherwise, there could be JavaScript bundling clashes when running in `prod` mode.
749
+
750
+ ### `NavigationMixin`
751
+
752
+ Some developers may prefer to use the `NavigationMixin` over the [`navigate()` and `generateUrl()` APIs](#navigation-apis). Both offer the same functionality, but only the `NavigationMixin` is compatible with Lightning Experience (LEX). So a developer writing a component for use in both LWR and LEX should choose the `NavigationMixin`.
753
+
754
+ ```js
755
+ import { LightningElement } from 'lwc';
756
+ import { NavigationMixin } from 'lightning/navigation';
757
+
758
+ const pageRef = {
759
+ type: 'standard__recordPage',
760
+ attributes: {
761
+ recordId: '001xx000003DGg0AAG',
762
+ objectApiName: 'Account',
763
+ actionName: 'view',
764
+ },
765
+ };
766
+
767
+ export default class Example extends NavigationMixin(LightningElement) {
768
+ // Navigate to a page reference
769
+ navPageRef() {
770
+ this[NavigationMixin.Navigate](pageRef);
771
+ }
772
+ // Generate a URL for a page reference
773
+ getUrl() {
774
+ this[NavigationMixin.GenerateUrl](pageRef).then((url) => console.log(url));
775
+ }
776
+ }
777
+ ```