@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 +795 -0
- package/build/bundle/prod/lwr/router/router.js +1 -1
- package/build/bundle/prod/lwr/routerContainer/routerContainer.js +1 -1
- package/build/cjs/modules/lwr/router/router.cjs +5 -4
- package/build/cjs/modules/lwr/routerUtils/routeUtils.cjs +16 -8
- package/build/cjs/modules/lwr/routerUtils/routerUtils.cjs +5 -0
- package/build/cjs/services/index.cjs +7 -0
- package/build/es/modules/lwr/router/router.js +6 -5
- package/build/es/modules/lwr/routerUtils/routeUtils.d.ts +5 -4
- package/build/es/modules/lwr/routerUtils/routeUtils.js +18 -8
- package/build/es/modules/lwr/routerUtils/routerUtils.d.ts +2 -0
- package/build/es/modules/lwr/routerUtils/routerUtils.js +4 -0
- package/build/es/modules/lwr/routerUtils/types.d.ts +5 -0
- package/build/es/services/index.d.ts +2 -0
- package/build/es/services/index.js +7 -0
- package/build/modules/lwr/router/router.js +6 -5
- package/build/modules/lwr/routerUtils/routeUtils.js +18 -8
- package/build/modules/lwr/routerUtils/routerUtils.js +5 -1
- package/package.json +8 -8
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¶ms)
|
|
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
|
+
```
|