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.
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.
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.
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.
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.
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.
This project is released under the MIT License.
Use github issue tracker to submit bugs and feature requests.
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');
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).
Creates a new route pattern listener and add it to crossroads routes collection.
pattern
:String|RegExp"{}"
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.
"{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.
"::"
(e.g. "news/:foo:/:bar:"
will match "news"
, "news/123"
and "news/123/asd"
)
*
) 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"
.
"?"
(eg: {?foo}
, :?bar:
). The matched value will be converted into an object and values will be typecasted if crossroads.shouldTypecast = true
.
[handler]
:Function (optional)[priority]
:Number (optional)
addRoute
returns a Route
object.
//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); });
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.
Remove a single route from crossroads collection.
route
:RouteRoute
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.
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.
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.
request
:String[defaultArgs]
:Array (optional)request
and response
objects.
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"]);
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.
Get number of Routes contained on the crossroads collection.
Signal dispatched every time that crossroads.parse can't find a Route that matches the request. Useful for debuging and error handling.
request
:Stringvar route1 = crossroads.addRoute('/news/{id}'); crossroads.bypassed.add(function(request){ console.log(request); }); //won't match any route, triggering `bypassed` Signal crossroads.parse('/foo');
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.
request
:Stringdata
:Objectroute
:Routeparams
:ArrayisFirst
:Booleancrossroads.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.
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
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)
var otherRouter = crossroads.create(); otherRouter.addRoute('/news/{id}', function(id){ console.log(id); }); otherRouter.parse('/news/123');
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()
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
.
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');
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.
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.
String representation of the crossroads
version number (e.g. "0.6.0"
).
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
.
Sets if the greedy routes feature is enabled. If false
it won't try to match multiple routes (faster).
The default value is true
.
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.
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.
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()
"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()
Signal dispatched every time a request matches the route pattern.
Signal handlers will receive all parameters captured by the Route pattern.
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
Signal dispatched every time a request "leaves" the route.
Signal handlers will receive the "request" string passed to crossroads.parse()
See also: Route.matched
Object used to configure parameters/segments validation rules.
Validation rules can be an Array, a RegExp or a Function:
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)crossroads.normalizeFn
if present.
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.
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
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
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.
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');
Remove route from crossroads and destroy it, releasing memory.
Test if Route matches against request. Return true
if request validate against route rules and pattern.
var route1 = crossroads.addRoute('/news/{id}'); route1.match('/foo/bar'); //false route1.match('/news/123'); //true route1.match('/news/foo-bar'); //true
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).
var route1 = crossroads.addRoute('news/{id}'); route1.interpolate({id: 123}); // "news/123" route1.interpolate({id: 'foo'}); // "news/foo"
If you want to see more usage examples check the examples wiki page and also the unit tests.