Routing library based on transplexer
This library is intended to provide routing support to applications using transplexer.
The core assumption is that you would like to react to route changes (strictly speaking, to the 'popstate' event). This library allows you to leverage the transplexer pipes for the purpose.
The library has one limitation that you should be aware of. It only allows one routing table (one set of registered routes). This is intentional, and it will most likely never be changed as it keeps the library relatively simple.
Install from the NPM repository with NPM:
npm install --save-dev transplexer-routing
or with Yarn:
yarn add --dev transplexer-routing
The general usage pattern for this library is as follows:
- Register routes.
- Create a pipe.
- Kick-start the route handling by triggering
popstate
manually. - Change URLs in response to user action.
To set up new routes, we import the register()
function and pass it the route
name, the URL pattern, and optionally some payload. We'll circle back to the
payload a bit later when we talk about handling routing events.
import {register} from 'transplexer-routing';
register('main', '/');
register('about', '/about');
register('book', '/book/:id');
All routes have a name. This allows us to build routes using the name and route parameters, for example. Names must be unique, and it is an error to use the same name twice.
URL patterns are paths with optional path argument placeholders. Placeholders
look like :abc
(colon followed by one or more alphanumeric characters or
underscore). They correspond to locations where URL arguments (dynamic parts of
URLs) would be present. For example, if we say /book/:id
, we imply that the
portion after the /book/
will be variable, and there could be /book/1
, and
/book/2
and so on, and the numbers 1 and 2 can be captured as id
when
handling the routing events.
Placeholders can appear anywhere in the path, and do not necessarily have to
take up an entire segment between consecutive slashes. For instance,
/books/:slug-:id
is a perfectly valid URL pattern. Any data appearing in the
URL where the placeholders are found will be captured and made available to the
code handling the routing event under the same name as the placeholder.
Routes can be registered in bulk using the registerRoutes()
. This function
takes an array where each member is an array consisting of two or more members
that match the arguments of the register()
function. For instance, the last
example can be written like this:
import {registerRoutes} from 'transplexer-routing';
registerRoutes([
['main', '/'],
['about', '/about'],
['book', '/book/:id'],
]);
Just like the register()
function, registerRoutes()
can be called multiple
times, and each call will extend the routing table. It will also raise an
exception when multiple routes with the same name are specified.
The route context deserves a section of its own as that is what you will use to identify the state that is encoded in the address bar. Think of it as an object version of the URL.
The route context object has the following keys:
name
- name of the route that matched.payload
- the route payload.args
- an object mapping route parameters to their values.query
- an object containing query parameters.hash
- an object containing hash parameters.
The args
, query
, and hash
are all objects, key-value pairs that map
parameter names to one or more values. If there are multiple values for a given
parameter name, they are collected into an array. Values coming from URLs are
always all strings.
The args
property contains parameters collected from the path placeholders in
the URL pattern, as discussed in the previous section. Args, unlike query
and
hash
do not allow multiple values per parameter name. If you have
placeholders of the same name appearing multiple times, only the last one will
capture its value.
The query
comes from the query string in the URL, and the hash
comes the
from fragment identifier, or hash. Both query
and hash
are treated
identically. This means that the text after the #
in the hash is treated the
same way as the characters after ?
in the query string: #id=12&show=author
and ?id=12&show=author
both result in the same object:
{id: '12', show: 'author'}
The payload
will be discussed later, but it's any value you pass as a payload
during registration.
To give you a concrete example, let's assume that a route is registered as follows:
import {register} from 'transplexer-routing';
register('book', '/book/:id');
For this route, an URL that looks like '/books/12?show=author&show=isbn#menu=1' is interpreted as the following object:
{
name: 'book',
payload: null,
args: {
id: '12',
},
query: {
show: ['author', 'isbn'],
},
hash: {
menu: '1',
},
}
A route context object for any object that has pathname
, search
and hash
properties (like window.location
), can be obtained using a match()
function. For example:
import {match} from 'transplexer-routing';
match(window.location);
For any matching route, this function returns a context object. If no route matches, it returns an empty object.
Depending on how you integrate this library into your application, manually matching routes may or may not be needed.
With this library, the primary mechanism for handling routes changes is the transplexer pipe. This pipes transmit route context objects every time there is a route change.
To create a pipe, we use the createPipe()
function:
import {createPipe} from 'transplexer-routing';
let pipe = createPipe();
pipe.connect(function (context) {
// do something with the context
});
The createPipe()
function accepts zero or more transformers which can be used
to customize the location object prior to route matching. For example, if we
want to have a customizable prefix in our routes, we might use a transformer
like this:
import {createPipe} from 'transplexer-routing';
function prefixTransformer(next) {
return function (loc) {
next({
...loc,
pathname: loc.pathname.replace(/^\/app/, ''),
});
};
}
let pipe = createPipe(prefixTransformer);
Pipe payloads are used to attach arbitrary information to a route. This information is static, and is specified during registraiton. Here's an example of doing just that using 'page objects', made-up objects that contain information about the pages we want to render.
import {register, createPipe} from 'transplexer-routing';
import * as pages from './pages';
let currentPage;
let root = document.querySelector('#app');
let pipe = createPipe();
register('home', '/', pages.home);
register('about', '/about', pages.about);
register('book', '/book/:id', pages.book);
pipe.connect(function (context) {
if (currentPage) {
currentPage.stop();
}
currentPage = context.payload || pages.notFound;
currentPage.start();
let html = currentPage.render();
root.innerHTML = '';
root.appendChild(html);
});
In this example, we have some objects that are called 'pages' which are used as
payloads for each route. Suppose that the pages have a start()
, stop()
and
render()
methods. Since the payload is part of the routing context, on each
routing event, we can obtain the page that matches the current route, and call
its methods to facilitate page switches.
Once all the routes are set up, you may want to kick-start the router and cause
it to emit the initial state of the application right away. This is done by
manually triggering the popstate
event. A handy shortcut for this is provided
in the form of the start()
function.
import {start} from 'transplexer-routing';
start();
You usually want to change the current URL when user does something (e.g., go
to another 'page' when user clicks a link). This could either be a click on a
link, or a button, or a redirect due to desired target not being available at
that time. To go to another URL at any moment, we can use the go()
function.
import {go} from 'transplexer-routing';
go('/about');
// goes to '/about'
When go()
is called, it updates the browser's history stack by adding the
specified URL to the end, which in turn sets the address bar to that URL. Then
it triggers the popstate
event and causes the routing event to propagate
through the pipe(s).
Although go()
takes a plain URL as a string, you don't normally want to pass
URLs directly. Instead, what you would do is calculate the URL for a particular
path dynamically. By doing that, you decouple the string that represents the
URL in the address bar from the internal name used to identify that string. If
you ever decide that the URL doesn't look good or there is a spelling error in
it, you can safely change it once where you registered it, and leave the rest
of the code base alone.
The last example of go()
usage can be rewritten like so:
import {go, register, url} from 'transplexer-routing';
register('about', '/about', pages.about);
go(url('about'));
// goes to '/about'
The first argument to url()
is the name. An optional second argument is a
parameters object which may have one or more of the following properties:
args
- an object that maps route parameter names to their values. This object must be present if the route has parameters, and an exception is thrown if missing.query
- an object representing query string parameters.hash
- an object representing hash parameters.
If a path has any placeholders, the parameters object must be specified. This
object should have all path arguments as the args
property. For example:
import {go, register, url} from 'transplexer-routing';
register('book', '/book/:id', pages.book);
go(url('book', {args: {id: 34}}));
// goes to '/book/34'
The query string and hash can also be added using the url()
, in particular
the parameters object and its query
and hash
properties. Here is an
example:
import {go, register, url} from 'transplexer-routing';
register('book', '/book/:id', pages.book);
go(url('book', {
args: {id: 34},
query: {
show: ['author', 'isbn']
},
hash: {
menu: 1,
sidebar: 2,
},
}));
// goes to '/book/34?show=author&show=isbn#menu=1&sidebar=2'