Skip to content

AegisJSProject/router

Repository files navigation

@aegisjsproject/router

A simple but powerful router module

CodeQL Node CI Lint Code Base

GitHub license GitHub last commit GitHub release GitHub Sponsors

npm node-current npm bundle size gzipped npm

GitHub followers GitHub forks GitHub stars Twitter Follow

Donate using Liberapay


[!CRITICAL] This package requires URLPattern to be polyfilled before any paths are registered. A common polyfill recommended by MDN can be found here.

Installation

npm install @aegisjsproject/router

CDN and importmap

You do not even need to npm install this or use any build process. You may either import it directly from a CDN or use an importmap.

<script type="importmap">
{
  "imports": {
    "@aegisjsproject/router": "https://unpkg.com/@aegisjsproject/router[@version]/router.mjs",
    "@aegisjsproject/state": "https://unpkg.com/@aegisjsproject/state[@version]/state.mjs"
  }
}
</script>

Advantages of @aegisjsproject/router

  • Lightweight and Efficient: The library is designed to be small and performant, with a focus on efficient URL matching and module loading.
  • Dynamic Loading: Modules are loaded on-demand, improving initial page load performance and reducing resource usage.
  • Flexible Exports: Supports a variety of module exports, including custom elements, functions, and HTML structures, making it adaptable to different UI architectures.
  • Component Injection: Automatically injects relevant URL information and state into registered components, simplifying component development and data management.
  • History Integration: Seamlessly manages browser history, allowing users to navigate back and forward without reloading the entire page.
  • Error Handling: Provides built-in error handling mechanisms to gracefully handle unexpected situations during module loading or navigation.
  • Customizable: Offers flexibility for customization, allowing you to tailor the router's behavior to your specific project requirements.
  • Easy to Use: The library provides a simple and intuitive API, making it easy to learn and integrate into your projects.

Fundamentals

At its core, this package matches URLs matching a URLPattern to modules to be dynamically imported. This yields a powerful but minimal package size, dynamic loading of "View"s as-needed, high reusability of code, and potentially nearly instant navigations, especially when used in conjunction with service workers and caches. Just create a script that has a default export that is a Document, DocumentFragment, HTMLElement and especially a custom element or web component, and map the URLPatterns to their respective modules.

Example

import { init, navigate, back, forward, reload } from '@aegisjsproject/router';

init({
  '/product/:productId': '@scope/views/product.js',
  '/test/': '@scope/views/home.js',
  '/search?q=:query': '@scope/views/search.js',
  '/img': '/views/img.js',
  '/path/page-:page(\\d+)': '@scope/foo.js',
}, {
  preload: true, // Preload all registered modules
  notFound: './views/404.js', // Set custom 404 module
  rootNode: '#root', // Declares element for base of content updates
  interceptRoot: document.body, // Use `MutationObserver` to observer `<a>` elements and intercept navigations
  signal: controller.signal, // An `AbortSignal`, should you want to disable routing funcitonality
});

document.querySelectorAll('[data-link]').forEach(el => {
  el.addEventListener('click', ({ currentTarget }) => {
    const { link, ...state } = currentTarget.dataset;
    navigate(link, state);
  });
});

document.querySelectorAll('[data-nav]').forEach(el => {
  el.addEventListener('click', ({ currentTarget }) => {
    switch (currentTarget.dataset.nav) {
      case 'back':
        back();
        break;

      case 'forward':
        forward();
        break;

      case 'reload':
        reload();

      default:
        throw new TypeError(`Invalid nav button type: ${currentTarget.dataset.nav}.`);
    }
  });
});

Registering Paths

At the core, this router module just uses URLPatterns in a map, mapped to a source for a module. When a URL is navigated to, it finds the pattern that the URL matches, dynamically imports that module, and passes the current state and URL and the results of urlPattern.exec(url) to the function or constructor.

You may register paths via either registerPath() or through an object given to the init() function. registerPath() allows for the use of new URLPattern() to be used, but as init() requires an object, its keys must be strings to be converted into URLPattern through new URLPattern(key, moduleSrc).

Handling Navigation

If you call the init() function, the popstate listener will be added automatically and the module for the current page will be loaded. Should you want more manual loading, you may also call addListener() on your own.

There is also a MutationObserver that adds click event handlers to intercept clicks on same-origin <a>s. This observer watches for <a>s in the children of what it is set to observe and calls event.preventDefault() to avoid the default navigation, then calls navigate(a.href).

Note

While the MutationObserver automatically adds the necessary click handlers on all <a> and <form> elements under its root, it cannot reach into Shadow DOM. For any web component with shadow, you should call interceptNav(shadow) in either the constructor or connectedCallback.

Cleanup

For all "Views"/modules that export a function or constructor, they are given an AbortSignal which is aborted on any navigation. This can and should be used for any necessary cleanup/freeing up memory, such as aborting any pending requests and removing event listeners.

404 Pages

You can register a module for 404 pages using either set404() or by passing it via { notFound } in init(). This component or function will be given the current state and URL and can be dynamically generated.

Preloading

You can preload modules for views by using preloadModule() or by passing { preload: true } in init(). Preloading modules will make navigation effectively instant and potentially without network traffic, however it will increase initial load times (though it defaults to a low priority).

Important

Be advised that there may be a functional difference between using the router in the context of a <script type="module"> vs as a non-module, namely in the availability of import.meta.resolve() for preloading. Also, that importmaps are not quite univerally supported yet. For best compatibility, you SHOULD use either absolute or relative URLs when declaring modules for routes, though use of module specifiers (e.g. @scope/package) is supported in certain contexts, with decent browser support.

State Management

This currently uses @aegisjsproject/state for state mangement. It is a lightweight wrapper around history.state that uses BroadcastChannel to sync state changes between tabs/windows. It should be noted that this is global state and not specific to some component, so please avoid generic names and be aware of the global nature.