
Product
Introducing Socket Fix for Safe, Automated Dependency Upgrades
Automatically fix and test dependency updates with socket fixβa new CLI tool that turns CVE alerts into safe, automated upgrades.
@webruntime/navigation
Advanced tools
This is a client-side routing package for off-core LWC-based applications, from Lightning Web Runtime (Webruntime).
It supplies an API with the ability to create a router, navigate, generate URLs and subscribe to navigation events.
Routers can be customized at various plug points, or used as-is. Routers can also be nested, to create a hierarchy on the page.
Built on top of the Webruntime routing APIs is a Lightning Navigation layer. It provides implementations for the NavigationMixin
and CurrentPageReference
wire adapter from lightning/navigation. This allows a component to be written once, and plugged in anywhere that uses the lightning/navigation
contracts.
A consumer of this package can write an application that uses the Webruntime routing APIs, the Lightning Navigation layer, or both. Customer components or applications will most likely use the Lightning Navigation API, as our public-facing contract.
These concepts are used throughout this documentation. All examples are using this URL:
https://www.somewhere.com/case/10?param1=one¶m2=two¶m3
https://www.somewhere.com/case/10?param1=one¶m2=two¶m3
or /case/10?param1=one¶m2=two¶m3
)pathname
of a URL (e.g.: /case/10
)pathname
of a URL (e.g.: /case/10
NOT /10
)?param1=one¶m2=two¶m3
){
"param1": "one",
"param2": "two",
"param3": ""
}
id
and an optionally parameterized path
is used. Path-to-regexp is used to parse the path
. To support lightning/navigation
APIs, add the page
property. Additional custom data may also be included, e.g.:{
/* required data */
"path": "/case/:recordId/:optional?", // Uses path-to-regexp for parsing parameters.
"exact": true/false, // When true, will only match if the path matches location.pathname exactly. The default is `true`.
/* Basic routing */
"id": "case-detail",
/* lightning/navigation */
"page": {
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case"
}
},
/* custom data */
"view": "caseDetail"
"label": "Case Detail"
}
Note: More information about route definition matching can be found here.
id
comes from the matching route definition. The attributes
property contains the parameters parsed from the URL using the route definition path
. The state
property contains the query object, e.g.:{
"id": "case-detail",
"attributes": {
"objectApiName": "Case",
"recordId": "place"
},
"state": {
"param1": "one",
"param2": "two",
"param3": ""
}
}
lightning/navigation
. The type
property is used instead of the id
, e.g.:{
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case",
"recordId": "place"
},
"state": {
"param1": "one",
"param2": "two",
"param3": ""
}
}
{
"type": "standard__simpleRoute",
"attributes": {
"path": "/case/10"
},
"state": {
"param1": "one",
"param2": "two",
"param3": ""
}
}
The navigation package has a dependency upon the LWC wire-service. To include the wire-service
:
wire-service
in webruntime-app.config.js
:preloadModules: ['wire-service'],
wire-service
in the root component of the application:import { LightningElement, register } from 'lwc';
import { registerWireService } from 'wire-service';
registerWireService(register);
export default class RootApp extends LightningElement {
// ...
}
The Webruntime routing APIs supply functions to create a root router, navigate (programmatically and declaratively), generate URLs, and subscribe to navigation events.
// app.js
import { createRouter, navigate, generateUrl, subscribe, NavigationContext } from 'webruntime/navigation';
<!-- component.html -->
<webruntime-link path="/some/where">CLICK ME</webruntime-link>`
createRouter()
const router = createRouter(config);
Create a new root router for an application with arguments:
config
: Configure the router with an object containing these properties:
basePath
: Set a base path for this router. The default is an empty string.routes
: Set an array of route definitions for URL parsing. The default is an empty array.noHistory
: By default, the router will manage the browser history. To turn this behavior off, set noHistory: true
.caseSensitive
: When true
, the route definition path matching will be case sensitive. The default is false
.A router object is returned:
connect()
: Call this to start the router.disconnect()
: Call this to stop the router from interacting with the application.addPreNavigate(function | function[]) -> this router
: Add a listener function, or array of functions, to the pre navigate event hook.addPostNavigate(function | function[]) -> this router
: Add a listener function, or array of functions, to the post navigate event hook.addErrorNavigate(function | function[]) -> this router
: Add a listener function, or array of functions, to the navigation error hook.id
: The navigation context ID for this router.A router can have 0 or more listeners attached to each of its lifecycle hooks. The listeners are fired synchronously in the order in which they were attached:
preNavigate(transaction)
: This runs during a navigation event, before the state is changed. Listeners can stop the transaction at this point. If stopped, the errorNavigate
listeners will be fired. The preNavigate
functions should return one of these values:
true
: The route should be processed.false
: The navigation event should be stopped, and no more listeners run on any router.Promise
: Resolves to one of the values above; rejected Promise
s are treated as false
.postNavigate(transaction)
: This runs after a navigation event completes. Subscribers will not be notified until all post navigation listeners have finished executing. If a postNavigate
listener returns false
or a rejected Promise
, the remaining listeners for that router are skipped, but the postNavigate
listeners on any descendent routers will still run.errorNavigate(error)
: This runs if the router encounters an error trying to navigate.The pre and post navigation listens are passed a read-only transaction object:
current
: The navigation information { route, data }
for the current state, route may be null
during preNavigate
.next
(preNavigate
only): The navigation information that will load next (if not stopped).previous
(postNavigate
only): The navigation information that just unloaded, route may be null
.The postNavigate
listeners are passed an error
object:
{
code: an integer error code,
message: a string error message,
level: 1,
url: an optional URL where more information on the error can be found
}
See an example here.
NavigationContext
@wire(NavigationContext)
navContext;
A wire adapter that gives a component access to the its navigation context. The navigation context is used to call the Webruntime routing APIs below.
navigate()
navigate(navigationContext, location, shouldReplace);
Navigate to a new page programmatically with arguments:
navigationContext
: The navigation context in which to navigate.location
: A route, or absolute path, to navigate to.shouldReplace
: Send true
if the new location should replace the current one in the browser history. The default is false
.generateUrl()
generateUrl(navigationContext, route).then((url) => this.path = url);
Given a route, returns a Promise
to a string URL.
subscribe()
const callback = (route, data) => console.log(route, data);
this.subscription = subscribe(navigationContext, callback);
// .....
if (this.subscription) {
this.subscription.unsubscribe();
}
Pass in a callback function that will get executed every time the navigation state changes. Receive a Promise
to an observer with an unsubscribe()
function in return.
The callback function takes these arguments:
route
: The new current route.data
: Extra data associated with the current route. In the default implementation, this is the route definition which matches the current route. It will be null in the case of Simple Route (i.e.: no match).webruntime-link
A LWC used for declarative navigation. It can take either a string URL or a route as input.
<webruntime-link path="/a/path?filter=all">
<span>CLICK HERE</span>
</webruntime-link>
<webruntime-link route="{route}">
<img src="/my/link.gif" alt="click me" />
</webruntime-link>
The following CSS variables are used to style the internal a
tag. Overwrite the values to update the look of the link
s.
a {
color: var(--webruntime-link-color);
font-size: var(--webruntime-link-font-size);
text-decoration: var(--webruntime-link-text-decoration);
}
a:hover,
a:active {
color: var(--webruntime-link-color-active, var(--webruntime-link-color));
font-size: var(--webruntime-link-font-size-active, var(--webruntime-link-font-size));
text-decoration: var(--webruntime-link-text-decoration-active);
}
import { createRouter } from 'webruntime/navigation';
const ROUTE_DEFINITIONS = [
{
id: 'recordPage',
path: '/r/:objectApiName/:recordId/:actionName',
},
{
id: 'objectPage',
path: '/o/:objectApiName/:actionName',
},
{
id: 'homePage',
path: '/', // This is the default route, and it must be last.
},
];
const router = createRouter({ basePath: '/demo', routes: ROUTE_DEFINITIONS });
// Add logger pre and post navigation hook listeners. Then connect.
router
.addPreNavigate((t) => console.log(`pre: Current: %o. Next: %o`, t.current.route, t.next.route))
.addPostNavigate((t) =>
console.log(`post: Current: %o. Previous: %o`, t.current.route, t.previous.route)
)
.addErrorNavigate((e) =>
console.error(`There was a problem during navigation: ${e.code} :: ${e.message}`)
)
.connect();
webruntime/navigation
APIsimport { track, wire, LightningElement } from 'lwc';
import { navigate, generateUrl, subscribe, NavigationContext } from 'webruntime/navigation';
const aRoute = {
id: 'page',
attributes: {
name: 'sample',
},
};
export default class Example extends LightningElement {
@track name = '';
@track path = null;
subscription = null;
// Get a reference to the navigation context for this component.
@wire(NavigationContext)
navContext;
// Subscribe to updates on the current state.
connectedCallback() {
this.subscription = subscribe(this.navContext, (route, data) => {
this.name = route.attributes.name || '';
this.path = data ? data.path : null;
});
}
// Navigate programmatically by URL.
navUrl() {
navigate(this.navContext, '/some/path');
}
// Navigate programmatically by route.
navRoute() {
navigate(this.navContext, aRoute);
}
// Generate a URL for a route.
getUrl() {
generateUrl(this.navContext, aRoute).then((url) => console.log(url));
}
// Disconnect from the navigation event subscription.
disconnectedCallback() {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
}
A router has plug-points that accept logic to override the default behavior. The translation layer (route <-> URL) and navigation event handling can be customized. Pass the plug-points in as properties on a router's config object.
f(url, defaultImpl) -> { route, data }
: Given a URL (absolute or relative), return the associated route and data. If a route could not be found to match the given URL then route: null
.f(route, defaultImpl) -> { url, data }
: Given a route, return the associated relative URL string with an absolute path and data. If a URL could not be created for the given route then url: null
.Things to note:
null
for a url
or route
will result in the errorNavigation
hook listeners being call on all routers.getRouteFromUrl
will never return route: null
. A Simple Route is returned for URLs that do not match any route definition. See this example for turning off Simple Routes.f(url | route, shouldReplace) -> boolean
: This is called whenever a navigation event bubbles up through a router. Return false to stop propagation and cancel the event. This function is a no-op by default. The arguments are:
url
| route
: The string URL or route object passed in from navigate()
.shouldReplace
: true
if the URL should replace the current one in the browser history.import { LightningElement } from 'lwc';
import { createRouter } from 'webruntime/navigation';
import ROUTE_DEFINITIONS from './routeDefinitions';
export default class Example extends LightningElement {
constructor() {
super();
this.router = createRouter({
basePath: '/demo',
routes: ROUTE_DEFINITIONS,
handleNavigation: this.handleNavigation.bind(this),
getRouteFromUrl: this.getRouteFromUrl.bind(this),
getUrlFromRoute: this.getUrlFromRoute.bind(this),
});
this.router.connect();
}
disconnectedCallback() {
this.router.disconnect();
}
// Process a navigation event bubbling up from descendent components.
// The getRouteFromUrl function is provided for convenience.
// This example stops propagation if the route id is 'no__good'.
handleNavigation(input, options, getRouteFromUrl) {
const route = typeof input === 'string' ? getRouteFromUrl(input).route : input;
return route.id !== 'no__good';
}
// Return null as the route instead of Simple Routes.
// This turns off the Simple Route feature.
// The errorNavigate hook listeners are run when { route: null }
getRouteFromUrl(url, defaultImpl) {
const defaultInfo = defaultImpl(url);
return defaultInfo.route.type === 'standard__simpleRoute'
? { route: null, data: null }
: defaultInfo;
}
// Set a sticky query parameter on every URL.
getUrlFromRoute(route, defaultImpl) {
const { url, routeDef } = defaultImpl(route);
return {
url: url ? `${url}?new=param` : null,
data: routeDef,
};
}
}
Multiple routers can be nested, with each router having up to 1 child. Each router is responsible for parsing part of the URL path, and providing the associated route to its descendent subscribers.
webruntime-router
<webruntime-router
routes="{routes}"
base-path="/base"
no-history
case-sensitive
onprenavigate="{preNavigate}"
onpostnavigate="{postNavigate}"
onerrornavigate="{errorNavigate}"
onhandlenavigation="{handleNavigation}"
></webruntime-router>
Create a child router declaratively with this LWC. Pass the config options to the router via @api properties:
routes
base-path
no-history
case-sensitive
Attach hooks with event listeners:
onprenavigate
is cancelableonpostnavigate
onerrornavigate
onhandlenavigation
is cancelablewebruntime-router
<template>
<webruntime-router
routes="{routes}"
base-path="/demo"
onprenavigate="{preNavigate}"
onpostnavigate="{postNavigate}"
onerrornavigate="{errorNavigate}"
onhandlenavigation="{handleNavigation}"
>
<template if:true="{message}"><error>{message}</error></template>
<webruntime-link path="/a/place">I'm inside the child navigation context</webruntime-link>
<view></view>
</webruntime-router>
</template>
import { track, LightningElement } from 'lwc';
import ROUTE_DEFINITIONS from './routeDefinitions';
import { user } from '../services/user';
import { getData } from '../services/xhr';
export default class Example extends LightningElement {
@track errorMessage = null;
routes = ROUTE_DEFINITIONS;
// Add lifecycle hook events.
preNavigate(e) {
e.stopPropagation();
// If any pre-navigate hook returns false, cancel the event.
if (!this.authorization(e.detail) || !this.unmountCheck(e.detail)) {
e.preventDefault();
}
}
postNavigate(e) {
e.stopPropagation();
this.logNav(e.detail);
this.getData(e.detail);
}
errorNavigate(e) {
e.stopPropagation();
this.showError(e.detail);
this.getData(e.detail);
}
handleNavigation(e) {
e.stopPropagation();
const { input } = e.detail;
console.log('Bubbling navigation event: ', input);
}
// Do not allow navigation to private pages for guest users.
authorization(transaction) {
if (user.isGuest() && transaction.next.route.id === 'private-page') {
return false;
}
return true;
}
// Warn users before leaving the current page.
// !transaction.current.route indicates a first time page load.
unmountCheck(transaction) {
return !transaction.current.route
? true
: new Promise((resolve) => {
resolve(confirm('Are you sure you want to leave?'));
});
}
// Log navigation events that just completed.
// Clear the error message.
logNav(transaction) {
console.log(
`postNav -> Current: %o. Previous: %o`,
transaction.current.route,
transaction.previous.route
);
this.errorMessage = null;
}
// Get some XHR data before the subscribers are notified of this navigation event.
getData(transaction) {
return getData(transaction.current.route.id).then((data) => doSomething(data));
}
// Show navigation errors to the user.
showError(e) {
this.errorMessage = `There was a problem during navigation: ${e.message}`;
}
}
webruntime-router
relationshipWhen a child router is added, there are two navigation context providers present in the application. Consider a root router with these properties:
basePath: '/lightning';
routes: [
{
id: 'app',
path: '/app/:appName',
exact: false, // This allows the child router at this route definition
},
];
a child router with:
basePath: '';
routes: [
{
id: 'page',
path: '/page/:name',
},
];
and a URL like this:
/lightning/app/cat+app/page/siamese?cute=yes
A successful navigation event looks like this:
preNavigate
hooks are fired from root -> leaf router node.{
"id": "app",
"attributes": {
"appName": "cat app"
},
"state": {
"cute": "yes"
}
}
{
"id": "page",
"attributes": {
"name": "siamese"
},
"state": {
"cute": "yes"
}
}
postNavigate
hooks are fired on the root and its subscribers are notified.postNavigate
hooks are fired on the child and its subscribers are notified.Note: Subscribers receive different data depending on their navigation context, provided by the closest ancestor router.
The LWR Routing Service allows a lwc to be specified for each route definition. LWR also provides a component which automatically displays the lwc associated with each route. To use the Routing Service, add it to the services
array in webruntime-app.config.js:
const { RoutingService } = require('@webruntime/navigation');
module.exports = {
services: [RoutingService],
};
Create one or more JSON files in a LWR project to hold route defintion data:
projectRoot
βββ routes/
β βββ cooking.json // route set id = "cooking"
β βββ child.json // route set id = "child"
Note: Multiple files are created to support nested routers.
Each file holds a "route set" with an ID matching the filename. The route set JSON looks like this:
{
"home": {
"path": "/",
"component": "x/home"
},
"recipe": {
"path": "/recipes/:title",
"component": "x/recipeItem"
}
}
Note: The route definition IDs are keys in this object. This automatically supports ID uniqueness across the route set.
The LWR Routing Service generates a router component, given a route set ID:
<!-- x/app template -->
<template>
<!-- Add a router to the application template
This router receives the data from cooking.json above -->
<webruntime-router-cooking></webruntime-router-cooking>
</template>
The webruntime-router-{setID}
components support the same properties and events as the webruntime-router
component, besides routes
which is passed automatically given the route set ID.
The route sets contain a component for each route definition. Add a webruntime-outlet
component under a router to automatically display the component on route change:
<!-- x/app template -->
<template>
<!-- Add a router to the application template
This router receives the data from cooking.json above -->
<webruntime-router-cooking>
<!-- Add an outlet as a child to the router
This renders the current component view -->
<webruntime-outlet>
<span slot="error">
<x-error></x-error>
</span>
<span slot="404">
<x-404></x-404>
</span>
</webruntime-outlet>
</webruntime-router-cooking>
</template>
The webruntime-outlet
component contains:
refocus-off
: The outlet automatically puts focus on the component when it is loaded, for accessibility. To turn this feature off, add the refocus-off
property to webruntime-outlet
The attributes for the current route are automatically passed into the corresponding component as public properties (@api
). Given this route:
{
"id": "recipe",
"attributes": {
"title": "bread"
}
}
and the "x/recipeItem" component:
import { LightningElement, api } from 'lwc';
export default class XRecipeItem extends LightningElement {
@api title;
}
the router outlet would pass in "bread" for the title property:
<x-recipe-item title="bread"></x-recipe-item>
The webruntime/lightningNavigation
package provides implementations for the NavigationMixin
and CurrentPageReference
wire adapter from lightning/navigation.
import { NavigationMixin, CurrentPageReference } from 'webruntime/lightningNavigation';
CurrentPageReference
A wire adapter that gives a component access to the current page reference (relative to its navigation context).
NavigationMixin
A JavaScript class mixin that provides functions to navigate or generate a URL:
this[NavigationMixin.Navigate](url | pageRef)
: Programmatically navigate to a string URL or page reference.this[NavigationMixin.GenerateUrl](pageRef) => Promise<string>
: Translate a page reference into an absolute path.NavigationMixin
and CurrentPageReference
This example is analogous to the webruntime/navigation
API example.
import { wire, LightningElement } from 'lwc';
import { NavigationMixin, CurrentPageReference } from 'webruntime/lightningNavigation';
const aPageRef = {
type: 'page',
attributes: {
name: 'sample',
},
};
export default class Example extends NavigationMixin(LightningElement) {
// Subscribe to updates on the current state.
@wire(CurrentPageReference)
currentPageRef;
// Use the current page reference.
@track
get name() {
return this.currentPageRef ? this.currentPageRef.attributes.name : '';
}
// Navigate by URL.
navUrl() {
this[NavigationMixin.Navigate]('/some/path');
}
// Navigate by page reference.
navPageRef() {
this[NavigationMixin.Navigate](aPageRef);
}
// Generate a URL.
getUrl() {
this[NavigationMixin.GenerateUrl](aPageRef).then((url) => console.log(url));
}
}
provideContext()
provideContext(context, node)
Using the createRouter()
API will create a Router
with navigation context automatically. To create custom navigation context, call the provideContext()
API with the following arguments:
context
: The provided context must have exactly these properties:
navigate
: An implementation of the navigate()
function.generateUrl
: An implementation of the generateUrl()
function.subscribe
: An implementation of the subscribe()
function.An object is returned containing these properties:
id
: An identifier for the navigation context. This is the value returned over the NavigationContext
wire adapter.update(value)
: Update the context
object to a new value. Subscribers will be updated with the new API implementations.disconnect()
: Stop the navigation context from being detected by descendant components.In a case where an application only has one router, the application developers can consider providing their users with Navigation APIs which are locked to the single navigation context. Then users do not need to pull in the navigation context themselves.
import {
navigate as webruntimeNavigate,
generateUrl as webruntimeGenerateUrl,
subscribe as webruntimeSubscribe,
createRouter
} from 'webruntime/navigation';
// Create and start the router.
const router = createRouter({...});
// Navigate programmatically.
export function navigate(loc, options) {
webruntimeNavigate(router.id, loc, options);
}
// Generate a URL for the given route.
export function generateUrl(route) {
return webruntimeGenerateUrl(router.id, route);
}
// Subscribe to navigation state changes.
export function subscribe(callback) {
webruntimeSubscribe(router.id, callback);
}
yarn add @webruntime/navigation
Build tasks can be run in this repository from the /packages/@webruntime/navigation directory with:
yarn clean
yarn build
Test tasks can be run in this repository from the /packages/@webruntime/navigation directory with:
yarn test // run jest tests
yarn coverage // test with coverage output
yarn lint // lint the code
A routing goal is to be able to statically analyze by URL path. Meaning, the code needed to display a component/view/page for a given path can be known ahead of runtime. Route definitions should contain the metadata needed for a builder plugin to do the analysis. A different builder plugin is written to analyze different styles of route definition metadata. Example route definitions:
{
"id": "home",
"path": "/home",
"component": "our-home"
}
{
"path": "/case/:recordId",
"page": {
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case"
}
}
}
{
"id": "path-detail",
"path": "/case/:recordId",
"view": "caseDetail",
"label": "Case Detail",
"page": {
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case"
}
}
}
Matching a route to a route definition is relatively straight forward, since the id
s are unique. Here is the criteria:
route.id === routeDef.id
/:recordId
) in the route definition path
, must match a key in the route attributes
Here is an example:
// route definition
{
"id": "home",
"path": "/app/:appName"
}
// matching route
{
"id": "home",
"attributes": {
"appName": "awesome"
}
}
// NOT matching route
{
"id": "home"
}
Matching a page reference is more complex. Here is the criteria:
pageRef.type === routeDef.page.type
/:recordId
) in the route definition path
, must match a key in the page reference attributes
page.attributes
, must match a key/value pair in the page reference attributes
Here are some examples:
attributes
:// page reference
{
"type": "home"
}
// matching route definition
{
"path": "/home",
"page": {
"type": "home"
}
}
// NOT matching route definition
{
"path": "/old/home",
"page": {
"type": "home"
}
}
path
:// page reference
{
"type": "standard__recordPage",
"attributes": {
"recordId": "0D50M00004NgNxtSAF"
}
}
// matching route definition
{
"path": "/record/:recordId",
"page": {
"type": "standard__recordPage"
}
}
path
containing optional parameters:// page reference
{
"type": "standard__recordPage",
"attributes": {
"recordId": "0D50M00004NgNxtSAF"
}
}
// matching route definition
{
"path": "/record/:recordId/:recordName?",
"page": {
"type": "standard__recordPage"
}
}
page.attributes
:// page reference
{
"type": "standard__objectPage",
"attributes": {
"objectApiName": "Case"
}
}
// matching route definition
{
"path": "/cases",
"page": {
"type": "standard__objectPage",
"attributes": {
"objectApiName": "Case"
}
}
}
path
and page.attributes
:// page reference
{
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case",
"recordId": "0D50M00004NgNxtSAF"
}
}
// matching route definition
{
"path": "/case/:recordId",
"page": {
"type": "standard__recordPage",
"attributes": {
"objectApiName": "Case"
}
}
}
Programmatic navigation events flow up from their source node through each navigation context. If not stopped, they eventually reach the root router, which processes the event.
The URL is then parsed starting at the root router and flowing down through any child routers. First the preNavigate
hook listeners are run, from root to leaf. If all return true, the postNavigate
hook listeners are run, then each router hydrates their subscribers with a new route/page reference.
FAQs
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Product
Automatically fix and test dependency updates with socket fixβa new CLI tool that turns CVE alerts into safe, automated upgrades.
Security News
CISA denies CVE funding issues amid backlash over a new CVE foundation formed by board members, raising concerns about transparency and program governance.
Product
Weβre excited to announce a powerful new capability in Socket: historical data and enhanced analytics.