Never Publish Malarkey

    @jack-henry/web-component-router

    1.0.2 • Public • Published

    @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';
    /**
     * @param {string} name of this route. Must be unique.
     * @param {string} tagName of the element. Case insensitive.
     * @param {string} path of this route (express style).
     *     Empty strings indicate abstract routes - they are not
     *     directly routable. Their callbacks are invoked and elements
     *     created when a child route is activated.
     * @param {!Array<string>=} namedParameters array in camelCase (optional).
     *     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)
     */
    const routeData = new RouteData(
        'Name of this route',
        'tag-name',
        '/path/:namedParameter',
        ['namedParameter'], // becomes attribute named-parameter="value"
        true);

    It is recommended to use enums and module imports to define the paths and ids so the strings are maintainable.

    Example Routing Configuration

    /**
     * @fileoverview
     *
     * Route tree definition
     *
     *           ___APP-ELEMENT___
     *          /                 \
     *     LOGIN-PAGE     ___MAIN-LAYOUT_____
     *                   /                   \
     *           MAIN-DASHBOARD         DETAIL-VIEW
     */
    
    import RouteData from '@jack-henry/web-component-router/lib/route-data.js';
    import RouteTreeNode from '@jack-henry/web-component-router/lib/route-tree-node.js';
    
    const dashboard = new RouteTreeNode(
        new RouteData('MainDashboard', 'MAIN-DASHBOARD', '/'));
    
    const detailView = new RouteTreeNode(
        new RouteData('DetailView', 'DETAIL-VIEW', '/detail/:viewId', ['viewId']));
    
    // This is an abstract route - you can't visit it directly.
    // However it is part of the dom tree
    const mainLayout = new RouteTreeNode(
        new RouteData('MainLayout', 'MAIN-LAYOUT', ''));
    
    mainLayout.addChild(dashboard);
    mainLayout.addChild(detailView);
    
    // This is an abstract route - you can't visit it directly.
    // However it is part of the dom tree
    // It also does not require authentication to view
    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;

    Redirecting

    To programmatically redirect to a page, use router.go():

    // Basic redirect; goes to the root page.
    router.go('/');
    
    // Specifies a value for named parameter in the path.
    // NOTE: You must quote the properties so that Closure Compiler does not rename them!
    router.go('/detail/:viewId', {'viewId': id});
    
    // Adds a query parameter to the URL.
    // NOTE: You must quote the properties so that Closure Compiler does not rename them!
    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 {
      /**
       * Implementation for the callback on entering a route node.
       * routeEnter is called for EVERY route change. If the node
       * is shared between the old and new routes, the element
       * will be re-used but have attributes updated here.
       *
       * @param {!RouteTreeNode} currentNode
       * @param {!RouteTreeNode|undefined} nextNodeIfExists - the
       *     child node of this route.
       * @param {string} routeId - unique name of the route
       * @param {!Context} context - page.js Context object
       * @return {!Promise<boolean=>}
       */
      async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
        // make sure to set this to indicate the route was recognized.
        context.handled = true;
        // 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
       * used by the next route destination.
       *
       * @param {!RouteTreeNode} currentNode
       * @param {!RouteTreeNode|undefined} nextNode - parent node
       * @param {string} routeId - unique name of the route
       * @param {!Context} context - page.js Context object
       */
      async routeExit(currentNode, nextNode, routeId, context) {
        const currentElement = currentNode.getValue().element;
    
        // remove the element from the dom
        if (currentElement.parentNode) {
          currentElement.parentNode.removeChild(/** @type {!Element} */ (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 from '@jack-henry/web-component-router';
    import routeMixin from '@jack-henry/web-component-router/routing-mixin.js';
    import pageJs from 'page';
    
    class AppElement extends routeMixin(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
        // common no matter what route is activated.
      }
    }

    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 from '@jack-henry/web-component-router';
    import routeMixin from '@jack-henry/web-component-router/routing-mixin.js';
    
    class AppElement extends routeMixin(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 {!pageJs.Context} context
       * @param {function(boolean=)} next
       * @private
       */
      saveScrollPosition_(context, next) {
        if (!(router.nextStateWasPopped || 'scrollTop' in context.state)) {
          context.state['scrollTop'] = this.scrollTop;
          context.save();
        }
        next();
      }
      
      async routeEnter(currentNode, nextNodeIfExists, routeId, context) {
        // Restoring the scroll position needs to be async
        setTimeout(() => {
          this.scrollTop = context.state['scrollTop'] || 0;
        }, 0);
        return super.routeEnter(currentNode, nextNodeIfExists, routeId, context);
      }
    }

    Router Reference

    /**
     * Get or define the routing tree
     * @type {!RouteTreeNode}
     */
    router.routeTree;
    
    /**
     * Get the current active route node
     * @type {string}
     */
    router.currentNodeId;
    
    /**
     * Get the previous active route node
     * @type {string}
     */
    router.prevNodeId;
    
    /**
     * Begin routing. Should only be called once. The routing tree
     * must first be defined.
     */
    router.start();
    
    /**
     * Navigate to the specified route path.
     * @param {string} path
     * @param {object=} params Values to use for named & query parameters
     */
    router.go(path, params);
    
    /**
     * Register an exit callback to be invoked on every route change
     * @param {function(!pageJs.Context, function(boolean=))} callback
     */
    router.addGlobalExitHandler(callback);
    
    /**
     * Register a callback function which will be invoked when a route change
     * is initiated.
     * @param {!Function} callback
     */
    router.addRouteChangeStartCallback(callback);
    
    /**
     * Unregister a callback function 
     * @param {!Function} callback
     */
    router.removeRouteChangeStartCallback(callback);
    
    /**
     * Register a callback function which will be invoked when a route change
     * is completed.
     * @param {!Function} callback
     */
    router.addRouteChangeCompleteCallback(callback);
    
    /**
     * Unregister a callback function
     * @param {!Function} callback
     */
    router.removeRouteChangeCompleteCallback(callback);
    
    /**
     * Anonymize the route path by replacing param values with their
     * param name. Used for analytics tracking
     *
     * @param {!pageJs.Context} context route enter context
     * @return {!string}
     */
    const urlPath = router.getRouteUrlWithoutParams(context);

    Keywords

    none

    Install

    npm i @jack-henry/web-component-router

    DownloadsWeekly Downloads

    392

    Version

    1.0.2

    License

    Apache-2.0

    Unpacked Size

    42.3 kB

    Total Files

    7

    Last publish

    Collaborators

    • bjork24
    • chadhikes