CrowdStrike/router.js

Name: router.js

Owner: Crowdstrike

Description: null

Created: 2013-01-11 18:47:14.0

Updated: 2015-10-30 17:23:03.0

Pushed: 2013-01-17 01:04:08.0

Homepage: null

Size: 189

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

router.js

router.js is a lightweight JavaScript library (under 1k!) that builds on route-recognizer 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.

Usage

Create a new router:

router = new Router();

Add a simple new route description:

er.map(function(match) {
tch("/posts/:id").to("showPost");
tch("/posts").to("postIndex");
tch("/posts/new").to("newPost");

Add your handlers:

er.handlers.showPost = {
serialize: function(params) {
return App.Post.find(params.id);


tup: function(post) {
// render a template with the post



er.handlers.postIndex = {
serialize: function(params) {
return App.Post.findAll();


tup: function(posts) {
// render a template with the posts



er.handlers.newPost = {
tup: function(post) {
// render a template with the post


Use another modular library to listen for URL changes, and tell the router to handle a URL:

atcher.onUpdate(function(url) {
uter.handleURL(url);

The router will parse the URL for parameters and then pass the parameters into the handler's deserialize method. It will then pass the return value of deserialize into the setup 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:

er.transitionTo('showPost', post);

If you pass an extra parameter to transitionTo, as above, the router will pass it to the handler's serialize method to extract the parameters. Let's flesh out the showPost handler:

er.handlers.showPost = {
 when coming in from a URL, convert parameters into
 an object
serialize: function(params) {
return App.Post.find(params.id);


 when coming in from `transitionTo`, convert an
 object into parameters
rialize: function(object) {
return { id: post.id };


tup: function(post) {
// render a template with the post


Changing the URL

As a modular library, router.js does not express an opinion about how to reflect the URL on the page. Many other libraries do a good job of abstracting hash and pushState 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:

er.updateURL = function(url) {
ndow.location.hash = url;

Always In Sync

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:

If you enter a state via transitionTo:

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.

Asynchronous Loading

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 deserialize method. Because jQuery's Ajax methods already return promises, this is easy!

er.handlers.showPost = {
serialize: function(params) {
return $.getJSON("/posts/" + params.id).then(function(json) {
  return new App.Post(json.post);
});


rialize: function(post) {
return { id: post.get('id') };


tup: function(post) {
// receives the App.Post instance


You can register a loading handler for router.js to call while it waits for promises to resolve:

er.handlers.loading = {
 no deserialize or serialize because this is not
 a handler for a URL

tup: function() {
// show a loading UI


Nesting

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.

er.map(function(match) {
tch("/posts").to("posts", function(match) {
match("/").to("postIndex");
match("/:id").to("showPost");
;


er.handlers.posts = {
serialize: function() {
return $.getJSON("/posts").then(function(json) {
  return App.Post.loadPosts(json.posts);
});


 no serialize needed because there are no
 dynamic segments

tup: function(posts) {
var postsView = new App.PostsView(posts);
$("#master").append(postsView.el);



er.handlers.postIndex = {
tup: function() {
$("#detail").hide();



er.handlers.showPost = {
serialize: function(params) {
return $.getJSON("/posts/" + params.id, function(json) {
  return new App.Post(json.post);
});



er.handlers.loading = {
tup: function() {
$("#content").hide();
$("#loading").show();


it: function() {
$("#loading").hide();
$("#content").show();


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.

Routes at any nested level can deserialize parameters into a promise. The router will remain in the loading state until all promises are resolved. If a parent state deserializes the parameters into a promise, that promise will be resolved before a child route is handled.

Transition Callbacks

When the URL changes and a handler becomes active, router.js invokes a number of callbacks:

For handlers that are no longer active after a change, router.js invokes the exit callback.

The order of callbacks are:

For example, consider the following tree of handlers. Each handler is followed by the URL segment it handles.

dex ("/")
posts ("/posts")
|-showPost ("/:id")
|-newPost ("/new")
|-editPost ("/edit")
about ("/about/:id")

Consider the following transitions:

  1. A URL transition to /posts/1.
  2. Triggers the deserialize callback on the index, posts, and showPost handlers
  3. Triggers the enter callback on the same
  4. Triggers the setup callback on the same
  5. A direct transition to newPost
  6. Triggers the exit callback on showPost
  7. Triggers the enter callback on newPost
  8. Triggers the setup callback on newPost
  9. A direct transition to about with a specified context object
  10. Triggers the exit callback on newPost and posts
  11. Triggers the serialize callback on about
  12. Triggers the enter callback on about
  13. Triggers the setup callback on about
Nesting Without Handlers

You can also nest without extra handlers, for clarity.

For example, instead of writing:

er.map(function(match) {
tch("/posts").to("postIndex");
tch("/posts/new").to("newPost");
tch("/posts/:id/edit").to("editPost");
tch("/posts/:id").to("showPost");

You could write:

er.map(function(match) {
tch("/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 work identically, so do whichever you prefer.

Events

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:

lers.postIndex = {
ents: {
expand: function(handler) {
  // the event gets a reference to the handler
  // it is triggered on as the first argument
}


For example:

er.map(function(match) {
tch("/posts").to("posts", function(match) {
match("/").to("postIndex");
match("/:id").to("showPost");
match("/edit").to("editPost");
;


er.handlers.posts = {
ents: {
collapseSidebar: function(handler) {
  // do something to collapse the sidebar
}



er.handlers.postIndex = {};
er.handlers.showPost = {};

er.handlers.editPost = {
ents: {
collapseSidebar: function(handler) {
  // override the collapseSidebar handler from
  // the posts handler
}



rigger the event
er.trigger('collapseSidebar');

When at the postIndex or showPost route, the collapseSidebar event will be triggered on the posts handler.

When at the editPost route, the collapseSidebar event will be triggered on the editPost 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.

Route Recognizer

router.js uses route-recognizer under the hood, which uses an NFA to match routes. This means that even somewhat elaborate routes will work:

er.map(function(match) {
 this will match anything, followed by a slash,
 followed by a dynamic segment (one or more non-
 slash characters)
tch("/*page/:location").to("showPage");

If there are multiple matches, route-recognizer will prefer routes with fewer dynamic segments, so /posts/edit will match in preference to /posts/:id if both match.

More to Come

router.js is functional today. I plan to add more features before a first official release:

router.js will be the basis for the router in Ember.js.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.