@curi/router
GitHub Repo
NPM PackageAbout
The @curi/router package provides functions for creating a single-page application's router.
Installation
npm install @curi/routerUMD scripts script are also available through Unpkg. You can access the package's exports using window.Curi.
API
prepareRoutes
The prepareRoutes function takes an application's routes and route interactions and returns an object. The returned object will be passed to createRouter.
import { prepareRoutes } from '@curi/router';
let routes = prepareRoutes([
{ name: "Home", path: "" },
// ...
{ name: "Not Found", path: "(.*)" }
]);prepareRoutes creates a reusable routing object, which means that it can be reused on the server instead of recompiling it for every request.
Arguments
routes
An array of route objects.createRouter
The createRouter function is used to create a router.
import { createRouter } from "@curi/router";
let router = createRouter(browser, routes, options);Arguments
history
A Hickory history function. The history guide provides more information on how to choose which history type is right for an application.
import { browser } from "@hickory/browser";
let router = createRouter(browser, routes);routes
An array of prepared route objects describing all valid routes in the application.
let routes = prepareRoutes([
{ name: "Home", path: "" },
{ name: "About", path: "about" }
]);
let router = createRouter(browser, routes);options
An optional object with additional properties that can be passed to the router.
sideEffects
An array of side effect objects.
import { createRouter, scroll } from "@curi/router";
let router = createRouter(browser, routes, {
sideEffects: [scroll()]
});external
Values that should be accessible to a route's resolve function respond functions.
Using external allows you to access APIs, data, etc. without having to import it in the module where the routes are defined.
let client = new ApolloClient();
let router = createRouter(browser, routes, {
external: { client, greeting: "Hi!" }
});let routes = prepareRoutes([
{
name: "User",
path: "user/:id",
resolve(match, external) {
// use the external object to make a query
return external.client.query();
}
}
]);invisibleRedirects
When a response object has a redirect property, Curi will automatically navigate to the location specified by the property.
If the invisibleRedirects property is false (the default), Curi will emit the redirect response (any observers will be called with the response).
If invisibleRedirects is set to true, Curi will skip emitting the redirect; this effectively makes the redirect invisible to the application.
invisibleRedirects should always be false for server-side rendering, otherwise the application will render content for the incorrect location.
let routes = prepareRoutes([
{
name: "Old",
path: "old/:id",
respond({ params }) {
// setup a redirect to the "New" route
return {
redirect: {
name: "New",
params
}
};
}
},
{
name: "New",
path: "new/:id"
}
]);
let router = createRouter(browser, routes, {
invisibleRedirects: false
});
// navigating to "/old/2" will automatically redirect
// to "/new/2" without emitting a response for the Old routeRouter
The router has a number of properties for you to use when rendering your application.
url
The url method is used to generate a URL string.
router.url({
name: "User",
params: { id: 1 },
hash: "other"
});Arguments
url takes a single argument, an object with details about the location to navigate to and how to navigate.
name
The name of a route.
params
An object of any route params for the named route (and any of its ancestors that require params).
hash
The hash string of a location.
query
The query value of a location.
navigate
The navigate method is used to navigate programmatically. It takes an object with the details of where and how to navigate.
router.navigate({ url: "/photos/123/456" });
// navigates to "/photos/123/456"
// using default "anchor" methodArguments
navigate takes a single argument, an object with details about the location to navigate to and how to navigate.
url
The URL string of the location to navigate to.
state
Any serializable state to attach to the location.
method
How to navigate. "push" appends the new location after the current one. "replace" replaces the current location. "anchor" is the default method and acts like clicking a link. This behavior is a mix of "push" and "replace" where the current location is replaced if the new location has the exact same URL.
finished & cancelled
finished is a function to call once the navigation has finished.
cancelled is a function to call if the navigation is superseded by another navigation.
These properties should only be provided when navigating to asynchronous routes. Synchronous routes navigate immediately, so there is no waiting time for the navigation.
once
Register a response handler that will only be called one time.
When a matched route is async (it has a resolve function), a response will not be created until the function has resolved. once is useful for delaying an application's initial render.
router.once(({ response }) => {
// render the application based on the response
});Arguments
Response Handler
A function that will be called once the router has generated a response. If the router has already generated a response, the function will be called immediately with the existing response. Otherwise, the function will be called once a new response is generated.
The function will be passed an object with three properties: the response, a navigation object, and the router.
Options
initial
When true (the default), the response handler will be called immediately if there is an existing response. When false, the response handler will not be called until a new response is generated.
router.once(responseHandler, {
initial: false
});observe
Register a response handler that will be called every time a response is generated.
When a matched route is async (it has a resolve function), a response will not be created until the function has resolved.
router.observe(({ response }) => {
// render the application based on the response
});observe returns a function, which can be called to stop observing.
let stopObserving = router.observe(fn);
// the router will call the response handler for all responses
stopObserving();
// the router no longer calls the response handlerArguments
Response Handler
A function that will be called whenever a new response is generated. If a response already exists when the function is called, the response handler will be called immediately with the existing response.
Options
initial
When true (the default), the response handler will be called immediately if there is an existing response. When false, the response handler will not be called until a new response is generated.
router.observe(responseHandler, {
initial: false
});cancel
With navigating to an asynchronous route, there is a time between when a navigation begins and when the route's asynchronous actions have finished, during which a user may decide to cancel the navigation.
In a multi-page application, the browser updates the refresh button to a stop button. There is no equivalent functionality for single-page applications, so Curi provides a cancel function to roughly imitate the behavior.
cancel takes an observer function that will be called when navigation starts and when the navigation is finished. When the navigation starts, the observer function will be called with a function to cancel the navigation. When the navigation finishes, the function will be called with undefined.
Calling cancel returns a function to stop observing.
let stopCancelling = router.cancel(fn);
// fn will be called for async navigation
stopCancelling();
// fn will no longer be calledArguments
Cancel Handler
A function that will be called when an asynchronous navigation begins and ends.
When the navigation begins, the function will be passed a function that can be called to cancel the navigation.
When the navigation ends, the function will be passed undefined.
router.cancel(fn => {
if (fn === undefined) {
// the navigation has finished/been cancelled
} else {
// calling fn will cancel the navigation
}
});Calling the cancel handler after the navigation has ended does nothing.
current
The current method returns the current response and navigation objects.
let router = createRouter(browser, routes);
let tooSoon = router.current();
// tooSoon.response === undefined
// tooSoon.navigation === undefined
router.once(({ response, navigation }) => {
let perfect = router.current();
// perfect.response === response
// perfect.navigation === navigation
});route
The route method is used to get the public data about a route. This is useful in conjuction with route interactions.
history
The route's history object.
external
The external value that was passed through createRouter's options.
announce
The announce side effect is used to announce navigation to screen readers. The announcement is done using an ARIA live region.
The side effect will create an element with a aria-live attribute and add it to the DOM. This element will be styled to not be displayed on screen (but not actually hidden) so that only screen readers detect it.
The announce function takes a single argument, which is a function that receives the object emitted by the router and returns the string that should be set for the screen reader to read.
The DOM element's aria-live attribute will be "assertive" by default, but you can use the side-effect factory's second argument to pass an alternative (i.e. "polite").
import { createRouter, announce } from '@curi/router';
let announcer = announce(
({ response }) => `Navigated to ${response.meta.title}`
);
let politeAnnouncer = announce(
({ response }) => `Navigated to ${response.meta.title}`,
"polite"
);
let router = curi(history, routes, {
sideEffects: [announcer]
});scroll
The scroll side effect will scroll the page after a navigation.
When Curi is running in a browser, it relies on the History API to change locations. Navigating using the History API does not trigger scrolling to the top of the page after navigation, so this side effect scrolls for you.
Pop navigation, such as clicking the browser's back and forward buttons, will rely on the browser to correctly restore the scroll position.
import { createRouter, scroll } from "@curi/router";
let router = createRouter(browser, routes, {
sideEffects: [scroll()]
});title
The title side effect will set the document's title.
The function takes a single argument, which is a function that takes the object emitted by a router and returns the string to set as the title.
import { createRouter, title } from '@curi/router';
let router = createRouter(history, routes, {
sideEffects: [
title(({ response }) => {
return `${response.meta.title} | My Site`;
})
]
});The recommended approach for determining a title is to have routes set their meta.title property in their respond method.
{
name: "About",
path: "about",
respond() {
return {
body: About,
meta: {
title: "About"
}
}
}
}
// when the About route matches, document.title = "About | My Site"Route Objects
route.name
A string that will be used to identify a route. This must be unique for every route.
[
{ name: 'Home' },
{ name: 'Album' },
{ name: 'Not Found' }
];route.path
A string pattern describing what the route matches. path strings should not have a leading slash.
[
{ path: "" }, // yes
{ path: "/" }, // no
]A route's path is used to check if it matches a location's pathname. When the path matches, the route is selected.
Path matching is done using regular expressions compiled by path-to-regexp. A path can include parameters, which are dynamic variables that are parsed from a location's pathname. For advanced path formatting, please read path-to-regexp's documentaion.
[
{ name: 'Home', path: '' },
// when the pathname is a/yo, albumID = "yo"
{ name: 'Album', path: 'a/:albumID' },
// the path (.*) matches every pathname
{ name: 'Not Found', path: '(.*)' }
];
// don't include a leading forward slash
// { name: 'Home', path: '/' }route.resolve
The resolve property is a function that returns a Promise. It is used to run asynchronous actions for a route prior to rendering.
A route with a resolve function is asynchronous, while one with no resolve functions is synchronous. You can read more about this in the sync or async guide.
Every time that a route with a resolve function matches, the route's resolve function will be called. Simple caching can be done with the once function from @curi/helpers, while more advanced caching is left to the user.
The function will be passed an object with the matched route properties: name, params, partials, and location.
let about = {
name: 'About',
path: 'about',
resolve({ name, params, partials, location }) {
return Promise.resolve("hurray!");
}
};The value returned by the resolve function will be passed to the route's respond function through its resolved property. If there is an uncaught error, resolved will be null and the error will be passed.
let about = {
name: 'About',
path: 'about',
resolve({ name, params, partials, location }) {
return Promise.resolve("hurray!");
},
respond({ resolved, error }) {
if (error) {
// there was an uncaught error in the resolve function
}
}
};route.respond
A function for modifying the response object.
The object returned by a route's respond function will be merged with the route's intrinsic match properties to create the response object.
Only valid properties will be merged onto the response; everything else will be ignored. The valid properties are:
Arguments
options
A respond function is passed an object with a number of properties that can be useful for modifying the response.
{
respond: ({ match, resolved, error }) => {
// ...
}
}match
An object with the intrinsic route properties of a response.
name
The name of the matched route.
params
Route parameters parsed from the location.
partials
The names of any ancestor routes of the matched route.
location
The location that was used to match the route.
key
A two number tuple. The first number is the location's place in the session array. The second number starts and zero and is incremented by replace navigation ([1,0] would be replaced by [1,1]).
resolved
An object with the value returned by the route's resolve function.
If a route isn't async, resolved will be null.
// attach resolved data to the response
let user = {
name: 'User',
path: ':id',
resolve({ params, location }) {
return fetch(`/api/users/${params.id}`)
.then(resp => JSON.parse(resp));
},
respond: ({ resolved }) => {
return {
data: resolved
};
}
}error
If the route has a resolve function that throws an uncaught error, the error property will be that error. Otherwise, the property will be null.
Ideally, the resolve function will always catch its errors, but error serves as a safety check.
// check if any of a route's resolve functions threw
let user = {
name: 'User',
path: ':id',
resolve({ params, location }) {
return fetch(`/api/users/${params.id}`)
.then(resp => JSON.parse(resp));
},
respond: ({ error, resolved }) => {
if (error) {
return {
meta: {
error
}
};
}
return {
data: resolved.data
};
}
}Return Value
body
Typically, the body is a component (or components) that will be rendered.
import Home from "./components/Home";
let routes = prepareRoutes([
{
name: "Home",
path: "",
respond() {
return { body: Home };
}
},
// ...
]);
// response = { body: Home, ... }meta
An object whose properties are metadata about the response. This may include the status of the response (200, 301, etc.), a title string for the document, or a description to be set as a page's <meta name="Description">.
{
respond(){
return {
meta: {
title: "Some Page",
description: "This is some page",
status: 200
}
};
}
}data
Anything you want it to be.
{
respond() {
return { data: Math.random() };
}
}
// response = { data: 0.8651606708109429, ... }redirect
An object with the name of the route to redirect to, params (if required), and optional hash, query, and state properties.
The other values are copied directly, but redirect will be turned into a location object using the object's name (and params if required).
[
{
name: "Old Photo",
path: "photo/:id",
respond({ params }) {
return {
redirect: { name: "Photo", params }
};
}
},
{
name: "New Photo",
path: "p/:id"
}
]
// when the user navigates to /photo/1:
// response = { redirect: { pathname: "/p/1", ... } }The redirect property can also be used to specify a redirect to an external location. An external redirect object has only one property: exernalURL.
{
name: "Redirects",
path: "redirects",
respond() {
return {
redirect: {
externalURL: "https://example.com"
}
}
}
}Responses with an external redirect are always emitted, even when invisibleRedirects is true. The actual location changing is left to the application.
children
An optional array of route objects for creating nested routes.
Any child routes will be matched relative to their parent route's path. This means that if a parent route's path string is "one" and a child route's path string is "two", the child will match a location whose pathname is /one/two.
// '/a/Coloring+Book/All+Night' will be matched
// by the "Song" route, with the params
// { album: 'Coloring+Book', title: 'All+Night' }
{
name: 'Album',
path: 'a/:album',
children: [
{
name: 'Song',
path: ':title'
}
]
}params
When path-to-regexp matches paths, all parameters are extracted as strings. The params object is used to specify functions to transform the extracted value.
Properties of the route.params object are the names of params to be parsed. The paired value should be a function that takes a string (the value from the pathname) and returns a new value (transformed using the function you provide).
By default, each param is decoded using decodeURIComponent. A param function can be used to leave the param in its encoded form or to parse an integer param into a number.
let routes = prepareRoutes([
{
name: 'Number',
path: 'number/:num',
params: {
num: n => parseInt(n, 10)
}
}
]);
// when the user visits /number/1,
// response.params will be { num: 1 }
// instead of { num: "1" }Unnamed params are referred to by their index in the path.
let routes = prepareRoutes([
{
name: 'Not Found',
path: '(.*)',
params: {
// skip decoding the unmatched value
0: value => value
}
}
]);pathOptions
An object for configuring how the path-to-regexp handles the path.
{
name: "My Route",
path: "my/:item",
pathOptions: {
match: {
sensitive: false
},
compile: {
encode: (value, token) => value
}
}
}match
Properties for parsing the path into a regular expression.
You can see the options and their default values in the path-to-regexp documentation.
compile
For pathname generation, the options are passed through a compile object. There is only one possible option, which is an encode function for encoding params. The default encode function encodes params using encodeURIComponent.
extra
If you have any additional properties that you want attached to a route, use the extra property. You will be able to use route.extra in any custom route interactions.
let routes = prepareRoutes([
{
name: 'A Route',
path: 'a-route',
extra: {
transition: 'fade'
}
},
{
name: 'B Route',
path: 'b-route',
extra: {
enter: 'slide-right'
}
}
]);