Created
September 19, 2012 04:16
-
-
Save ianstormtaylor/3747665 to your computer and use it in GitHub Desktop.
Enhanced Backbone Router to make a lot of things easier.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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