@aegisjsproject/router
v1.1.0
Published
A simple but powerful router module
Downloads
875
Readme
@aegisjsproject/router
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.
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 URLPattern
s 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 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)
.
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 callinterceptNav(shadow)
in either the constructor orconnectedCallback
.
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 ofimport.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.