rendr
Version:
Render your Backbone.js apps on the client and the server.
233 lines (191 loc) • 5.8 kB
JavaScript
var _ = require('underscore'),
Backbone = require('backbone'),
isServer = (typeof window === 'undefined'),
isAMDEnvironment = !isServer && (typeof define !== 'undefined'),
loadNumber = 0;
if (!isServer) {
Backbone.$ = window.$ || require('jquery');
}
function stringRouteDefinitionToObject(element) {
var parts = element.split('#');
return {
controller: parts[0],
action: parts[1]
};
}
function parseRouteDefinitions(definitions) {
return definitions.reduce(function(route, element) {
if (_.isString(element)) {
element = stringRouteDefinitionToObject(element);
}
return _.extend(route, element);
}, {});
}
/**
* Base router class shared between ClientRouter and ServerRouter.
*/
function BaseRouter(options) {
this.route = this.route.bind(this);
this._routes = [];
this._initOptions(options);
}
_.extend(BaseRouter.prototype, Backbone.Events, {
/**
* Config
* - errorHandler: function to correctly handle error
* - paths
* - entryPath (required)
* - routes (optional)
* - controllerDir (optional)
*/
options: null,
/**
* Internally stored route definitions.
*/
_routes: null,
reverseRoutes: false,
initialize: _.noop,
_initOptions: function(options) {
var entryPath;
options = options || {};
options.paths = options.paths || {};
entryPath = options.paths.entryPath || options.entryPath;
options.paths = _.defaults(options.paths, {
entryPath: entryPath,
routes: entryPath + 'app/routes',
controllerDir: entryPath + 'app/controllers'
});
this.options = options;
},
getControllerPath: function(controllerName) {
var controllerDir = this.options.paths.controllerDir;
return controllerDir + '/' + controllerName + '_controller';
},
loadController: function(controllerName) {
var controllerPath = this.getControllerPath(controllerName);
return require(controllerPath);
},
getAction: function(route) {
var controller, action;
if (route.controller) {
if (isAMDEnvironment) {
action = this.getControllerPath(route.controller);
} else {
controller = this.loadController(route.controller);
action = controller[route.action];
}
}
return action;
},
getRedirect: function(route, params) {
var redirect = route.redirect;
if (typeof redirect === 'function') {
redirect = redirect(params);
}
return redirect;
},
getRouteBuilder: function() {
return require(this.options.paths.routes);
},
buildRoutes: function() {
var routeBuilder = this.getRouteBuilder(),
routes = [];
function captureRoutes() {
routes.push(_.toArray(arguments));
}
routeBuilder(captureRoutes);
if (this.reverseRoutes) {
routes = routes.reverse();
}
routes.forEach(this.addRouteDefinition, this);
return this.routes();
},
addRouteDefinition: function(route) {
try {
this.route.apply(this, route);
} catch (error) {
error.message = 'Error building routes (' + error.message + ')';
throw error;
}
},
/**
* Returns a copy of current route definitions.
*/
routes: function() {
return this._routes.slice().map(function(route) {
return route.slice();
});
},
/**
* Method passed to routes file to build up routes definition.
* Adds a single route definition.
*/
route: function(pattern, controller, options) {
var realAction, action, handler, route, routeObj, routerContext = this;
route = parseRouteDefinitions([controller, options]);
realAction = this.getAction(route);
if (isServer) {
action = realAction;
} else {
action = function(params, callback) {
var self = this;
var myLoadNumber = ++loadNumber;
function next() {
// To prevent race conditions we ensure that no future requests have been processed in the mean time.
if (myLoadNumber === loadNumber) {
callback.apply(self, arguments);
}
}
// in AMD environment realAction is the string containing path to the controller
// which will be loaded async (might be preloaded)
// Only used in AMD environment
if (typeof realAction === 'string') {
routerContext._requireAMD([realAction], function(controller) {
// check we have everything we need
if (typeof controller[route.action] != 'function') {
throw new Error("Missing action \"" + route.action + "\" for controller \"" + route.controller + "\"");
}
controller[route.action].call(self, params, next);
});
}
else {
realAction.call(self, params, next);
}
}
}
if (!(pattern instanceof RegExp) && pattern.slice(0, 1) !== '/') {
pattern = "/" + pattern;
}
handler = this.getHandler(action, pattern, route);
routeObj = [pattern, route, handler];
this._routes.push(routeObj);
this.trigger('route:add', routeObj);
return routeObj;
},
/**
* exposing for mocking in test
*/
_requireAMD: require,
/**
* Support omitting view path; default it to ":controller/:action".
*/
defaultHandlerParams: function(viewPath, locals, route) {
if (typeof viewPath !== 'string') {
locals = viewPath;
viewPath = route.controller + '/' + route.action;
}
return [viewPath, locals];
},
/**
* Methods to be extended by subclasses.
* -------------------------------------
*/
/**
* This is the method that renders the request.
*/
getHandler: _.noop
});
module.exports = BaseRouter;
module.exports.setAMDEnvironment = function(flag) {
isAMDEnvironment = flag;
};