Skip to content

Instantly share code, notes, and snippets.

@ianstormtaylor
Created September 19, 2012 04:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save ianstormtaylor/3747665 to your computer and use it in GitHub Desktop.
Save ianstormtaylor/3747665 to your computer and use it in GitHub Desktop.
Enhanced Backbone Router to make a lot of things easier.
define([
'underscore',
'backbone'
],
function (_, Backbone) {
return Backbone.Router.extend({
// Variants of the routes to be automatically bound to. This way you
// never need to worry about trailing slash or querystrings.
variants : [
'', // normal route
'/', // trailing slash
'?*querystring', // querystrings
'/?*querystring' // trailing slash + querystrings
],
// A place to store the current route, routeName and parameters.
current : {
routeName : '',
route : '',
parameters : {}
},
initialize : function () {
// Make a navigator helper that can be passed to others without
// having to pass the entire router to a subview.
this.navigator = {
build : _.bind(this.build, this),
buildHref : _.bind(this.buildHref, this),
load : _.bind(this.load, this),
update : _.bind(this.update, this)
};
},
// Actions
// -------
// Enhanced route function that automatically adds any passed in prefix
// and binds variant routes.
route : function (route, name, callback) {
// Bind a route for each of the variants.
for (var i = 0, l = this.variants.length; i < l; i++) {
var variant = this.variants[i];
Backbone.Router.prototype.route.call(this, route+variant, name, callback);
}
},
// Returns a URL fragment string with parameters filled in from
// the appropriate route.
build : function (routeName, parameters) {
var fragment, route = this.findRouteByName(routeName);
if (route === null) return null;
fragment = this.compileFragment(route, parameters);
return fragment;
},
// Like `build`, but adds the `root` as well, so it's a fully
// functional URL path.
// TODO: `root` is hardcoded lol.
buildHref : function (routeName, parameters) {
var fragment = this.build(routeName, parameters);
if (!fragment) return null;
return '/' + fragment;
},
// Loads a route by name, with optional parameters to fill in.
// Trigger's the callback by default.
load : function (routeName, parameters, options) {
options || (options = {});
// Unless otherwise specified, load should trigger.
_.defaults(options, {trigger:true});
// Make sure parameters are set and backed by defaults.
parameters || (parameters = {});
_.defaults(parameters, this.current.parameters[routeName]);
var fragment = this.build(routeName, parameters);
if (fragment === null) return;
this.navigate(fragment, options);
// Update current in the case of a silent route not doing it.
if (!options.trigger) {
_.extend(this.current.parameters[routeName], parameters);
this.current.routeName = routeName;
this.current.route = this.findRouteByName(routeName);
}
},
// Updates the URL by route name and optional parameters. Doesn't
// trigger and replaces the current history item by default.
update : function (routeName, parameters, options) {
parameters || (parameters = {});
options || (options = {});
// Default to not triggering and replacing.
_.defaults(options, {
trigger : false,
replace : true
});
// If routeName is an object, it was omitted, use current one.
if (_.isObject(routeName)) {
parameters = routeName;
routeName = this.current.routeName;
}
// Grab current parameters for any missing pieces.
_.defaults(parameters, this.current.parameters[routeName]);
this.load(routeName, parameters, options);
},
// Helpers
// -------
// Overridden! and straight up copied from Backbone, but then tweaked to
// add optional placeholders.
_routeToRegExp : function (route) {
var optionalParam = /\[(.*?)\]/g; // [/:param]
var namedParam = /:\w+/g; // :param
var splatParam = /\*\w+/g; // *param
var escapeRegExp = /[-\{\}()+?.,\\\^$|#\s]/g; // All but []'s
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(/[\[\]]/g, '\\$&')
.replace(namedParam, '([^\/]+)')
.replace(splatParam, '(.*?)');
return new RegExp('^' + route + '$');
},
// Overridden! to store current parameters before triggering a route
// handler. We do this here instead of on an `all` callback, because
// we want this to already have happened by the time a normal route
// handler is invoked.
_extractParameters : function (route, fragment) {
var routeName = _.find(this.routes, function (routeName, roote) {
return route.toString() === this._routeToRegExp(roote).toString();
}, this);
this.current.routeName = routeName;
for (var roote in this.routes) {
if (this.routes[roote] === this.current.routeName) {
this.current.route = roote;
break;
}
}
var placeholders = this.current.route.match(/[:|\*]\w+/g);
var parameters = route.exec(fragment).slice(1);
// For each placeholder, store a current parameter.
if (placeholders) {
for (var i = 0, placeholder; placeholder = placeholders[i]; i++) {
placeholders[i] = placeholder.replace(/[:|\*]/, '');
this.current.parameters[routeName] || (this.current.parameters[routeName] = {});
if (parameters[i]) this.current.parameters[routeName][placeholders[i]] = parameters[i];
}
}
return parameters;
},
// Returns a fragment string by fillin a route's placeholders with
// parameters.
compileFragment : function (route, parameters) {
var fragment = route;
_.each(parameters, function (parameter, key) {
fragment = fragment.replace(new RegExp(':' + key + '\\b'), parameter || '')
.replace(new RegExp('\\*' + key + '\\b'), parameter || '');
});
// Remove any optional placholders and extra slashes and brackets.
fragment = fragment.replace(/\[\/:\w+\b\]/g, '')
.replace(/[\[|\]]/g, '')
.replace(/\/+/g, '/');
return fragment;
},
findRouteByName : function (routeName) {
var route;
for (var key in this.routes) {
if (this.routes[key] === routeName) {
route = key;
break;
}
}
return route === undefined ? null : route;
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment