@domx/router
v1.1.0
Published
A DOM based custom element router for client side routing
Downloads
3
Maintainers
Readme
Router ·
A full featured DOM based custom element router for client side routing.
Description
Installation
Basic Example
Route Patterns
Query Parameters
Element Creation
Subroutes
Element Caching
Route.navigate
Route Events
Not Found Routes
Router
DomxRoute Public API
TypeScript Interfaces
Redux Dev Tools
Description
The router is built with custom elements so it can be used by any code stack since it relies only on the DOM and the browser platform. This makes the implementation very light and fast. It can also be configured to use Redux Dev Tools.
The main exports are the Router
class which contains static methods for updating the URL, and a DomxRoute
custom element
used to define routes.
Examples use LitElement for its simplicity.
Installation
npm install @domx/router
Basic Example
This basic example shows two routes. Note that nothing special needs to be done to the links on the page. They are just standard HTML hyperlinks. If a route is configured whose pattern matches a link href, then it will be triggered.
The two main attributes of domx-route
elements are the pattern
and the element
attributes. When a link is clicked that matches
a route pattern the element will be created and appended to the DOM.
The append-to
attribute can be set to either "body", "parent", or to a DOM query.
- body - appends the element to the document body.
- parent (default) - appends the element to the shadow root of the custom element that the route is in.
- DOM query - a query selector used on the shadow root to find an element to append to.
NOTE: There is also a way to take control of DOM insertion; this can be useful for adding animations. See: Route Events.
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "@domx/router/domx-route";
import "./example-page-1";
import "./example-page-2";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<nav>
<ul><a href="/page1"></a>
<ul><a href="/page2"></a>
</nav>
<domx-route
pattern="/page1"
element="example-page-1"
append-to="#container"
></domx-route>
<domx-route
pattern="/page1"
element="example-page-2"
append-to="#container"
></domx-route>
<main id="container"></main>
`;
}
}
The
append-to
attribute in this example uses a DOM query for#container
so the element will be appended to themain
element.
Note: a
replace-state
attribute can be added to hyperlinks to usehistory.replaceState
overhistory.pushState
. This may be desirable for something like tab navigation.
Route Patterns
All patterns start with "/" and support exact matches, optional segments/parameters, route parameters, and route tails.
Exact matches
The route is triggered if the URL matches the pattern exactly.
<domx-route
pattern="/user"
element="app-user"
></domx-route>
<domx-route
pattern="/user/home"
element="app-user-home"
></domx-route>
Optional URL Segments
Optional parameters are created using parentheses.
<domx-route
pattern="/user(/home)"
element="app-user-home"
></domx-route>
This pattern matches both
/user
and/user/home
.
Route Parameters
Route parameters create variables whose values are added as attributes on the element created.
<domx-route
pattern="/users/:user-id"
element="app-user"
></domx-route>
Givent the url:
/users/1234
, this route will match and create theapp-user
element with auser-id
attribute set to"1234"
.
Optional Route Parameters
Route parameters can also use parentheses to denote they are optional.
<domx-route
pattern="/users/:user-id(/:tab)"
element="app-user"
></domx-route>
This route will match either
/users/1234
or/users/1234/profile
; Bothuser-id
andtab
(if exists) will be set as attributes on theapp-user
element.
Route Tails (enabling subroutes)
Tails are created using an asterisk. Tails are used to capture any remaining parts of the URL and can be used for subroutes.
<domx-route
pattern="/attachments/*file-path"
element="app-attachment"
></domx-route>
This pattern would match
/attachments/path/to/file.ext
and add afile-path
attribute to theapp-attachment
element with the value:"/path/to/file.ext"
.
It also sets a read-only
tail
property on the element which is used internally to set parent routes which enables subroutes. See: Subroutes.
Query Parameters
A queryParams
property is set on each element when created and they are kept
in sync with the current URLs search parameters as long as the route matches.
<domx-route
pattern="/search/users"
element="user-search"
></domx-route>
Given the url:
/search/users?userName=joe&status=active
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("user-search")
class UserSearch extends LitElement {
@property({type:Object, attribute:false})
queryParams;
render() {
// access userName and status from queryParams
const { userName, status } = this.queryParams;
return html`
<!-- User Search Page Content -->
`;
}
}
Element Creation
There are five types of items that are added to an element when created.
- routeParams - for each matching route parameter, an attribute is added on the element with the route parameter name. (parent route params are also added when the route is a subroute.)
- queryParams - a
queryParams
property is set on the element containing the query parameters in the current URL. This is updated as long as the route matches. - tail path - an attribute is added to the element when using an asterisk; the name of the attribute is denoted by the text after the asterisk and the value is the remaining portion of the URL.
- tail - a
tail
property is set on the element if the pattern uses the asterisk. This is used internally for subroutes. It is an object that containsprefix
,path
, androuteParams
properties. - parentRoute - a
parentRoute
property is set when a route is created as a subroute. Its value is thetail
of the parent route and can be used for subroutes.
Subroutes
Subroutes allow routes to prefix their pattern with the parent routes tail. This enables these routes to be agnostic of their parent.
There are two ways to create subroutes:
- Using the same DOM tree.
- With separate elements; which enables routes that can exist within multiple scopes.
Using the same DOM tree
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "@domx/router/domx-route";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<domx-route
pattern="/users/*routeTail"
element="example-users-page">
<domx-route
pattern="/:userId"
element="example-user-page"
></domx-route>
</domx-route>
`;
}
}
Here the subroute is created inside the parent and so the parents tail is kept in sync with the child's parentRoute property.
Using Separate Elements
Subrouting can also be accomplished in separate elements by setting the parentRoute
property of a sub route.
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "@domx/router/domx-route";
import "./user-page";
import "./users-profile";
import "./users-events-list";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<domx-route
pattern="/user/:userId/*routeTail"
element="user-page">
</domx-route>
`;
}
}
@customElement("user-page")
class UserPage extends LitElement {
@property({type:Object, attribute: false})
parentRoute;
render() {
return html`
<domx-route
.parentRoute="${this.parentRoute}"
pattern="/profile"
element="users-profile">
</domx-route>
<domx-route
.parentRoute="${this.parentRoute}"
pattern="/events"
element="users-events-list">
</domx-route>
`;
}
}
The
parentRoute
property of theuser-page
was added by the route in theExampleApp
. This will prepend the matching part of the parent route to the pattern of the subroute.
Element Caching
The cache-count attribute on the route element can be set to the number of elements to keep in memory per route. This is useful for various use cases including working down a list of items. The default cache-count is 1.
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "@domx/router/domx-route";
import "./user-page";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<domx-route
pattern="/users/:user-id"
element="user-page"
cache-count="10"
></domx-route>
`;
}
}
In this example, for each user navigated to, the route will cache 10 user-page elements keeping the most recently viewed elements on the top of the cache.
Route.navigate
It is generally recommended to use hyperlinks to trigger routes, however, there may be times where you need to do so programmatically.
If you know the full path to navigate to you can use Router.pushUrl(url)
or Router.replaceUrl(url)
. Otherwise, you can
call navigate
on the route itself.
import { LitElement, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import "@domx/router/domx-route";
import "./user-page"
@customElement("example-app")
class ExampleApp extends LitElement {
@query("#userPageRoute")
$userPageRoute;
render() {
return html`
<button @click="${this.userPageButtonClicked}">User Page</button>
<domx-route
id="userPageRoute"
pattern="/user/:userId"
element="user-page">
</domx-route>
`;
}
userPageButtonClicked(event) {
this.$userPageRoute.navigate({
routeParams: { userId: 1234 }
})
}
}
Note: all routeParams in the pattern are required.
Route.navigate(options)
- replaceState - set to true to use
history.replaceState
overhistory.pushState
. - routeParams - an object that defines all required route parameters.
- queryParams - used to pass any desired queryParameters on the URL.
Route Events
There are two events triggered on a DomxRoute element: one when the route becomes active and another when it becomes inactive.
Both events send the same event detail parameters which contains an element
and a sourceElement
. The element
is the element that is
created when the route becomes active and the sourceElement
is the EventTarget
that triggered the route (which can be undefined).
event.preventDefault()
can be called to keep the route from inserting or removing the element automatically. This may be useful
if you want to provide animation or some other form of custom/dynamic insertion.
The active event is also a great place to lazy load other custom elements required by that route.
Example
import { LitElement, html } from "lit";
import { customElement, query } from "lit/decorators.js";
import "@domx/router/domx-route";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<domx-route
id="userPageRoute"
pattern="/users"
element="user-list"
@route-active="${this.usersRouteActive}"
@route-inactive="${this.usersRouteInActive}"
></domx-route>
`;
}
usersRouteActive(event) {
// lazy load the user-list element and any of its dependencies
import("./user-list");
// the element created or activated when the route becomes active
const element = event.detail.element;
// the EventTarget that triggered the route (if exists)
const sourceElement = event.detail.sourceElement;
// stops route from appending the element to the DOM
// so custom behaviors such as animation can be applied
event.preventDefault();
}
usersRouteInActive(event) {
// the element and sourceElement are the same as when the route is activated.
const { element, sourceElement } = event.detail;
// keeps the route from removing the element from the DOM
event.preventDefault();
}
}
Not Found Routes
There is a domx-route-not-found
element that can be used to capture when a url does not match any of the routes defined at the same level.
There are only two attributes that can be set.
- element - the name of the element to create
- append-to - where to append the element to
Example
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
import "@domx/router/domx-route";
import "@domx/router/domx-route-not-found";
import "./example-page-1";
import "./example-page-2";
import "./example-page-1-2";
import "./example-not-found-page";
@customElement("example-app")
class ExampleApp extends LitElement {
render() {
return html`
<nav>
<ul><a href="/page1"></a>
<ul><a href="/page2"></a>
<ul><a href="/page2/sub-page1"></a>
</nav>
<!-- not found if url is not /page1 or /page2 -->
<domx-route-not-found
element="example-not-found-page"
append-to="body"
></domx-route-not-found>
<domx-route
pattern="/page1"
element="example-page-1"
append-to="#container"
></domx-route>
<domx-route
pattern="/page2(/*routeTail)"
element="example-page-2"
append-to="#container">
<!--
not found if the parent matches
but the subroutes do not have a match
-->
<domx-route-not-found
element="example-not-found-page"
append-to="body"
></domx-route-not-found>
<domx-route
pattern="/sub-page1"
element="example-page-1-2"
append-to="#container"
></domx-route>
</domx-route>
<main id="container"></main>
`;
}
}
Note: It does not matter what order the routes are defined in.
Router
The Router
has static methods that can be useful for navigation and for setting a root path for all routes.
- Router.pushUrl(url) - uses
history.pushState
to add the URL to the browser history and trigger route matching. - Router.replaceUrl(url) - uses
history.replaceState
to replace the URL in the browser history and trigger route matching. - Router.replaceUrlParams(params) - replaces the current query parameters in the URL and triggers route matching.
- Router.root - set the root path for all routes; this can only be set once and should start with a backslash, e.g.
Router.root = "/demo";
DomxRoute Public API
Attributes
- pattern - the route pattern to match.
- element - the element to create when the route matches
- append-to - where the element should be appended to when the route matches; can be "body", "parent" (the default), or a DOM query selector.
- cache-count - the number of elements to cache per route (default is 1)
Properties
- parentRoute - a parent route object used for subrouting.
- tail - read only route tail.
Methods
- navigate(options) - navigates to the route.
Events
- route-active - triggered when the route pattern matches.
- route-inactive - triggered when the route pattern no longer matches.
TypeScript Interfaces
Here are a few TypeScript interfaces that may be helpful when developing in TypeScript.
/** Contains the parsed route segments */
interface RouteParams extends StringIndex<string|null> {}
/** Parsed query parameters */
interface QueryParams extends StringIndex<string> {}
/** Used for parent and tail routes */
interface Route {
prefix: string,
path: string,
routeParams: RouteParams
}
/** Options when calling the route.navigate(options) method */
interface NavigateOptions {
replaceState?: boolean,
routeParams?: RouteParams,
queryParams?: QueryParams
}
Redux Dev Tools
The router does not use Redux
but it can be configured to send events/actions to the Redux Dev Tools for visualizing
the current state of the routes in the DOM as well as using time travel.
This is because the DomxRoute
element uses the DataElement
package from Domx which exposes middleware to do this.
If you are using Data Elements then it is recommended that you import and apply the middleware from that package, however, it is also included here for convenience.
import { applyDataElementRdtLogging } from "@domx/router/middleware";
applyDataElementRdtLogging(/*options*/);