A lightweight JavaScript library is built on top of route-recognizer and rsvp.js to provide an API for handling routes
npm install router_js
router.js is a lightweight JavaScript library
that builds onroute-recognizer
and rsvp
to provide an API for handling routes.
In keeping with the Unix philosophy, it is a modular library
that does one thing and does it well.
router.js is the routing microlib used by
Ember.js.
To install using npm, run the following command:
```
npm install --save router_js rsvp route-recognizer
Create a new router:
`javascript`
var router = new Router();
Add a simple new route description:
`javascript`
router.map(function(match) {
match("/posts/:id").to("showPost");
match("/posts").to("postIndex");
match("/posts/new").to("newPost");
});
Add your handlers. Note that you're responsible for implementing your
own handler lookup.
`javascript
var myHandlers = {}
myHandlers.showPost = {
model: function(params) {
return App.Post.find(params.id);
},
setup: function(post) {
// render a template with the post
}
};
myHandlers.postIndex = {
model: function(params) {
return App.Post.findAll();
},
setup: function(posts) {
// render a template with the posts
}
};
myHandlers.newPost = {
setup: function(post) {
// render a template with the post
}
};
router.getRoute = function(name) {
return myHandlers[name];
};
`
Use another modular library to listen for URL changes, and
tell the router to handle a URL:
`javascript`
urlWatcher.onUpdate(function(url) {
router.handleURL(url);
});
The router will parse the URL for parameters and then pass
the parameters into the handler's model method. Itmodel
will then pass the return value of into thesetup method. These two steps are broken apart to support
async loading via promises (see below).
To transition into the state represented by a handler without
changing the URL, use router.transitionTo:
`javascript`
router.transitionTo('showPost', post);
If you pass an extra parameter to transitionTo, as above,serialize
the router will pass it to the handler's showPost
method to extract the parameters. Let's flesh out the handler:
`javascript
myHandlers.showPost = {
// when coming in from a URL, convert parameters into
// an object
model: function(params) {
return App.Post.find(params.id);
},
// when coming in from transitionTo, convert an
// object into parameters
serialize: function(post) {
return { id: post.id };
},
setup: function(post) {
// render a template with the post
}
};
`
As a modular library, router.js does not express anhash
opinion about how to reflect the URL on the page. Many
other libraries do a good job of abstracting andpushState and working around known bugs in browsers.
The router.updateURL hook will be called to give you
an opportunity to update the browser's physical URL
as you desire:
`javascript`
router.updateURL = function(url) {
window.location.hash = url;
};
Some example libraries include:
No matter whether you go to a handler via a URL change
or via transitionTo, you will get the same behavior.
If you enter a state represented by a handler through a
URL:
* the handler will convert the URL's parameters into an
object, and pass it in to setup
* the URL is already up to date
If you enter a state via transitionTo:
* the handler will convert the object into params, and
update the URL.
* the object is already available to pass into setup
This means that you can be sure that your application's
top-level objects will always be in sync with the URL,
no matter whether you are extracting the object from the
URL or if you already have the object.
When extracting an object from the parameters, you may
need to make a request to the server before the object
is ready.
You can easily achieve this by returning a promise
from your model method. Because jQuery's Ajax
methods already return promises, this is easy!
`javascript
myHandlers.showPost = {
model: function(params) {
return $.getJSON("/posts/" + params.id).then(function(json) {
return new App.Post(json.post);
});
},
serialize: function(post) {
return { id: post.get('id') };
},
setup: function(post) {
// receives the App.Post instance
}
};
`
Because transitions so often involve the resolution of
asynchronous data, all transitions in router.js,transitionTo
are performed asynchronously, leveraging the
RSVP promise library.
For instance, the value returned from a call
to is a Transition object with athen method, adhering to the Promise API. Any code.then
that you want to run after the transition has finished
must be placed in the success handler of , e.g.:
`javascript`
router.transitionTo('showPost', post).then(function() {
// Fire a 'displayWelcomeBanner' event on the
// newly entered route.
router.send('displayWelcomeBanner');
});
You can nest routes, and each level of nesting can have
its own handler.
If you move from one child of a parent route to another,
the parent will not be set up again unless it deserializes
to a different object.
Consider a master-detail view.
`javascript
router.map(function(match) {
match("/posts").to("posts", function(match) {
match("/").to("postIndex");
match("/:id").to("showPost");
});
});
myHandlers.posts = {
model: function() {
return $.getJSON("/posts").then(function(json) {
return App.Post.loadPosts(json.posts);
});
},
// no serialize needed because there are no
// dynamic segments
setup: function(posts) {
var postsView = new App.PostsView(posts);
$("#master").append(postsView.el);
}
};
myHandlers.postIndex = {
setup: function() {
$("#detail").hide();
}
};
myHandlers.showPost = {
model: function(params) {
return $.getJSON("/posts/" + params.id, function(json) {
return new App.Post(json.post);
});
}
};
`
You can also use nesting to build nested UIs, setting up the
outer view when entering the handler for the outer route,
and setting up the inner view when entering the handler for
the inner route.
When the URL changes and a handler becomes active, router.js
invokes a number of callbacks:
#### Model Resolution / Entry Validation Callbacks
Before any routes are entered or exited, router.js firstrouter.js
attempts to resolve all of the model objects for destination
routes while also validating whether the destination routes
can be entered at this time. To do this, makesmodel
use of the , beforeModel, and afterModel hooks.
The value returned from the model callback is the modelsetup
object that will eventually be supplied to
(described below) once all other routes have finished
validating/resolving their models. It is passed a hash
of URL parameters specific to its route that can be used
to resolve the model.
`javascript`
myHandlers.showPost = {
model: function(params, transition) {
return App.Post.find(params.id);
}
model will be called for every newly entered route,transitionTo
except for when a model is explicitly provided as an
argument to .
There are two other hooks you can use that will always
fire when attempting to enter a route:
* beforeModel is called before model is called,transition
or before the passed-in model is attempted to be
resolved. It receives a as its solemodel
parameter (see below).
* afterModel is called after is called,transition
or after the passed-in model has resolved. It
receives both the resolved model and
as its two parameters.
If the values returned from model, beforeModel,afterModel
or are promises, the transition will
wait until the promise resolves (or rejects) before
proceeding with (or aborting) the transition.
#### serialize
serialize should be implemented on as many handlerstransitionTo
as necessary to consume the passed in contexts, if the
transition occurred through . A context
is consumed if the handler's route fragment has a
dynamic segment and the handler has a model method.
#### Entry, update, exit hooks.
The following hooks are called after all
model resolution / route validation hooks
have resolved:
* enter only when the handler becomes active, not when
it remains active after a change
* setup when the handler becomes active, or when the
handler's context changes
For handlers that are no longer active after a change,
router.js invokes the exit callback.
The order of callbacks are:
* exit in reverse order
* enter starting from the first new handler
* setup starting from the first handler whose context
has changed
For example, consider the following tree of handlers. Each handler is
followed by the URL segment it handles.
``
|~index ("/")
| |~posts ("/posts")
| | |-showPost ("/:id")
| | |-newPost ("/new")
| | |-editPost ("/edit")
| |~about ("/about/:id")
Consider the following transitions:
1. A URL transition to /posts/1.beforeModel
1. Triggers the , model, afterModelindex
callbacks on the , posts, and showPostenter
handlers
2. Triggers the callback on the samesetup
3. Triggers the callback on the samenewPost
2. A direct transition to beforeModel
1. Triggers the , model, afterModelnewPost
callbacks on the .exit
2. Triggers the callback on showPostenter
3. Triggers the callback on newPostsetup
4. Triggers the callback on newPostabout
3. A direct transition to with a specifiedbeforeModel
context object
1. Triggers , resolves the specifiedafterModel
context object if it's a promise, and triggers
.exit
1. Triggers the callback on newPostposts
and serialize
2. Triggers the callback on aboutenter
3. Triggers the callback on aboutsetup
4. Triggers the callback on about
You can also nest without extra handlers, for clarity.
For example, instead of writing:
`javascript`
router.map(function(match) {
match("/posts").to("postIndex");
match("/posts/new").to("newPost");
match("/posts/:id/edit").to("editPost");
match("/posts/:id").to("showPost");
});
You could write:
`javascript
router.map(function(match) {
match("/posts", function(match) {
match("/").to("postIndex");
match("/new").to("newPost");
match("/:id", function(match) {
match("/").to("showPost");
match("/edit").to("editPost");
});
});
});
`
Typically, this sort of nesting is more verbose but
makes it easier to change patterns higher up. In this
case, changing /posts to /pages would be easier
in the second example than the first.
Both recognize the same sets of URLs but only the nested
ones invoke the hooks in the ancestor routes too.
When handlers are active, you can trigger events on
the router. The router will search for a registered
event backwards from the last active handler.
You specify events using an events hash in the
handler definition:
`javascript`
handlers.postIndex = {
events: {
expand: function(handler) {
// the event gets a reference to the handler
// it is triggered on as the first argument
}
}
}
For example:
`javascript
router.map(function(match) {
match("/posts").to("posts", function(match) {
match("/").to("postIndex");
match("/:id").to("showPost");
match("/edit").to("editPost");
});
});
myHandlers.posts = {
events: {
collapseSidebar: function(handler) {
// do something to collapse the sidebar
}
}
};
myHandlers.postIndex = {};
myHandlers.showPost = {};
myHandlers.editPost = {
events: {
collapseSidebar: function(handler) {
// override the collapseSidebar handler from
// the posts handler
}
}
};
// trigger the event
router.trigger('collapseSidebar');
`
When at the postIndex or showPost route, the collapseSidebarposts
event will be triggered on the handler.
When at the editPost route, the collapseSidebar eventeditPost
will be triggered on the handler.
When you trigger an event on the router, router.js will
walk backwards from the last active handler looking for
an events hash containing that event name. Once it finds
the event, it calls the function with the handler as the
first argument.
This allows you to define general event handlers higher
up in the router's nesting that you override at more
specific routes.
If you would like an event to continue bubbling after it
has been handled, you can trigger this behavior by returning
true from the event handler.
There are a few built-in events pertaining to transitions that you
can use to customize transition behavior: willTransition anderror.
The willTransition event is fired at the beginning of anyTransition
attempted transition with a object as the sole
argument. This event can be used for aborting, redirecting,
or decorating the transition from the currently active routes.
`js`
var formRoute = {
events: {
willTransition: function(transition) {
if (!formEmpty() && !confirm("Discard Changes?")) {
transition.abort();
}
}
}
};
You can also redirect elsewhere by calling
this.transitionTo('elsewhere') from within willTransition.willTransition
Note that will not be fired for thetransitionTo
redirecting , since willTransition doesn'twillTransition
fire when there is already a transition underway. If you want
subsequent events to fire for the redirectingtransition.abort()
transition, you must first explicitly call.
When attempting to transition into a route, any of the hooks
may throw an error, or return a promise that rejects, at which
point an error event will be fired on the partially-entered
routes, allowing for per-route error handling logic, or shared
error handling logic defined on a parent route.
Here is an example of an error handler that will be invoked
for rejected promises / thrown errors from the various hooks
on the route, as well as any unhandled errors from child
routes:
`js
var adminRoute = {
beforeModel: function() {
throw "bad things!";
// ...or, equivalently:
return RSVP.reject("bad things!");
},
events: {
error: function(error, transition) {
// Assuming we got here due to the error in beforeModel,afterModel
// we can expect that error === "bad things!",
// but a promise model rejecting would also
// call this hook, as would any errors encountered
// in .
// The error hook is also provided the failedtransition
// , which can be stored and later.retry()
// d if desired.
router.transitionTo('login');
}
}
};
`
Often, you'll want to be able to generate URLs from their components. To do so, use the router.generate(*parts) method.
`js
myRouter = new Router()
myRouter.map(function(match){
match("/posts/:id/:mode").to("showPost", function(match){
match("/version/:versionId", "postVersion");
});
});
myHandlers.showPost = {
serialize: function(obj) {
return {
id: obj.id,
tag: obj.modeName
};
} //...
};
myHandlers.postVersion = {
serialize: function(obj) {
return {
versionId: obj.id
};
}
//...
};
//...
`
*parts can accept either a set of primitives, or a set of objects. If it is a set of strings, router.generate will attempt to build the route using each string in order.
`js`
myRouter.generate("showPost", 4, 'a'); // returns '/posts/4/a'
If it is a set of objects, it will attempt to build the route by serializing each object.
`js`
myRouter.generate("showPost", {id: 4, modeName: 'a'}); // returns '/posts/4/a'
One can also use generate with nested routes. With strings, one simply provides all the URL fragments for each route in order:
`js`
myRouter.generate("postVersion", 4, 'a', 'first'); // returns '/posts/4/a/version/first'
With objects, one provides one object for each route in the chain; each route will then deserialize the corresponding object.
`js`
myRouter.generate("postVersion", {id: 4, modeName: 'a'}, {id: 'first'}); // returns '/posts/4/a/version/first'
One can mix and match between strings and objects; however, this is not recommended, as it can be extremely confusing and error prone:
`js`
myRouter.generate("postVersion", 4, modeName: 'a', {id: 'first'}); // returns '/posts/4/a/version/first'
myRouter.generate("postVersion", {id: 4, modeName: 'a'}, 'first'); // returns '/posts/4/a/version/first'
router.js uses route-recognizer under the hood, which
uses an NFA
to match routes. This means that even somewhat elaborate
routes will work:
`javascript`
router.map(function(match) {
// this will match anything, followed by a slash,
// followed by a dynamic segment (one or more non-
// slash characters)
match("/*page/:location").to("showPage");
});
If there are multiple matches, route-recognizer will/posts/edit
prefer routes that are more specific, so will be preferred/posts/:id
over, say, .
An architectural overview of router.js and its related libraries can be
found in ARCHITECTURE.md. Please read this document
if you are interested in better understanding / contributing to
router.js.
1. Ensure that Node.js is installed.
2. Run npm install to ensure the required dependencies are installed.npm run build
3. Run to build router.js. The builds will be placed in the dist/ directory.
1. To start the development server, run npm start.http://localhost:4200/tests/
2. Visit
or from the command line:
1. run npm test`