diff --git a/README.md b/README.md index 5268334..3c9fcf1 100644 --- a/README.md +++ b/README.md @@ -45,13 +45,15 @@ import RouteData from '@jack-henry/web-component-router/lib/route-data.js'; * These should match to named path segments. Each camel case name * is converted to a hyphenated name to be assigned to the element. * @param {boolean=} requiresAuthentication (optional - defaults true) + * @param {function():Promise=} beforeEnter Optionally allows you to dynamically import the component for a given route. The route-mixin.js will call your beforeEnter on routeEnter if the component does not exist in the dom. */ const routeData = new RouteData( 'Name of this route', 'tag-name', '/path/:namedParameter', ['namedParameter'], // becomes attribute named-parameter="value" - true); + true, + () => import('../tag-name.js')); ``` It is recommended to use enums and module imports to define the paths @@ -119,16 +121,19 @@ const routeConfig = { 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') }] }; diff --git a/lib/route-data.js b/lib/route-data.js index d7396fd..3280bbd 100644 --- a/lib/route-data.js +++ b/lib/route-data.js @@ -8,8 +8,9 @@ class RouteData { * @param {!Array=} namedParameters list in camelCase. Will be * converted to a map of camelCase and hyphenated. * @param {boolean=} requiresAuthentication + * @param {function():Promise=} beforeEnter */ - constructor(id, tagName, path, namedParameters, requiresAuthentication) { + constructor(id, tagName, path, namedParameters, requiresAuthentication, beforeEnter) { namedParameters = namedParameters || []; /** @type {!Object} */ const params = {}; @@ -28,6 +29,8 @@ class RouteData { /** @type {!Element|undefined} */ this.element = undefined; this.requiresAuthentication = requiresAuthentication !== false; + + this.beforeEnter = beforeEnter || (() => Promise.resolve()); } } diff --git a/lib/routing-mixin.js b/lib/routing-mixin.js index 84e55fd..930abe9 100644 --- a/lib/routing-mixin.js +++ b/lib/routing-mixin.js @@ -28,7 +28,6 @@ function routingMixin(Superclass) { async routeEnter(currentNode, nextNodeIfExists, routeId, context) { context.handled = true; const currentElement = /** @type {!Element} */ (currentNode.getValue().element); - if (nextNodeIfExists) { const nextNode = /** @type {!RouteTreeNode} */ (nextNodeIfExists); @@ -37,13 +36,13 @@ function routingMixin(Superclass) { const thisElem = /** @type {!Element} */ (/** @type {?} */ (this)); /** @type {Element} */ let nextElement = nextNodeData.element || thisElem.querySelector(nextNodeData.tagName.toLowerCase()); - // Reuse the element if it already exists in the dom. // Add a sanity check to make sure the element parent is what we expect if (!nextElement || nextElement.parentNode !== currentElement) { if (nextNodeData.tagName.indexOf('-') > 0) { let Elem = customElements && customElements.get(nextNodeData.tagName.toLowerCase()); if (!Elem) { + await nextNodeData.beforeEnter(); // When code splitting, it's possible that the element created is not yet in the registry. // Wait until it is before creating it await customElements.whenDefined(nextNodeData.tagName.toLowerCase()); @@ -74,7 +73,6 @@ function routingMixin(Superclass) { } currentElement.appendChild(nextElement); } - nextNode.getValue().element = /** @type {!Element} */ (nextElement); } catch (e) { // Internet Explorer can sometimes throw an exception when setting attributes immediately diff --git a/package.json b/package.json index 5e05207..7a539da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jack-henry/web-component-router", - "version": "3.6.0", + "version": "3.7.0", "description": "Web Components Router", "main": "router.js", "type": "module", diff --git a/router.js b/router.js index 9bc352e..53a7c21 100644 --- a/router.js +++ b/router.js @@ -23,6 +23,7 @@ * params: (Array|undefined), * authenticated: (boolean|undefined), * subRoutes: (Array|undefined), + * beforeEnter: (function():Promise|undefined) * }} RouteConfig */ let RouteConfig; @@ -84,7 +85,7 @@ class Router { /** @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.params || [], authenticated)); + const node = new RouteTreeNode(new RouteData(routeConfig.id, routeConfig.tagName, routeConfig.path, routeConfig.params || [], authenticated, routeConfig.beforeEnter)); if (routeConfig.subRoutes) { routeConfig.subRoutes.forEach(route => { node.addChild(this.buildRouteTree(route)); diff --git a/test/router-spec.js b/test/router-spec.js index f36afc8..e23cd37 100644 --- a/test/router-spec.js +++ b/test/router-spec.js @@ -112,6 +112,7 @@ describe('Router', () => { path: '/users/:userId([0-9]{1,6})', requiresAuthentication: true, params: ['userId'], + beforeEnter: () => Promise.resolve(), }, { id: 'app-user-account', tagName: 'APP-ACCOUNT-PAGE', @@ -136,6 +137,7 @@ describe('Router', () => { if (testSubRouteData[index].params) { expect(Object.keys(data.attributes)).toEqual(testSubRouteData[index].params); } + expect(data.beforeEnter).not.toBe(undefined); ['id', 'tagName', 'path', 'requiresAuthentication'].forEach((prop) => { expect(data[prop]).toBe(testSubRouteData[index][prop]); }); diff --git a/test/utils/test-route-config.js b/test/utils/test-route-config.js index 165ae05..01ace2e 100644 --- a/test/utils/test-route-config.js +++ b/test/utils/test-route-config.js @@ -7,6 +7,7 @@ const testRouteConfig = { tagName: 'APP-USER-PAGE', path: '/users/:userId([0-9]{1,6})', params: ['userId'], + beforeEnter: () => Promise.resolve(), }, { id: 'app-user-account', tagName: 'APP-ACCOUNT-PAGE',