UNPKG

rendr

Version:

Render your Backbone.js apps on the client and the server.

233 lines (191 loc) 5.8 kB
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; };