dojo-routing
A routing library for Dojo 2 applications.
WARNING This is alpha software. It is not yet production ready, so you should use at your own risk.
Dojo 2 applications consist of widgets, stores and actions. Stores provide data to widgets, widgets call actions, and actions mutate data in stores. An application factory will materialize the necessary widgets and connect them to stores, thus forming the application.
This routing library lets you construct route hierarchies that are matched against URLs. Each selected route can tell the application factory to materialize a different set of widgets and influence the state of those widgets.
History managers are included. The recommended manager uses pushState()
and replaceState()
to add or modify history entries. This requires server-side support to work well. The hash-based manager uses the fragment identifier, so can work for static HTML pages. A memory-backed manager is provided for debugging purposes.
Features
The examples below are provided in TypeScript syntax. The package does work under JavaScript, but for clarity, the examples will only include one syntax.
Creating a router
; ;
Appending routes
With the router
from the previous example:
; router.appendcreateRoute;router.appendcreateRoute;
These routes won't (yet) do anything.
You can append multiple routes at once:
router.append;
Routes can only be appended to a router once.
Dispatching paths
The router doesn't track navigation events by itself. Changed paths need to be dispatched by application code. Context must be provided, this is made available to the matched routes.
; ; router.dispatchcontext, '/about';
Route selection starts in a future turn. An async Task is returned (see dojo-core
) which is resolved with a result object. The object has a success
property which is false
if no route was selected, or dispatch was canceled. It's true
otherwise.
An optional redirect
property may be present, in case one of the matched routes requested a redirect. The value of the redirect
property is the new path. It may be an empty string. No routes are executed when a redirect is returned, instead you're expected to change the path and call dispatch()
again.
You can cancel the task in case a new navigation event occurs.
Creating routes
The following creates a simple route. The exec()
function is called when the route is executed.
; ;
Note that path
defaults to /
, so the above is equivalent to:
;
The context provided in the router.dispatch()
call is available as request.context
:
;
You may return a thenable in order to capture errors. Route dispatch does not wait for the thenable to resolve.
Route hierarchies
Routes can be appended to other routes:
; ; ; posts.appendcreate; router.appendposts;
In this example the posts
route is executed for both /posts
and /posts/new
paths. The create
route is only executed for the /posts/new
path.
Like Router#append()
you can append multiple routes at once by passing an array:
posts.append;
Routes can only be appended to another route, or a router, once.
Starting the path of a nested route with a leading slash will not make it absolute. The nested route's path will still be relative to that of the route it's appended to.
Index routes
The posts
route in the above example is executed for both /posts
and /posts/new
paths. You can handle /posts
paths specifically by specifying an index
method:
;
You may return a thenable in order to capture errors. Route dispatch does not wait for the thenable to resolve.
Named parameters
Extract pathname segments
You can extract pathname segments. These will be added to the params
object of the request
:
;; createRoute;
Parameter names must not be repeated in the route's path. They can't contain {
, &
or :
characters. Only entire segments can be matched.
You can customize the params
object:
;; ;
The params()
function receives an array with string values for the extracted parameters, in declaration order.
You can prevent the route from being selected by returning null
from the params()
function:
;
This also prevents any nested routes from being selected.
Extract query parameters
Each route's path may include a search component. Name parameters to extract them into the params
object:
;; createRoute;
Again, parameter names must not be repeated in the route's path, and can't contain {
, &
or :
characters.
Named query parameters do not have to be present in a path for the route to be selected. Only the specified parameters are available in the params
object. Each route in a hierarchy can extract parameters.
You cannot specify expected values or other non-named parameters:
createRoute; createRoute;
You can extract multiple parameters though:
createRoute;
By default the params
object will contain the first occurrence of each query parameter. However if you specify a params()
function you'll get access to all values:
;; ;
searchParams
is a UrlSearchParams
instance from dojo-core
.
Preventing routes from being selected
You already know you can return null
from a params()
function to stop that route (and any nested routes) from being selected.
You can use a guard()
function to decide whether a particular route (and any nested routes) should be selected. It receives the same request
object as exec()
functions:
createRoute;
guard()
functions must return a boolean. Use them if you can synchronously determine whether a route should be selected.
Fallback routes
Sometimes paths are dispatched that don't match any routes. You can specify a fallback()
function at the router level:
;
The request
object will have a context, but no extracted parameters.
You can also use fallback()
functions in a route hierarchy. The fallback()
of the deepest route that matched the path will be called:
; ; ; byId.appendedit;posts.appendbyId;
No route will match /posts/5/stats
, however there is a fallback for the byId
route. The router will call exec()
on the posts
route and fallback()
on the byId
route.
You may return a thenable in order to capture errors. Route dispatch does not wait for the thenable to resolve.
Preventing dispatches altogether
You may want to prevent new routes from executing until the user has completed a certain task. You can listen to the navstart
event emitted by the router to cancel or defer dispatches:
; router.on'navstart', ;
Use event.path
to inspect the dispatched path. This is a regular string, so without any extracted parameters.
Use event.cancel()
to cancel the dispatch outright. You need to invoke this method synchronously when the event listener is called.
Use event.defer()
to defer the dispatch. This returns an object with resume()
and cancel()
functions. Dispatching will halt until you resume it using resume()
, or cancel it using cancel()
. This may be done asynchronously.
A dispatch may be deferred multiple times. All deferrers need to call resume()
for the dispatch to continue.
Note that if you cancel the dispatch the URL displayed in the browser will still be for the new path!
Selecting routes even if trailing slashes don't match
If the dispatched path ends with a /
, a route hierarchy can only be selected if its deepest route's path also ends with a /
. Similarly, if the dispatched path does not end with a /
, the deepest route's path also must not end with a /
.
This behavior can be disabled on a per-route basis by setting the trailingSlashMustMatch
option to false
:
;consts byId = createRoute; posts.appendbyId; ;router.appendposts;
Now the posts
and byId
routes will be selected both for /posts/5
and /posts/5/
.
Note that it's irrelevant whether any intermediate routes' paths end with a /
.
Repeated slashes
You cannot create routes with repeated slashes:
createRoute; // Throws!
However repeated slashes are ignored when dispatching:
;router.appendcreateRoute; router.dispatchcontext, '//foo///bar'; // Selects the /foo/bar route
Link generation
The router can generate links for a given route:
;;router.appendblog; router.linkblog === '/blog';
This also works with parameters:
;blog.appendshow; router.linkshow, === '/blog/5';
And query parameters:
;blog.appendshow; router.linkshow, === '/blog/5?highlight=40';router.linkshow, === '/blog/5?highlight=40';router.linkshow, === '/blog/5?highlight=40&highlight=55';
Note that if routes share the same parameter name they'll receive the same value:
;;blog.appendcategory;category.appendpost; router.linkpost, === '/blog/categories/5/posts/5';
You can also generate links without having a reference to the router:
;;;blog.appendshow; show.link === '/blog/5';
History management
This library ships with three history managers. They share the same interface but have different ways of monitoring and changing the navigation state.
pushState()
and friends
Using The recommended manager uses pushState()
and replaceState()
to add or modify history entries. This requires server-side support to work well:
; ;
This assumes the global object is a browser window
object. It'll access window.location
and window.history
, as well as add an event listener for the popstate
event.
You can provide an explicit window
object:
;
This is mostly useful for testing purposes.
Use history.current
to get the current path. It's initialized to the browser's location when the history object was created. It always starts with a /
, regardless of the path string passed to the history.set()
and history.replace()
methods.
Call history.set()
with a path string to set a new path. This will use window.history.pushState()
to change the URL shown in the browser.
history.replace()
works like history.set()
, but uses window.history.replaceState()
instead.
The change
event is emitted when history is set or replaced, or when popstate
is emitted on the window
object. The value
property of the event object contains the new path:
history.on'change', ;
Applications should call Router#dispatch()
with this value as the path.
Specifying a base pathname
A base pathname can be provided when creating the history manager:
;
In this example, if the browser's location is /myapp/index
, the path available at history.current
and the change
event value will be /index
. When calling history.set()
and history.replace()
with say /settings
, the browser's location will be changed to /myapp/settings
.
You may specify the base with or without a trailing slash.
Fragment identifiers
The hash-based manager uses the fragment identifier to store navigation state. This makes it a better fit for applications that are served as a static HTML file:
; ;
The history
object has the same current
getter and set()
and replace()
methods. The createHashHistory()
factory too assumes the global object is a browser window
object, but an explicit object can be provided. It'll access window.history
and add an event listener for the hashchange
event.
Path strings are stored in the fragment identifier. history.current
returns the current path, without a #
prefix. The same goes for the value
property of the change
event object.
Memory-only
Finally there is a memory-backed manager. This isn't very useful in browsers but can be helpful when writing tests.:
; ;
The createMemoryHistory()
factory accepts a path
option. It defaults to the empty string.
Making the router aware of the history manager
In browser-based applications it is desirable for the router to be aware of the history manager. This is why you can provide the history manager when creating the router:
;;
Now instead of using history.set()
and history.replace()
you can use router.setPath()
and router.replacePath()
.
start()
Automatic routing and clever linking through You could manually wire a history manager's change
event to a Router#dispatch()
, but that's a bit cumbersome. Instead if you provided the history manager when creating the router, you can use the start()
method to make the router observe the history manager:
;router.start;
By default start()
dispatches for the current history value. You can disable this:
router.start;
As an added benefit, when you use start()
it ensures the previous dispatch is canceled when the history changes and it dispatches a new request.
start()
also ensures history is replaced with the new path when routes request a redirect.
The context for these dispatches defaults to an empty object. A new object is used for every dispatch. You can configure the context when creating the router:
;
Provide a function if you want a new context for every dispatch:
;
link()
can use the currently selected routes when generating a new link. For instance given this router:
;; ;;; router.appendblog;blog.appendshow;show.appendedit;
If the current URL is /blog/5
, then you can generate a link for the edit
route without having to provide any parameters:
router.linkedit === '/blog/5/edit';
Calling dispatch()
directly will prevent the router from tracking selected routes. They'll also be unavailable after a redirect has been requested, before new routes have been selected.
Capturing errors
Errors that occur during dispatch are emitted under the error
event. The event object contains the error as well as the context and path used for the dispatch.
How do I use this package?
TODO: Add appropriate usage and instruction guidelines
How do I contribute?
We appreciate your interest! Please see the Dojo 2 Meta Repository for the Contributing Guidelines and Style Guide.
Testing
Test cases MUST be written using Intern using the Object test interface and Assert assertion interface.
90% branch coverage MUST be provided for all code submitted to this repository, as reported by istanbul’s combined coverage results for all supported platforms.
To test locally in node run:
grunt test
To test against browsers with a local selenium server run:
grunt test:local
To test against BrowserStack or Sauce Labs run:
grunt test:browserstack
or
grunt test:saucelabs
Licensing information
© 2004–2016 Dojo Foundation & contributors. New BSD license.