Crossroads.js: JavaScript Routes System

Crossroads.js is a routing library inspired by URL Route/Dispatch utilities present on frameworks like Rails, Pyramid, Django, CakePHP, CodeIgniter, etc... It parses a string input and decides which action should be executed by matching the string against multiple patterns.

It is a powerful and flexible routing system. If used properly it can reduce code complexity by decoupling objects and also by abstracting navigation paths and server requests.

Why?

JavaScript applications are becoming bigger and more complex each day and it is hard to keep control of what should be loaded/executed at each section.

Most of the back-end frameworks already provide some sort of routes or URL dispatch system but it usually requires a strict structure that limits the flexibility of the code, specially since it is directly related with the server request and calling actions on specific kinds of classes/objects (like views or controllers).

Crossroads.js was built with flexibility in mind and with the goal to be used as a standalone utility which can be plugged into any application and other libraries or frameworks to allow easy and robust URL dispatch.

One of the main differences of Crossroads.js is that it uses plain strings for the routing system which means it can be used to route URLs, server requests or simply as a messaging system across your application. It also means that Crossroads.js doesn't do things that it isn't supposed to do like updating the window.location, loading data or changing the application state unless you want to. — A routes system shouldn't do anything else besides routing.

It also has many advanced features to enable easy and robust mapping of strings to actions that aren't present on most JavaScript solutions.


Why isn't it a plugin for one of the famous JS Libraries/Frameworks?

Extending a library to do things it isn't supposed to do and that are not directly related with its core goal defeats the Single Responsibility Principle and is considered a bad practice besides its broad use.

DOM manipulation libraries should be used only for manipulating the DOM. Routing libraries shouldn't do anything besides routing...

A duck can walk, fly and swim, but he can't do any of these things well...

Another reason for not extending an existing library/framework is that the amount of code reused would be so minimal that it doesn't pay-off the flexibility loss. It means that you can use Crossroads.js with "any" other library/framework.


Dependencies

The only dependency is JS-Signals which is used for the event system to allow greater flexibility and also to provide advanced features that wouldn't be available with simple callbacks. Although JS-Signals dependency can be easily removed if needed.


Should I use it for every kind of project?

Nope. If your project have just a few static pages a simple switch statement may be enough to solve your problems, but as applications start to grow and you have a slightly more complex flow, having a system that supports dynamic variable parsing and a flexible validation schema may help you to reduce the overhead of doing those things manually and also reduce chance of errors.

I consider the main target to be single page applications with complex navigation paths and also large websites that shares the same JS files across multiple pages or would benefit from this kind of approach. It is also very useful for server-side and RESTful applications.


Reliable & Easy to Maintain

Crossroads.js source code is clean and well structured, meaning that it is easy to maintain. It is also fully unit-tested to ensure that code works as expected on different scenarios and that future updates won't break existing features by accident.


License

This project is released under the MIT License.


Bugs / Feature Requests

Use github issue tracker to submit bugs and feature requests.


Author

Miller Medeiros


Saving history state & updating browser URL

If you are using Crossroads in the browser you may also need a library to handle history state changes. Hasher is a "sibling" library designed to work together with Crossroads.js, both follow the same principles and use JS-Signals for the event dispatch. Example:

//setup crossroads
crossroads.addRoute('foo');
crossroads.addRoute('lorem/ipsum');
crossroads.routed.add(console.log, console); //log all routes

//setup hasher
function parseHash(newHash, oldHash){
  crossroads.parse(newHash);
}
hasher.initialized.add(parseHash); //parse initial hash
hasher.changed.add(parseHash); //parse hash changes
hasher.init(); //start listening for history change

//update URL fragment generating new history record
hasher.setHash('lorem/ipsum');

Documentation

Crossroads.js can be used as a regular JS file creating a global crossroads variable, an AMD module which doesn’t generate any global variables and also as a CommonJS module. You can use the same distribution file for all the environments (since v0.6.0).


API


crossroads.addRoute(pattern, [handler], [priority]):Route

Creates a new route pattern listener and add it to crossroads routes collection.

Parameters

pattern:String|RegExp
String pattern or Regular Expression that should be used to match against requests.
If pattern is a String it can contain named variables surrounded by "{}" that will be evaluated and passed to handlers as parameters. Each pattern segment is limited by the "/" char, so named variables will match anything until it finds a "/" char or the next string token located after the variable.
The pattern "{foo}/{bar}" will match "lorem/ipsum-dolor" but won't match "lorem/ipsum-dolor/sit". Trailing slashes at the end/begin of the request are ignored by default, so /{foo}/ matches same requests as {foo}. - If you need to match segments that may contain "/" use a regular expression instead of a string pattern.
A pattern can also have optional segments, which should be surrounded by "::" (e.g. "news/:foo:/:bar:" will match "news", "news/123" and "news/123/asd")
If pattern is a RegExp, capturing groups will be passed as parameters to handlers on the same order as they were matched.
It also allows "rest" segments (ending with *) which can match multiple segments. Rest segments can be optional and/or required and don't need to be the last segment of the pattern. The pattern "{foo}/:bar*:" will match news "news/123", "news/123/bar", "news/123/lorem/ipsum".
After version 0.9.0 crossroads added support to decoding query strings as well by starting the capturing groups with a "?" (eg: {?foo}, :?bar:). The matched value will be converted into an object and values will be typecasted if crossroads.shouldTypecast = true.
[handler]:Function (optional)
Function that should be executed when a request matches the Route pattern. (This is just a convenient way to attach a handler to the Route.matched Signal)
[priority]:Number (optional)
Route execution priority.
Routes with higher priority will be tested before during crossroads.parse. It is important to note that crossroads will stop pattern tests as soon as it finds a Route that matches the request. Setting the priority is a way to invert “natural” test order. Routes are tested by order of creation if priority is omitted.

Return

addRoute returns a Route object.

Example

//String rule with param:
//match '/news/123' passing "123" as param to handler
var route1 = crossroads.addRoute('/news/{id}', function(id){
  console.log(id);
});

//String rule with optional param:
//match '/foo/123/bar' passing "123" and "bar" as param
//match '/foo/45' passing 45 as param (slug is optional)
var route2 = crossroads.addRoute('/foo/{id}/:slug:');
//addRoute returns a Route object
route2.matched.add(console.log, console);

//RegExp rule:
//match '/lorem/ipsum' passing "ipsum" as param to handler
//note the capturing group around segment
var route3 = crossroads.addRoute(/^\/lorem\/([a-z]+)$/, function(id){
  console.log(id);
});

//String rule with rest segments:
//match '/foo/123/edit' passing "123" as argument
//match '/foo/45/asd/123/edit' passing "45/asd/123" as argument
var route4 = crossroads.addRoute('/foo/{id*}/edit');
//addRoute returns a Route object
route4.matched.add(console.log, console);

//Query String:
//match 'foo.php?lorem=ipsum&dolor=amet'
crossroads.addRoute('foo.php{?query}', function(query){
    // query strings are decoded into objects
    console.log('lorem '+ query.lorem +' dolor sit '+ query.dolor);
});

More Info

The String patterns are converted into RegExp during addRoute. The "named segments" and optional segments are also captured during this phase to avoid extra work on crossroads.parse() and improve performance.

Capturing groups are used to match named arguments and to validate them.


crossroads.removeRoute(route)

Remove a single route from crossroads collection.

Parameters

route:Route
Reference to the Route object returned by crossroads.addRoute().

PS: It is important to note that Route object will be destroyed during removal so trying to call any method or accessing any property of the Route object will throw errors.


crossroads.removeAllRoutes()

Remove all routes from crossroads collection.

PS: It is important to note that Route object will be destroyed during removal so trying to call any method or accessing any property of the Route object will throw errors.


crossroads.parse(request[, defaultArgs])

Parse a string input and dispatch matched Signal of the first Route that matches the request.

Routing priority is defined by order or insertion or by the priority parameter of the crossroads.addRoute method.

Parameters

request:String
String that should be evaluated and matched against Routes to define which Route handlers should be executed and which parameters should be passed to the handlers.
[defaultArgs]:Array (optional)
Array containing values passed to matched/routed/bypassed signals as first arguments. Useful for node.js in case you need to access the request and response objects.

Example

var sectionRoute = crossroads.addRoute('/{section}/{id}');
function onSectionMatch(section, id){
  console.log(section +' - '+ id);
}
sectionRoute.matched.add(onSectionMatch);
//will match `sectionRoute` passing "news" and `123` as param
crossroads.parse('/news/123');
var sectionRoute = crossroads.addRoute('/{section}/{id}');
function onSectionMatch(foo, bar, section, id){
  console.log(foo +' - '+ bar +' - '+ section +' - '+ id);
}
sectionRoute.matched.add(onSectionMatch);
//will match `sectionRoute` and pass "lorem" and "ipsum" as first arguments
crossroads.parse('/news/123', ["lorem", "ipsum"]);

More Info

The parse algorithm is very straightforward, since the string patterns were already converted into regular expressions during addRoute the router just need to loop through all the routes (following the priority order) and check which one matches the current input. If it does find a route that matches the "request" it will check if route contains the special property rules and then it will validate each segment (capturing group). If after all that route is considered "matched" than the Route.matched signal is dispatched together with the crossroads.routed signal.

After v0.10.0 calling parse() multiple times on a row passing the same request will dispatch the matched/routed/bypassed signals only once.


crossroads.getNumRoutes():Number

Get number of Routes contained on the crossroads collection.


crossroads.bypassed:Signal

Signal dispatched every time that crossroads.parse can't find a Route that matches the request. Useful for debuging and error handling.

Signal Handler Parameters

request:String
String passed to the crossroads.parse method.

Example

var route1 = crossroads.addRoute('/news/{id}');
crossroads.bypassed.add(function(request){
    console.log(request);
});
//won't match any route, triggering `bypassed` Signal
crossroads.parse('/foo');

crossroads.routed:Signal

Signal dispatched every time that crossroads.parse find a Route that matches the request. Useful for debuging and for executing tasks that should happen at each routing.

Signal Handler Parameters

request:String
String passed to the crossroads.parse method.
data:Object
Object containing additional info about routed request. (since v0.7)
route:Route
Route object that matches the string request.
params:Array
Array containing all the parameters captured by Route pattern.
isFirst:Boolean
If it is the first time crossroads.routed is being dispatched for the same crossroads.parse() call. - Will be false unless route is greedy and it is being matched after another route did already matched request.

Example

var route1 = crossroads.addRoute('/news/{id}');
crossroads.routed.add(function(request, data){
    console.log(request);
    console.log(data.route +' - '+ data.params +' - '+ data.isFirst);
});
crossroads.parse('/news/123'); //match `route1`, triggering `routed` Signal

crossroads.create():Router

Create a new independent Router instance. The instance shares same API as crossroads but won't share any properties, so adding a new Route to the created Router won't add the route to the crossroads object and vice-versa. (crossroads is just an instance of the Router object)

Basic Example

var otherRouter = crossroads.create();
otherRouter.addRoute('/news/{id}', function(id){
    console.log(id);
});
otherRouter.parse('/news/123');

Chain/Pipe Routers

Since Crossroads uses JS-Signals for the event system it is easy to chain/pipe multiple Routers together.

crossroads.routed.add(otherRouter.parse, otherRouter);
crossroads.bypassed.add(otherRouter.parse, otherRouter);
// same effect as calling: `crossroads.pipe(otherRouter)`

See: crossroads.pipe()


crossroads.normalizeFn:Function

Sets a default function that should be used to normalize parameters before passing them to the Route.matched, works similarly to Route.rules.normalize_.

normalizeFn should return an Array with parameters that should be dispatched by Route.matched Signal.

Important: It will be bypassed if Route have a rules.normalize_.

The 2 most common normalizations are are provided as static properties crossroads.NORM_AS_ARRAY and crossroads.NORM_AS_OBJECT.

Example

crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
crossroads.addRoute('/{foo}/{bar}', function(vals){
    //can access captured values as object properties
    console.log(vals.foo +' - '+ vals.bar);
});
crossroads.parse('/lorem/ipsum');
crossroads.normalizeFn = crossroads.NORM_AS_ARRAY;
crossroads.addRoute('/{foo}/{bar}', function(vals){
    //can access captured values as Array items
    console.log(vals[0] +' - '+ vals[1]);
});
crossroads.parse('/dolor/amet');
crossroads.normalizeFn = function(request, vals){
    //make sure first argument is always "news"
    return ['news', vals.id];
};
crossroads.addRoute('/{cat}/{id}', function(cat, id){
    console.log(cat +' - '+ id);
});
crossroads.parse('/article/123');

crossroads.shouldTypecast:Boolean

Set if crossroads should typecast route paths. Default value is false (IMPORTANT: on v0.5.0 it was true by default).

Note that you need to set this property for each Router returned by the crossroads.create() since all Routers have false as the default value.

Typecasted values

  • "true"
  • "false"
  • "null"
  • "undefined"
  • numeric values

Example

crossroads.shouldTypecast = true; //default = false
crossroads.addRoute('/news/{id}', function(id){
    console.log(id); // 12 (remove trailing zeroes since it's typecasted)
});
crossroads.parse('/news/00012');
crossroads.shouldTypecast = false; //default = false
crossroads.addRoute('/news/{id}', function(id){
    console.log(id); // "00012" (keep trailing zeroes)
});
crossroads.parse('/news/00012');

This affects also the values parsed by query strings.


crossroads.VERSION:String

String representation of the crossroads version number (e.g. "0.6.0").


crossroads.greedy:Boolean

Sets global route matching behavior to greedy so crossroads will try to match every single route with the supplied request (if true it won't stop at first match). See: Route.greedy for more info.

The default value is false.


crossroads.greedyEnabled:Boolean

Sets if the greedy routes feature is enabled. If false it won't try to match multiple routes (faster).

The default value is true.


crossroads.resetState()

Resets the Router internal state. Will clear reference to previously matched routes (so they won't dispatch switched signal when matching a new route) and reset last request.

This feature should NOT be needed by most users. Introduced on v0.10.0.

See: crossroads.ignoreState


crossroads.ignoreState:Boolean

Sets if Router should care about previous state, so multiple crossroads.parse() calls passing same argument would not trigger the routed, matched and bypassed signals.

The default value is false.

PS: The behavior prior to version 0.10.0 was the same as if this options was set to true. This feature was introduced on v0.11.0.

See: crossroads.resetState()


crossroads.pipe(Router)

Pipe routers, so all crossroads.parse() calls will be forwarded to the other router as well.

var sectionRouter = crossroads.create();
var navRouter = crossroads.create();

sectionRouter.pipe(navRouter);
// will also call `parse()` on `navRouter`
sectionRouter.parse('foo');

This feature was introduced on v0.11.0.

See: crossroads.unpipe()


crossroads.unpipe(Router)

"Ceci n'est pas une pipe"

var sectionRouter = crossroads.create();
var navRouter = crossroads.create();

sectionRouter.pipe(navRouter);
// will also call `parse()` on `navRouter`
sectionRouter.parse('foo');

sectionRoute.unpipe(navRouter);
// won't forward url since they aren't piped anymore
sectionRouter.parse('bar');

This feature was introduced on v0.11.0.

See: crossroads.pipe()


Route.matched:Signal

Signal dispatched every time a request matches the route pattern.

Signal handlers will receive all parameters captured by the Route pattern.

Example

var route1 = crossroads.addRoute('/news/{id}');
route1.matched.add(function(id){
  console.log('handler 1: '+ id);
});
route1.matched.add(function(id){
  console.log('handler 2: '+ id);
});
crossroads.parse('/news/123'); //will trigger both handlers of `route1`

See also: Route.switched, crossroads.shouldTypecast


Route.switched:Signal

Signal dispatched every time a request "leaves" the route.

Signal handlers will receive the "request" string passed to crossroads.parse()

See also: Route.matched


Route.rules:Object

Object used to configure parameters/segments validation rules.

Validation rules can be an Array, a RegExp or a Function:

  • If rule is an Array, crossroads will try to match a request segment against items of the Array, if item is found parameter is valid.
  • If rule is a RegExp, crossroads will try to match a request segment against it.
  • If rule is a Function, crossroads will base validation on value returned by Function (should return a Boolean).

Rules keys should match route pattern segment names or should be a numeric value, starting at index 0, that match each RegExp capturing group or path segment.

The rules object can also contain 2 special properties request_ and normalize_:

normalize_:Function(request, values)
Used to modify/normalize values before dispatching the matched Signal. It should return an Array with parameters that should be passed to listeners.
Can be used to create route aliases and also to convert data format.
Works exactly like crossroads.normalizeFn. It will overwrite crossroads.normalizeFn if present.
request_:*
Rule used to validate whole request.

It is important to note that values and requests are typecasted if Router.shouldTypecast = true (it's false by default).

All the properties of the rules object are optional, you can specify only the segments you want to validate.

Example

var route1 = crossroads.addRoute('/{section}/{date}/{id}');

//note that `rules` keys have the same as route pattern segments
route1.rules = {

    //match only values inside array
    section : ['blog', 'news', '123'],

    //validate dates on the format "yyyy-mm-dd"
    date : /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/,

    /*
     * @param {string|number|boolean} value  Request segment value.
     * @param {string} request  Value passed to crossroads.parse method.
     * @param {object} valuesObj  Values of all pattern segments.
     * @return {boolean} If segment value is valid.
     */
    id : function(value, request, valuesObj){
        if(isNaN(value)){
            return false;
        }else{
            if(+value < 100 && valuesObj.section == 'blog'){
                return true;
            }else if(valuesObj.section == 'news'){
                return true;
            }else{
                return false;
            }
        }
    },

    /**
     * `request_` is a special rule used to validate whole request
     * It can be an Array, RegExp or Function.
     * Note that request will be typecasted if value is a boolean
     * or number and crossroads.shouldTypecast = true (default = false).
     */
    request_ : function(request){
        return (request != '123');
    },

    /**
     * Normalize params that should be dispatched by Route.matched signal
     * @param {*} request Value passed to crossroads.parse method.
     * @param {object} vals All segments captured by route, it also have a
     *  special property `vals_` which contains all the captured values and
     *  also a property `request_`.
     * @return {array} Array containing parameters.
     */
    normalize_ : function(request, vals){
        //ignore "date" since it isn't important for the application
        return [vals.section, vals.id];
    }

};

route1.match("/foo/2011-05-04/2"); //false. {section} isn't valid
route1.match("/blog/20110504/2"); //false. {date} isn't valid
route1.match("/blog/2011-05-04/999"); //false. {id} is too high
route1.match("/blog/2011-05-04/2"); //true. all segments validate

Example RegExp pattern

var route1 = crossroads.addRoute(/([\-\w]+)\/([\-\w]+)\/([\-\w]+)/);

//note that `rules` keys represent capturing group index
route1.rules = {
  '0' : ['blog', 'news', '123'],
  '1' : /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/,
  '2' : function(value, request, valuesObj){
          return ! isNaN(value);
        }
};

route1.match("/foo/2011-05-04/2"); //false. {0} isn't valid
route1.match("/blog/20110504/2"); //false. {1} isn't valid
route1.match("/blog/2011-05-04/abc"); //false. {2} isn't numeric
route1.match("/blog/2011-05-04/2"); //true. all segments validate

Route.greedy:Boolean

If crossroads should try to match this Route even after matching another Route.

Default value is false.

Crossroads will trigger all "greedy" Routes that matches the request during parse.

Example

var projectsRoute = crossroads.addRoute('/projects/:id:');
projectsRoute.add(console.log, console);
projectsRoute.greedy = true; // greedy!

var projectDetailRoute = crossroads.addRoute('/projects/{id}', null, 2);
projectDetailRoute.add(console.log, console);

//match `projectsRoute`
crossroads.parse('/projects');

//match `projectDetailRoute` (priority 2) than `projectsRoute`
crossroads.parse('/projects/123');

Route.dispose()

Remove route from crossroads and destroy it, releasing memory.


Route.match(request):Boolean

Test if Route matches against request. Return true if request validate against route rules and pattern.

Example

var route1 = crossroads.addRoute('/news/{id}');
route1.match('/foo/bar'); //false
route1.match('/news/123'); //true
route1.match('/news/foo-bar'); //true

Route.interpolate(replacements):String

Return a string that matches the route replacing the capturing groups with the values provided in the replacements object. If the generated string doesn't pass the validation rules it throws an error (help spotting errors sooner).

Example

var route1 = crossroads.addRoute('news/{id}');
route1.interpolate({id: 123});   // "news/123"
route1.interpolate({id: 'foo'}); // "news/foo"

Examples

If you want to see more usage examples check the examples wiki page and also the unit tests.

Download

You can download this project in either zip or tar formats. You can also clone the project with Git by running:

$ git clone git://github.com/millermedeiros/crossroads.js.git

Or install it on node.js using npm:

$ npm install crossroads
Fork me on GitHub