Jouter is a minimalist client-side routing library. It's main advantage compared to some of the other libraries that do similar things is its extremely small footprint (around 1KB minified and gzipped).
Jouter is written as an ES6 module. The compiled version is available in the
dist
directory in the source tree. The compiled version uses the
UMD module format, so
it can be used with AMD and CommonJS loaders, or as jouter
browser global
when added to your project with a simple <script>
tag.
You can also install Jouter using npm
:
npm install --save jouter
Before you can do anything with Jouter, you need to create a router object.
var router = jouter.createRouter()
Once you have your router object, you can start adding route handlers.
router.add(function (x) {
console.log('Hello, ' + x)
}, '/hello/:name')
The routes can contain capturing and/or wildcard placeholders.
The capturing placeholders use the :NAME
format where NAME
can be any
arbitrary string that does not include a slash (including whitespace
characters). The name has no special meaning except to document the purpose
of the placeholder, and the captured strings will be passed to the handler as
positional arguments.
Wildcard placeholders are written as a single *
character, and will swallow
any number of characters including slashes.
Alternatively, a RegExp
object can be used directly as a route:
router.add(function (x) {
console.log(x)
}, /^\/user\/(?:add|remove)\/([0-9a-f]{7})$/)
NOTE: When using a RegExp
object as a route, flags are ignored. For
instance, /foo/gi
would be interpreted as /foo/
.
You can also use ellipsis pattern to match and capture sub paths. For instance:
router.add(function (sub) {
// Sub will be any portion of the path after /foo
}, '/foo/...')
Note that the pattern is /...
, not just ...
. The slash after the prefix
/foo
is included in the match. This pattern is especially useful for working
with sub routes which are discussed further below.
You can add multiple handlers for the same route.
Once you've added all the routes, you can start the router:
router.start()
This will add a popstate
event handler
and trigger the handlers for the current path.
You can cause the browser to go to another path by using the go()
method.
router.go('/some/path', 'Target path title')
NOTE: The second parameter, title, is ignored in at least some of the mainstream browsers. It is recommended that you specify the title anyway, as the specs allow for it, and it may be supported in future.
You can also use the replace()
method to swap the currently entry in location
history with a new one. This also triggers the associated route handler.
Arguments are the same as for the go()
method:
router.replace('/some/path', 'Target path title')
You can also rig elements to trigger routing on events using
router.handleEvent
function as the event handler. The target path is derived
from the href
attribute of the element so this technique is suitable for
anchor elements. Note that preventDefault()
is always called on the event
object, so this technique is not usable on browsers without preventDefault()
or buggy preventDefault()
.
var a = document.getElementById('my-anchor')
a.onclick = router.handleEvent
That's it, we've covered all of the public APIs.
The router object also doubles as a route handler. This allows several router objects to be composed together. Here is a simple example:
var root = createRouter()
var users = createRouter()
users.add(function () {
// create a new user
})
users.add(function (id) {
// do something with user id
}, '/edit/:id')
users.add(function (id) {
// delete user
}, '/delete/:id')
root.add(users, '/users/...')
root.start()
The /...
at the end of a route captures the remainder of the route after
/users
, and calls the router on the captured part. The route handlers mapped
to the users
router will match /users/add
, /users/edit/:id
, and so on.
The users
router is otherwise a router just like any other, and the only
difference is that we don't call start()
on it.
When creating a router object, we have the ability to pass a path handler object, which customizes the way paths are physically handled.
The default path handler looks like this:
var pathHandler = {
get: function () { return window.location.pathname },
set: function (path, title) {
window.history.pushState(undefined, title, path),
},
swap: function (path, title) {
window.history.replaceState(undefined, title, path),
},
listen: function (f) { window.onpopstate = f },
decorate: function (f) { return f },
onNoMatch: function (p) { return; }
}
The handler object encapsulates the implementation details specific to the
environment (in this case, a browser). By implementing a new handler object,
and passing it to createRouter()
, we can adapt the router to different
environments or use different routing implementation in the browser (e.g.,
use hashes instead of History API).
In addition to containing the environment-specific functionality, the handler objects can also be used to customize the behavior of the route functions.
-
handler.get()
: must return the current path as a string. -
handler.set(path, title)
: must take a path, and optionally a title, and cause the application to switch to the specified path. -
handler.swap(path, title)
: must take a path, and optionally a title, and cause the application to replace the current item in location history with the specified one. -
handler.listen(func)
: must take a function that is to be invoked without any arguments every time current path changes. -
handler.decorate(func)
: must take a function and return a function. This can be used to customize route handler functions centrally (e.g., perform dependency injection). -
handler.onNoMatch(path)
: This function is invoked if no handler matched the given path. We usually only specify this on the top-level route handler when using subroutes. The only argument it receives is the path that failed to match. No return value is expected.
When passing a handler object to createRouter()
, we may pass an object that
contains a subset of the properties listed above, and thus override only the
aspects of route handling that we are interested in.
In some situations, we may want to modify the behavior of all the route handler functions. Purpose of decorating route handlers may vary depending on your situation, but a common use case is dependency injection.
For example, let's say we are working on an application that uses some secret token that must be available to route handlers. For some reason (e.g., to make code easier to test), we don't want to, or cannot, import the token into individual modules where our route handlers are defined. In this situation, we can give the token to a decorator function, and let it inject the token into all our route handlers, such that they will receive it as their first argument, followed by any arguments that were part of the URL.
var myHandler = function (token, arg1, arg2) {
// ....
}
var decorator = (function (token) {
return function realDecorator(fn) {
return function wrapper() {
var args = [token].concat([].slice.call(arguments))
return fn.apply(undefined, args)
}
}
})(generateToken())
var router = jouter.createRouter({decorate: decorator})
router.add(myHandler, '/:x/:y')
router.start()
In the above example, the token is generated ad-hoc, and not importable into
other modules. The decorator()
function is the only piece of code that has
access to the token, and it makes it available to the route handlers by
returning a decorated proxy handler function that takes the route arguments
and calls the actual handler with the token as the first argument.
A working example is available in the dist
directory. To run the example,
you will need to install the dependencies first:
cd path/to/jouter
npm install
Next, start the static server:
npm start
The default browser will open automatically.
The Jouter sources are hosted on GitHub. If you like it, don't forget to stop by and star it!
Jouter is a young library. It's virtually guaranteed to miss a feature or two that you may need, or have a bug in your particular use case. If you need to report an issue, use the GitHub issue tracker.
Jouter is licensed under the MIT license. See the LICENSE
file for more
information.