UNPKG

rendr

Version:

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

299 lines (243 loc) 7.96 kB
/** * Since we make rendr files AMD friendly on app setup stage * we need to pretend that this code is pure commonjs * means no AMD-style require calls */ var _ = require('underscore'), Backbone = require('backbone'), BaseRouter = require('../shared/base/router'), BaseView = require('../shared/base/view'), isServer = (typeof window === 'undefined'), extractParamNamesRe = /:(\w+)/g, plusRe = /\+/g, firstRender = true, defaultRootPath = ''; if (!isServer) { Backbone.$ = window.$ || require('jquery'); } module.exports = ClientRouter; function ClientRouter(options) { this._router = new Backbone.Router(); BaseRouter.apply(this, arguments); this.app = options.app; var AppView = this.options.appViewClass; // We do this here so that it's available in AppView initialization. this.app.router = this; this.on('route:add', this.addBackboneRoute, this); this.on('action:start', this.trackAction, this); this.app.on('reload', this.renderView, this); this.appView = new AppView({ app: this.app }); this.appView.render(); this.buildRoutes(); this.initialize(options); } /** * Set up inheritance. */ ClientRouter.prototype = Object.create(BaseRouter.prototype); ClientRouter.prototype.constructor = ClientRouter; ClientRouter.prototype.currentFragment = null; ClientRouter.prototype.previousFragment = null; /** * In a controller action, can access the current route * definition with `this.currentRoute`. */ ClientRouter.prototype.currentRoute = null; /** * Instance of Backbone.Router used to manage browser history. */ ClientRouter.prototype._router = null; /** * We need to reverse the routes in the client because * Backbone.History matches in reverse. */ ClientRouter.prototype.reverseRoutes = true; ClientRouter.prototype.initialize = _.noop; /** * Piggyback on adding new route definition events * to also add to Backbone.Router. */ ClientRouter.prototype.addBackboneRoute = function(routeObj) { var handler, name, pattern, route; // Backbone.History wants no leading slash on strings. pattern = (routeObj[0] instanceof RegExp) ? routeObj[0] : routeObj[0].slice(1); route = routeObj[1]; handler = routeObj[2]; name = route.controller + ":" + route.action; this._router.route(pattern, name, handler); }; ClientRouter.prototype.getHandler = function(action, pattern, route) { var router = this; // abstract action call function actionCall(action, params) { action.call(router, params, router.getRenderCallback(route)); } // This returns a function which is called by Backbone.history. return function() { var params, paramsArray, redirect; router.trigger('action:start', route, firstRender); router.currentRoute = route; if (firstRender) { firstRender = false; BaseView.getChildViews(router.app, null, function(views) { router.currentView = router.getMainView(views); router.trigger('action:end', route, true); }); } else { paramsArray = _.toArray(arguments); params = router.getParamsHash(pattern, paramsArray, window.location.search); redirect = router.getRedirect(route, params); /** * If `redirect` is present, then do a redirect and return. */ if (redirect != null) { router.redirectTo(redirect, {replace: true}); } else { if (!action) { throw new Error("Missing action \"" + route.action + "\" for controller \"" + route.controller + "\""); } else { actionCall(action, params); } } } }; }; /** * Can be overridden by applications * if the initial render is more complicated. */ ClientRouter.prototype.getMainView = function(views) { var $content = this.appView.$content; return _.find(views, function(view) { return view.$el.parent().is($content); }); }; /** * Proxy to Backbone.Router. */ ClientRouter.prototype.navigate = function(path, options) { var fragment = Backbone.history.getFragment(path); // check if local router can handle route if (this.matchesAnyRoute(fragment)) { this._router.navigate.apply(this._router, arguments); } else { this.redirectTo(fragment, {pushState: false}); } }; ClientRouter.prototype.getParamsHash = function(pattern, paramsArray, search) { var paramNames, params, query; if (pattern instanceof RegExp) { paramNames = paramsArray.map(function(val, i) { return String(i); }); } else { paramNames = (pattern.match(extractParamNamesRe) || []).map(function(name) { return name.slice(1); }); } params = (paramNames || []).reduce(function(memo, name, i) { memo[name] = decodeURIComponent(paramsArray[i]); return memo; }, {}); query = search.slice(1).split('&').reduce(function(memo, queryPart) { var parts = queryPart.split('='); if (parts.length > 1) { memo[parts[0]] = decodeURIComponent(parts[1].replace(plusRe, ' ')); } return memo; }, {}); return _.extend(query, params); }; ClientRouter.prototype.matchingRoute = function(path) { return _.find(Backbone.history.handlers, function(handler) { return handler.route.test(path); }); }; ClientRouter.prototype.matchesAnyRoute = function(path) { return this.matchingRoute(path) != null; }; ClientRouter.prototype.redirectTo = function(path, options) { var hashParts; if (options == null) { options = {}; } _.defaults(options, { trigger: true, pushState: true, replace: false }); if (options.pushState === false) { // Do a full-page redirect. this.exitApp(path); } else { // Do a pushState navigation. hashParts = path.split('#'); path = hashParts[0]; // But then trigger the hash afterwards. if (hashParts.length > 1) { this.once('action:end', function() { window.location.hash = hashParts[1]; }); } // Ignore hash for routing. this.navigate(path, options); } }; ClientRouter.prototype.exitApp = function (path) { var exitPath = this.noRelativePath(path); window.location.href = exitPath; } ClientRouter.prototype.noRelativePath = function (path) { //if path doesn't have a protocol and lacks a leading slash if (/^[a-z]+:/i.test(path) === false && path.charAt(0) !== '/') { path = '/' + path; } return path; } ClientRouter.prototype.handleErr = function(err, route) { this.trigger('action:error', err, route); } ClientRouter.prototype.getRenderCallback = function(route) { return function(err, viewPath, locals) { if (err) return this.handleErr(err, route); var View, _router = this; if (this.currentView) { this.currentView.remove(); } var defaults = this.defaultHandlerParams(viewPath, locals, route); viewPath = defaults[0]; locals = defaults[1]; locals = locals || {}; _.extend(locals, { fetch_summary: BaseView.extractFetchSummary(this.app.modelUtils, locals) }); // Inject the app. locals.app = this.app; this.getView(viewPath, this.options.entryPath, function(View) { _router.currentView = new View(locals); _router.renderView(); _router.trigger('action:end', route, firstRender); }); }.bind(this); }; ClientRouter.prototype.renderView = function() { this.appView.setCurrentView(this.currentView); }; ClientRouter.prototype.start = function() { Backbone.history.start({ pushState: true, hashChange: false, root: this.options.rootPath || defaultRootPath }); }; ClientRouter.prototype.trackAction = function() { this.previousFragment = this.currentFragment; this.currentFragment = Backbone.history.getFragment(); }; ClientRouter.prototype.getView = function(key, entryPath, callback) { var View = BaseView.getView(key, entryPath, function(View) { // TODO: Make it function (err, View) if (!_.isFunction(View)) { throw new Error("View '" + key + "' not found."); } callback(View); }); };