Skip to content

Commit

Permalink
Merge pull request #67 from Banno/router-constructor-config
Browse files Browse the repository at this point in the history
Router constructor config
  • Loading branch information
chrisgubbels authored Oct 25, 2023
2 parents 67b67a6 + de249b5 commit e1426e9
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 17 deletions.
59 changes: 46 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,39 @@ 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'],
}, {
id: 'app-user-account',
tagName: 'APP-ACCOUNT-PAGE',
path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})',
params: ['userId', 'accountId'],
}, {
id: 'app-about',
tagName: 'APP-ABOUT',
path: '/about',
authenticated: false,
}]
};
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()`:
Expand Down Expand Up @@ -151,7 +184,7 @@ class MyElement extends HtmlElement {
// do something with the node
const currentElement = currentNode.getValue().element;
}

/**
* Implementation for the callback on exiting a route node.
* This method is ONLY called if this element is not being
Expand Down Expand Up @@ -209,34 +242,34 @@ 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;
// Define this instance as the root element
router.routeTree.getValue().element = this;

// Start routing
router.start();
}

async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
context.handled = true;
const destinationNode = router.routeTree.getNodeByKey(routeId);
if (isAuthenticated || !destinationNode.requiresAuthentication()) {
// carry on. user is authenticated or doesn't need to be.
return super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
}

// Redirect to the login page
router.go('/login');

// Don't continue routing - we have redirected to the
// login page
return false;
}

async routeExit(currentNode, nextNode, routeId, context) {
// This method should never be called. The main app element
// should never be on an exit path as it should always be in
Expand All @@ -260,21 +293,21 @@ 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;
// Define this instance as the root element
router.routeTree.getValue().element = this;

// Save the scroll position for every route exit
router.addGlobalExitHandler(this.saveScrollPosition_.bind(this));

// Start routing
router.start();
}

/**
* @param {!Context} context
* @param {function(boolean=)} next
Expand All @@ -287,7 +320,7 @@ class AppElement extends routingMixin(Polymer.Element) {
}
next();
}

async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
// Restoring the scroll position needs to be async
setTimeout(() => {
Expand Down Expand Up @@ -346,7 +379,7 @@ router.addGlobalExitHandler(callback);
router.addRouteChangeStartCallback(callback);

/**
* Unregister a callback function
* Unregister a callback function
* @param {!Function} callback
*/
router.removeRouteChangeStartCallback(callback);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jack-henry/web-component-router",
"version": "3.0.0",
"version": "3.0.1",
"description": "Web Components Router",
"main": "router.js",
"type": "module",
Expand Down
27 changes: 25 additions & 2 deletions router.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@
* B C E
*/

/**
* @typedef {Object} RouteConfig
* @property {string} id
* @property {string} tagName
* @property {string} path
* @property {Array<string>=} parameters
* @property {boolean=} authenticated
* @property {Array<RouteConfig>=} subRoutes
*/

import {Context, Page} from './lib/page.js';
import RouteTreeNode from './lib/route-tree-node.js';
import routingMixin from './lib/routing-mixin.js';
Expand All @@ -23,15 +33,16 @@ import BasicRoutingInterface from './lib/routing-interface.js';
import RouteData from './lib/route-data.js';

class Router {
constructor() {
/** @param {RouteConfig=} routeConfig */
constructor(routeConfig) {
/** @type {string|undefined} */
this.currentNodeId_;

/** @type {string|undefined} */
this.prevNodeId_;

/** @type {!RouteTreeNode|undefined} */
this.routeTree_;
this.routeTree_ = routeConfig ? this.buildRouteTree(routeConfig) : undefined;

this.nextStateWasPopped = false;

Expand Down Expand Up @@ -68,6 +79,18 @@ class Router {
return this.prevNodeId_;
}

/** @param {!RouteConfig} routeConfig */
buildRouteTree(routeConfig) {
const authenticated = [true, false].includes(routeConfig.authenticated) ? routeConfig.authenticated : true;
const node = new RouteTreeNode(new RouteData(routeConfig.id, routeConfig.tagName, routeConfig.path, routeConfig.parameters || [], authenticated));
if (routeConfig.subRoutes) {
routeConfig.subRoutes.forEach(route => {
node.addChild(this.buildRouteTree(route));
});
}
return node;
}

/**
* Build the routing tree and begin routing
* @return {void}
Expand Down
60 changes: 59 additions & 1 deletion test/router-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import testRouteTree from './utils/testing-route-setup.js';
import testRouteConfig from './utils/test-route-config.js';
import Router, {Context, RouteTreeNode} from '../router.js';
import RoutedElement from './fixtures/custom-fixture.js';

Expand All @@ -19,7 +20,7 @@ function JSCompiler_renameProperty(propName, instance) {
}

describe('Router', () => {
const router = new Router();
let router = new Router();

const A = testRouteTree.tree.getNodeByKey(testRouteTree.Id.A);
const B = testRouteTree.tree.getNodeByKey(testRouteTree.Id.B);
Expand Down Expand Up @@ -86,6 +87,63 @@ describe('Router', () => {
expect(router.currentNodeId_).toBe(testRouteTree.Id.B);
});

describe('Router constructor', () => {
afterAll(() => {
// reset routeTree
router = new Router();
});

it('should leave the routeTree undefined if instantiated without a route configuration', () => {
router = new Router();
expect(router.routeTree).toBe(undefined);
});

it('should create the routeTree when instantiated with the route configuration', () => {
router = new Router(testRouteConfig);
expect(router.routeTree).not.toBe(undefined);
});
});

describe('buildRouteTree', () => {
const testSubRouteData = [{
id: 'app-user',
tagName: 'APP-USER-PAGE',
path: '/users/:userId([0-9]{1,6})',
requiresAuthentication: true,
}, {
id: 'app-user-account',
tagName: 'APP-ACCOUNT-PAGE',
path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})',
requiresAuthentication: true,
}, {
id: 'app-about',
tagName: 'APP-ABOUT',
path: '/about',
requiresAuthentication: false,
}];

it('should create a routeTree with the correct properties', () => {
const routeTree = router.buildRouteTree(testRouteConfig);
const subRoutes = routeTree.getChildren();
expect(routeTree.requiresAuthentication()).toBe(true);
expect(routeTree.getKey()).toBe('app');
expect(subRoutes.length).toBe(3);
subRoutes.forEach((route, index) => {
const data = route.getValue();
['id', 'tagName', 'path', 'requiresAuthentication'].forEach((prop) => {
expect(data[prop]).toBe(testSubRouteData[index][prop]);
})
});
});

it('should set authentication to true by default', () => {
const routeTree = router.buildRouteTree(testRouteConfig);
const subRoutes = routeTree.getChildren();
expect(subRoutes[0].getValue().requiresAuthentication).toBe(true);
expect(subRoutes[2].getValue().requiresAuthentication).toBe(false);
});
});

describe('url()', () => {
it('should return the path if there are no other parameters', () => {
expect(router.url('/A')).toBe('/A');
Expand Down
23 changes: 23 additions & 0 deletions test/utils/test-route-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const testRouteConfig = {
id: 'app',
tagName: 'APP-MAIN',
path: '',
subRoutes: [{
id: 'app-user',
tagName: 'APP-USER-PAGE',
path: '/users/:userId([0-9]{1,6})',
params: ['userId'],
}, {
id: 'app-user-account',
tagName: 'APP-ACCOUNT-PAGE',
path: '/users/:userId([0-9]{1,6})/accounts/:accountId([0-9]{1,6})',
params: ['userId', 'accountId'],
}, {
id: 'app-about',
tagName: 'APP-ABOUT',
path: '/about',
authenticated: false,
}]
};

export default testRouteConfig;

0 comments on commit e1426e9

Please sign in to comment.