@jack-henry/web-component-router
Router for web-components based apps. The router creates a
dom node tree and assigns attributes to the nodes based on path segments.
When switching between routes, the router will re-use any element in
common between the trees and simply update the attributes of existing
elements. Elements should change/reset their state based solely off of
attributes.
By default, the router places child elements as the sole light-dom child
of the parent (all other nodes are removed). Elements can override the
routeEnter
method functionality to customize this behavior.
Installation
npm install @jack-henry/web-component-router
Defining Routes
The router uses Page.js internally
for route path definitions and callbacks. You must start by defining your
routes and route tree.
To create a tree you create RouteTreeNodes
and add children.
import RouteTreeNode from '@jack-henry/web-component-router/lib/route-tree-node.js';
const routeNode = new RouteTreeNode(data);
Each node requires a RouteData
object to describe it.
import RouteData from '@jack-henry/web-component-router/lib/route-data.js';
const routeData = new RouteData(
'Name of this route',
'tag-name',
'/path/:namedParameter',
['namedParameter'],
true,
() => import('../tag-name.js'));
It is recommended to use enums and module imports to define the paths
and ids so the strings are maintainable.
Example Routing Configuration
import {RouteData, RouteTreeNode} from '@jack-henry/web-component-router';
const dashboard = new RouteTreeNode(
new RouteData('MainDashboard', 'MAIN-DASHBOARD', '/'));
const detailView = new RouteTreeNode(
new RouteData('DetailView', 'DETAIL-VIEW', '/detail/:viewId', ['viewId']));
const mainLayout = new RouteTreeNode(
new RouteData('MainLayout', 'MAIN-LAYOUT', ''));
mainLayout.addChild(dashboard);
mainLayout.addChild(detailView);
const app = new RouteTreeNode(
new RouteData('App', 'APP-ELEMENT', '', [], false));
app.addChild(mainLayout);
const loginPage = new RouteTreeNode(
new RouteData('Login', 'LOGIN-PAGE', '/login', [], false));
app.addChild(loginPage);
export default app;
Defining a route configuration in the Router's constructor
Alternatively you can pass a routeConfig
object when instantiating your router. This will use the RouteTreeNode
and RouteData
to create your applications routeTree.
Example RouteConfig object
const routeConfig = {
id: 'app',
tagName: 'APP-MAIN',
path: '',
subRoutes: [{
id: 'app-user',
tagName: 'APP-USER-PAGE',
path: '/users/:userId([0-9]{1,6})',
params: ['userId'],
beforeEnter: () => import('../app-user-page.js')
}, {
id: 'app-user-account',
tagName: 'APP-ACCOUNT-PAGE',
path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})',
params: ['userId', 'accountId'],
beforeEnter: () => import('../app-account-page.js')
}, {
id: 'app-about',
tagName: 'APP-ABOUT',
path: '/about',
authenticated: false,
beforeEnter: () => import('../app-about.js')
}]
};
const router = New Router(routeConfig);
When using this method the default is that a route requires authentication, as shown above in the 'about' route, set authenticated
to false to create a route which does not require authentication.
Redirecting
To programmatically redirect to a page, use router.go()
:
router.go('/');
router.go('/detail/:viewId', {'viewId': id});
router.go('/login', {'redirect': destAfterLogin});
Note: router.go
usage can quickly become an anti pattern. Using proper HTML anchors with
hrefs is preferable. router.go
should only be used when programatic route changes are strictly
required.
Creating Routing Enabled Components
Components used with the router are expected to define two methods
which take the same arguments:
class MyElement extends HtmlElement {
async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
context.handled = true;
const currentElement = currentNode.getValue().element;
}
async routeExit(currentNode, nextNode, routeId, context) {
const currentElement = currentNode.getValue().element;
if (currentElement.parentNode) {
currentElement.parentNode.removeChild( (currentElement));
}
currentNode.getValue().element = undefined;
}
}
Most elements will either use (or inherit) the default implementations.
Two mixins are provided to make this easy. When
using the mixin, routeEnter
and routeExit
methods are only need defined
when the default behavior needs modified. In most cases any overridden
method should do minimal work and call super.routeEnter
or super.routeExit
.
Standard Routing Mixin
import routeMixin from '@jack-henry/web-component-router/routing-mixin.js';
class MyElement extends routeMixin(HTMLElement) { }
Animated Routing Mixin
The animated mixin applies a class to animated a node tree on entrance.
Exit animations are currently not supported.
import animatedRouteMixin from '@jack-henry/web-component-router/animated-routing-mixin.js';
class MyElement extends animatedRouteMixin(HTMLElement, 'className') { }
Root App Element
The routing configuration is typically defined inside the main app element
which should be defined as the root node of the routing tree.
The root element typically has a slightly different configuration.
import myAppRouteTree from './route-tree.js';
import router, {Context, routingMixin} from '@jack-henry/web-component-router';
class AppElement extends routingMixin(Polymer.Element) {
static get is() { return 'app-element'; }
connectedCallback() {
super.connectedCallback();
router.routeTree = myAppRouteTree;
router.routeTree.getValue().element = this;
router.start();
}
async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
context.handled = true;
const destinationNode = router.routeTree.getNodeByKey(routeId);
if (isAuthenticated || !destinationNode.requiresAuthentication()) {
return super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
}
router.go('/login');
return false;
}
async routeExit(currentNode, nextNode, routeId, context) {
}
}
Saving Scroll Position
When using the back button for navigation, the previous route scroll
position should be preserved. To accomplish this, we use a global
page.js exit callback. However, care must be taken as saving the scroll
position should only occur on normal navigation. Back/Forward browser
navigation should not save the scroll position as it causes a timing
issue.
import myAppRouteTree from './route-tree.js';
import router, {routingMixin} from '@jack-henry/web-component-router';
class AppElement extends routingMixin(Polymer.Element) {
static get is() { return 'app-element'; }
connectedCallback() {
super.connectedCallback();
router.routeTree = myAppRouteTree;
router.routeTree.getValue().element = this;
router.addGlobalExitHandler(this.saveScrollPosition_.bind(this));
router.start();
}
saveScrollPosition_(context, next) {
if (!(router.nextStateWasPopped || 'scrollTop' in context.state)) {
context.state['scrollTop'] = this.scrollTop;
context.save();
}
next();
}
async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
setTimeout(() => {
this.scrollTop = context.state['scrollTop'] || 0;
}, 0);
return super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
}
}
Router Reference
router.routeTree;
router.currentNodeId;
router.prevNodeId;
router.start();
router.go(path, params);
router.addGlobalExitHandler(callback);
router.addRouteChangeStartCallback(callback);
router.removeRouteChangeStartCallback(callback);
router.addRouteChangeCompleteCallback(callback);
router.removeRouteChangeCompleteCallback(callback);
const urlPath = router.getRouteUrlWithoutParams(context);