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

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