npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jack-henry/web-component-router

v3.9.0

Published

Web Components Router

Downloads

209

Readme

@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)
 * @param {function():(Promise<unknown>|undefined)=} 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,
    () => 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

/**
 * @fileoverview
 *
 * Route tree definition
 *
 *           ___APP-ELEMENT___
 *          /                 \
 *     LOGIN-PAGE     ___MAIN-LAYOUT_____
 *                   /                   \
 *           MAIN-DASHBOARD         DETAIL-VIEW
 */

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']));

// 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;

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():

// 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, {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
    // 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, {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
   * @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(!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 {!Context} context route enter context
 * @return {!string}
 */
const urlPath = router.getRouteUrlWithoutParams(context);