relaks-route-manager
v2.0.9
Published
Route manager for Relaks
Downloads
18
Readme
Relaks Route Manager
Relaks Route Manager is a simple, flexible route manager designed for React applications that use Relaks. It monitors the browser's current location using the History API and extract parameters from the URL. You can then vary the contents displayed by your app based on these parameters. In addition, it traps clicks on hyperlinks, automatically handling page requests internally.
The library has a promise-based asynchronous interface. It's specifically designed with WebPack code-splitting in mind. It's also designed to be used in isomorphic React apps.
Installation
npm --save-dev install relaks-route-manager
Usage
import RouteManager from 'relaks-route-manager';
const options = {
basePath: '',
trackLinks: true,
trackLocation: true,
routes: routingTable,
rewrites: rewriteRules,
preloadingDelay: 2000,
useHashFallback: true,
reloadFaultyScript: true,
};
const routeManager = new RouteManager(options);
routeManager.activate();
await routeManager.start();
import React, { useMemo, useEffect } from 'react';
import { useEventTime } from 'relaks';
import { RouteManagerProxy } from 'relaks-route-manager';
/* Root-level React component */
function FrontEnd(props) {
const { routeManager } = props;
const [ routeChanged, setRouteChanged ] = useEventTime();
const route = useMemo(() => {
return new RouteManagerProxy(routeManager);
}, [ routeManager, routeChanged ]);
useEffect(() => {
routeManager.addEventListener('change', setRouteChanged);
return () => {
routeManager.removeEventListener('change', setRouteChanged);
}
}, [ routeManager ]);
...
}
Components are expected to access functionalities of the route manager through a proxy object--Route
in the sample code above. See the documentation of Relaks for an explanation. A default implementation is provided for reference purpose.
Options
basePath
When specified, create a rewrite rule that strips the base path from the URL prior to matching and adds in the base path when a URL is requested through find()
.
preloadingDelay
Amount of time (in milliseconds) to wait before initiating page preloading. Relevant only for apps that employ code-splitting. When specified, the route manager will call load()
of every route after the delay.
Default value: NaN
(no preploading of pages)
reloadFaultyScript
Force the document to reload when WebPack fails to load a JavaScript module. The Navigation Timing API is used to check whether the document has already been refreshed, so that the document will not continually reload if the error cannot be resolved by reloading.
Default value: false
trackLinks
Intercept click events emitted by hyperlinks (A
elements). Links with the attribute target
or download
are ignored.
Default value: true
when the window object is present (i.e. in a web-browser); false
otherwise (e.g. in Node.js).
trackLocation
Track changes of the current location caused by the visitor pressing the browser's back or forward button.
Default value: true
when the window object is present (i.e. in a web-browser); false
otherwise (e.g. in Node.js).
useHashFallback
Place the URL of the app's current route in the hash portion of the browser location instead of changing the actual path. When useHashFallback
is false, the location might look like the following:
https://example.com/news/
When it's true, the location will look like this:
https://example.com/#/news/
Hash fallback is useful when you're unable to add rewrite rules to the web server in order to enable client-side path changes. It's the only way to use this library when your app is running as a local file (in Cordova or Electron, for example).
Default value: false
rewrites
An array containing rewrite functions that modify a URL prior to matching it against the routing table. See rewrite rules.
routes
A hash table (i.e. an object) containing your app's routes. See routing table.
Routing table
Here's a section of the routing table used in one of the examples:
const routes = {
'welcome': {
path: '/',
load: async (match) => {
match.params.module = await import('pages/welcome-page' /* webpackChunkName: "welcome-page" */);
}
},
'film-list': {
path: '/films/',
load: async (match) => {
match.params.module = await import('pages/film-list' /* webpackChunkName: "film-list" */);
}
},
'film-summary': {
path: '/films/${id}/',
params: { id: Number },
load: async (match) => {
match.params.module = await import('pages/film-page' /* webpackChunkName: "film-page" */);
}
},
/* ... */
};
The key of each entry is the route's name. A route definition object may have these properties:
path
- a pattern used to match against the path of the URLquery
- an object containing patterns used to match against query variableshash
- a patterned used to match against the hash of the URLparams
- an object specifying the parameters' typesload
- a function that is called every time the route is used
The ES6-like ${name}
syntax is used to specify where capturing occurs. If an entry with the name is found in params
, it'll used to cast the parameter to the correct type. Otherwise it'll left as a string. A parameter's type impacts matching. If it's Number
or Boolean
, the matching string can only contain digits. If it's String
, the string can have any character, except when matching is done on a path, in which case it may not contain /
.
The following shows a route where parameters are extracted from all parts of a URL:
{
'dog-page': {
path: '/owners/${ownerName}/dogs/${dogName}',
query: {
f: '${friendly}'
},
hash: 'P${paragraphNumber}',
params: {
ownerName: String,
dogName: String,
friendly: Boolean,
paragraphNumber: Number,
},
load: async (match) => {
match.params.module = await import('pages/dog-page' /* webpackChunkName: "dog-page" */);
},
authentication: true,
}
}
A route is chosen when its path
matches the URL. Parameters from query
and hash
are treated as optional. When path
is "*"
, it'll match any path. Such a route could be used for a 404 page. It should be placed at the bottom of the routing table.
The route definition may contain custom fields. In the example above, we're specifying that our dog page requires authentication.
Loading a route
Once the route manager finds the correct entry for a route, it'll invoke its load()
function. The function will receive an object containing params
and context
, as well as properties of the URL, such as path
and query
. If on-demand loading is employed, the function should initiate the code import and return a promise. The example above show how that's done using ES7 async/await syntax. It would look as follows if we write it in old-fashioned JavaScript:
load: function(match) {
return import('pages/dog-page' /* webpackChunkName: "dog-page" */).then(function(module) {
match.params.module = module;
});
},
The /* webpackChunkName: ... */
comment gives the module module a name. Without it the JavaScript file would have an unintuitive numeric name. Consult the WebPack documentation for more details about code-splitting.
The parameter module
is not special. It's simply a name used by the example app. The route manager does not doing anything beyond calling the function. It's up to your code to make correct use of the parameters. Imagine your app have different navigation bar depending on which page the visitor is in. Your load()
functions might look something like the following:
load: async (match) => {
match.params.page = await import('pages/television' /* webpackChunkName: "television-page" */);
match.params.nav = await import('nav/electronics' /* webpackChunkName: "electronics-nav" */);
},
Custom matching
In lieu of a string pattern, you can supply an object containing two functions: from()
and to()
. The route manager invokes from()
when it tries to match a URL to a route. It invokes to()
when it forms a URL. For example, the code below can be used to capture the rest of the path, something that isn't possible using the default mechanism:
class WikiPath {
static from(path, params) {
const regExp = /^\/wiki\/(.*)/;
const match = regExp.exec(path);
if (match) {
params.pagePath = match[1];
return true;
}
}
static to(params) {
return `/wiki/${params.pagePath}`;
}
}
The route manager will not perform typecasting on parameters extracted in this manner.
Custom typecasting
You can perform more sophisticated typecasting by placing an object with from()
and to()
methods in params
. The following code converts a string to an array of Number
and back:
class NumberArray {
static from(s) {
if (s) {
return s.split(',').map((s) => {
return parseInt(s);
});
} else {
return [];
}
}
static to(a) {
return a.join(',');
}
}
Rewrite rules
Rewrite rules let you extract parameters from the URL and save them the route manager's rewrite context. They're useful in situations when you have parameters that are applicable to all routes. For example, suppose we are building a CMS that uses Git as a backend. By default, the app would fetch data from the HEAD of the master branch. The end user can also view data from a different branch or a commit earlier in time. Instead of adding these parameters to every route, we can use a rewrite rule to extract them from the URL if it begins with /@<branch>:<commit>
. Our app can then obtain the branch and commit ID from the route manager's context
property.
A rewrite rule is an object containing two functions: from()
and to()
. The route manager invokes from()
before it tries to match a URL to a route. It invokes to()
when it forms a URL. The rule for our hypothetical app might something look like this:
class ExtractCommitID {
static from(urlParts, context) {
const regExp = /^\/@([^\/]+)/;
const match = regExp.exec(urlParts.path);
if (match) {
// e.g. https://example.net/@master:/news/
const parts = match[1].split(':');
context.branch = parts[0];
context.commit = parts[1] || 'HEAD';
urlParts.path = urlParts.path.substr(match[0].length) || '/';
} else {
// e.g. https://example.net/news/
context.branch = 'master';
context.commit = 'HEAD';
}
}
static to(urlParts, context) {
if (context.branch !== 'master' || context.commit !== 'HEAD') {
const parts = [ context.branch ];
if (context.commit !== 'HEAD') {
parts.push(context.commit);
}
urlParts.path = `/@` + parts.join(':') + urlParts.path;
}
}
}
urlParts
is an object containing the different parts of a URL: path
, query
, and hash
. Typically, from()
would remove the part of the path that it's looking for.
When a new context isn't provided to find()
, it'll use the existing one. That means once the user clicks a link with a branch specified, all links will specify it automatically.
Multiple rules can be supplied to the route manager. If a rewrite function wishes to prevent subsequent rules from coming into play, it should return false
.
Properties
Concerning the current route:
context
- the current rewrite contextname
- the name of the routeparams
- parameters extracted from the URLroute
- the route definition object
Concerning the current URL:
hash
- the hash (without leading '#')path
- the path partquery
- the query variablessearch
- the query string (with leading '?')url
- the URL itself
Methods
Event handling:
Activation
Navigation:
Look-up:
Others:
addEventListener()
function addEventListener(type: string, handler: function, beginning?:boolean): void
Add an event listener to the route manager. handler
will be called whenever events of type
occur. When beginning
is true, the listener will be place before any existing listeners. Otherwise it's added at the end of the list.
Inherited from relaks-event-emitter.
removeEventListener()
function removeEventListener(type: string, handler: function): void
Remove an event listener from the route manager. handler
and type
must match what was given to addEventListener()
.
Inherited from relaks-event-emitter.
waitForEvent()
async function waitForEvent(type: string): Event
Return a promise that is fulfilled when an event of the specified type occurs.
Inherited from relaks-event-emitter.
activate()
function activate(): void
Activate the route manager, attaching event listeners to the DOM.
deactivate()
function deactivate(): void
Deactivate the route manager, removing event listeners from the DOM.
back()
async function back(): void
Go back to the previous page. The function will reject attempts to go beyond the browsing history of the app.
change()
async function change(url: string, options?: object): boolean
async function change(link: HTMLAnchorElement, options?: object): boolean
Use a URL to change the route. By default, the previous route is pushed into browsing history. Supply the option { replace: true }
to override this behavior.
url
must be an internal, relative URL.
Generally, you would use push()
or replace()
instead when changing the route programmatically.
push()
async function push(name: string, params?: object, newContext?: object): boolean
Change the route, saving the previous route in browsing history. name
is the name of desired page (i.e. a key in the routing table), while params
are the route parameters.
If newContext
is supplied, it'll be merged with the existing rewrite context and becomes the new context. Otherwise the existing context is reused.
No checks are done on params
. It's possible to supply parameters that could not appear in a route's URL.
The returned promise is fulfilled with false
when evt.preventDefault()
is called during beforechange
.
replace()
async function replace(name: string, params?: object, newContext?: object): boolean
Change the route, displacing the previous route.
start()
async function start(url?: string): boolean
Start the route manager, using url
for the initial route. If url
is omitted and trackLocation is true
, the URL will be obtained from the browser's location
object.
The promise returned by this method is fulfilled when a change
event occurs. This can happen either because the intended route is reached or if evt.postponeDefault()
and evt.substitute()
are used during a beforechange
event.
find()
function find(name: string, params?: object, newContext?: object): string
Find the URL of a route. name
is the name of desired page, while params
are the route parameters.
If newContext
is supplied, it'll be merged with the existing context and used for rewrite the URL. Otherwise the existing context is used.
match()
function match(url: string): object
Match a URL with a route, returning a object containing the following fields:
context
- rewrite contexthash
- hash part of the URL (without leading '#')name
- name of the routeparams
- parameters extract fromurl
path
- path part of the URLquery
- an object containing query variablesroute
- route definitionsearch
- query string (with leading '?')url
- the parameter passed to this function
url
should be an internal, relative URL.
An exception is thrown if no match is found.
preload()
async function preload(): void
Run the load()
methods of every route. The object passed to load()
will contain params
and context
so you can safely update their properties. Other field such as name
and url
will be undefined
.
Events
beforechange
The beforechange
event is emitted when a route change is about to occur. It gives your app a chance to prevent or postpone the change. For example, you might wish to ask the user to confirm the decision to leave a page when doing so means the loss of unsaved changes. Another usage scenario is to ask the user to log in before viewing a non-public page.
Default action:
Permit the change to occur.
Properties:
context
- rewrite contexthash
- hash part of the URL (without leading '#')name
- name of the new routeparams
- parameters extract from the URLpath
- path part of the URLquery
- an object containing query variablesroute
- route definitionsearch
- query string (with leading '?')url
- the URLdefaultPrevented
- whetherpreventDefault()
was calledpropagationStopped
- whetherstopImmediatePropagation()
was calledtarget
- the route managertype
- "beforechange"
Methods:
substitute(name: string, params?: object, newContext?: object)
- change the route while the requested route change is on holdpostponeDefault(promise: Promise)
- postpone the route change utilpromise
is fulfilledpreventDefault()
- stop the route change from happeningstopImmediatePropagation()
- stop other listeners from receiving the event
If an event listener decides that the visitor cannot immediately proceed to the route, it can call evt.postponeDefault()
to defer the change. The method expects a promise. If the promise is fulfilled with anything other than false
, the route change will occur then. While the promise is pending, evt.substitute()
can be used to switch to a page where the visitor can perform the necessary action. The following describes a login process involving a login page:
- The user clicks on a link to an access-controlled page, triggering a
beforechange
event. - The
beforechange
handler notices the route requires authentication. It callsevt.postponeDefault()
to defer the change, thenevt.substitute()
to redirect to the login page. evt.substitute()
triggers achange
event. The app rerenders to show the login page.- The visitor logs in.
- The promise given to
evt.postponeDefault()
is fulfilled. The route changes to the intended, access-controlled page.
A call to start()
will also trigger the beforechange
event. Suppose a visitor has enter the URL of an access-controlled page into the browser location bar. The following sequence would occur:
start()
triggers abeforechange
event.- The
beforechange
handler notices the route requires authentication. It callsevt.postponeDefault()
to defer the change, thenevt.substitute()
to redirect to the login page. evt.substitute()
triggers achange
event. The promise returned bystart()
is fulfilled.- The application's UI code starts (i.e. the root React component is rendered), with the login page as the current route.
- The visitor logs in.
- The promise given to
evt.postponeDefault()
is fulfilled. The route changes to the intended, access-controlled page.
change
The change
event is emitted after a route change has occurred, meaning the route manager has successfully load the necessary code and updated its internal state.
Properties:
propagationStopped
- whetherstopImmediatePropagation()
was calledtarget
- the route managertype
-"change"
Methods:
stopImmediatePropagation()
- stop other listeners from receiving the event
Examples
- Starwars API: Episode V - sequel to the first Starwars API example
License
This project is licensed under the MIT License - see the LICENSE file for details