A simple but powerful router module
[!CRITICAL] This package requires
URLPattern
to be polyfilled before any paths are registered. A common polyfill recommended by MDN can be found here.
npm install @aegisjsproject/router
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>
- 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.
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 URLPattern
s to their respective modules.
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}.`);
}
});
});
At the core, this router module just uses URLPattern
s 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)
.
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
.
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.
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.
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.
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.