UNPKG

mixdown-router

Version:
290 lines (223 loc) 8.78 kB
var _ = require('lodash'); var ControllerFactory = require('./lib/controller_factory.js'); var Router = require('./lib/router.js'); var url = require('url'); var Generator = require('./lib/generator.js'); var events = require('events'); var MockRequest = require('hammock').Request; var MockResponse = require('hammock').Response; var HttpContext = require('./lib/http_context.js'); require('substr-polyfill'); module.exports = Generator.extend({ _namespace_default: 'router', init: function (options) { var self = this; this._super(options); this.controllers = new ControllerFactory(options); // cache the bound scope functions for use in router.create() this.authenticate_cached = _.bind(this.authenticate, this); this.authorize_cached = _.bind(this.authorize, this); // bubble both events this.controllers.on('invalid-route', function (data) { self.emit('invalid-route', data); }); // bubble both events this.controllers.on('no-browser-handler', function (data) { self.emit('no-browser-handler', data); }); }, controllers: null, clientRouter: null, root: typeof (window) !== 'undefined' && window.location ? window.location.pathname : null, authorize: function (httpContext, callback) { callback(null, httpContext); }, authenticate: function (httpContext, callback) { httpContext.user = { logged_in: false }; callback(null, httpContext); }, hasPushState: function () { return !!(window.history && window.history.pushState); }, // Functions that use the route table and controller manifest. manifest: function (sanitize) { if (sanitize) { var manifestCopy = _.cloneDeep(this.controllers.manifest()); _.each(manifestCopy, function (m, name) { _.each(m.params, function (p) { if (typeof (p.regex) !== 'string' && p.regex.source) { p.regex = p.regex.source; } }); }); return manifestCopy; } else { return this.controllers.manifest(); } }, // creates a router instance. This is the single server side node interface. create: function () { var new_router = new Router({ controllers: this.controllers, timeout: this._options.timeout, app: this._options.app }); new_router._authenticate = this.authenticate_cached; new_router._authorize = this.authorize_cached; return new_router; }, // client side url navigation // 2 overloaded options // // 1. function(route, params, callback) // @param route {String}: named route to generate the url. // @param params {Object}: hash of params to send to url generation. // // 2. function(url, callback) // @param url {Object|String}: Can be url string or node url object. navigate: function (route, params, callback) { var self = this; if (!route) { return callback(new Error('Route not defined.')); } // handle 2 arg variant function signatures. if (arguments.length === 2) { var arg1 = arguments[1]; if (typeof (arg1) === 'function') { callback = arg1; params = null; } else { callback = null; params = arg1; } } // keep a single instance around in a browser. if (!this.clientRouter) { this.clientRouter = this.create(); this.clientRouter.on('error', function (err) { console.log(err.stack); }); } var newUrl = null; var loc = this.location || url.parse(window.location.href); // If the route is in the route table, then generate the url. If not, check for hash or finally a literal url. if (this.manifest()[route]) { newUrl = this.url(route, params); } else { newUrl = url.parse(route); } this.emit('navigate', newUrl); // old school url change for browsers w/o pushstate or without the polyfill. if (this.initialized && !this.hasPushState()) { window.location.href = url.format(newUrl); return; } var req_opt = { url: url.format(newUrl) }; var req = new MockRequest(req_opt); var res = new MockResponse(); // if the route was matched, then change the url. this.clientRouter.once('match', function (httpContext) { if (httpContext.url.href !== window.location.href) { // if supports pushState, then use it. if not, then the controllers will already have replaced location in browser. if (self.hasPushState() && httpContext.controller && httpContext.controller.browser) { // emit page_loaded on all handler matches. self.emit('page_loaded', httpContext); if (httpContext.url.pathname === window.location.pathname && httpContext.url.search == window.location.search && (httpContext.url.hash || '') == window.location.hash) { window.history.replaceState(req_opt, document.title, httpContext.url.href); } else { window.history.pushState(req_opt, document.title, httpContext.url.href); } } } else { self.location = url.parse(window.location.href); } }); // fire callback once the handler has executed. Note: javascript is async. The handler might not be done when this callback is fired... but you already knew that! this.clientRouter.once('end', function () { // these are here b/c EventEmitter.once() does not remove the event properly after it executes in // older browsers (specifically IE8) if (!self._hasPushState) { self.clientRouter.removeAllListeners('match'); self.clientRouter.removeAllListeners('end'); } return typeof (callback) === 'function' ? callback.apply(self, arguments) : null; }); this.clientRouter.dispatch(req, res); return req; }, // Used to call a controller within another controller. tunnel: function (route, params, parentContext, callback) { var controller = this.controllers.controllers[route]; if (!controller) { return callback(new Error('No controller for route name - ' + route)); } var httpContext = new HttpContext( new MockRequest({ url: this.format(route, params), method: parentContext.request.method }), new MockResponse() ); httpContext.app = parentContext.app; httpContext.id = parentContext.id; httpContext.user = parentContext.user; httpContext.user_agent = parentContext.user_agent; httpContext.body = parentContext.body; httpContext.files = parentContext.files; httpContext.controller = controller; controller.parse(httpContext); httpContext.response.on('finish', function () { var body = httpContext.response.buffer.join(''); if (httpContext.response.statusCode != 200) { return callback(new Error('Response failed: ' + httpContext.response.statusCode), httpContext.response, body); } if (/json/.test(httpContext.response.getHeader('Content-Type'))) { try { body = JSON.parse(body); } catch (e) { return callback(e, httpContext.response, body); } } callback(null, httpContext.response, body); }); controller.handle(httpContext); }, // For browsers: attach to popstate and perform initial navigate match listen: function (callback) { var self = this; if (this.hasPushState()) { // The popstate event - A popstate event is dispatched to the window every time the active history // entry changes. If the history entry being activated was created by a call to pushState or affected // by a call to replaceState, the popstate event's state property contains a copy of the history // entry's state object. // https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history // https://developer.mozilla.org/en-US/docs/Web/API/window.onpopstate // Previous comment - "WebKit-based browsers fire onpopstate on page load."" // This is only true when the script is evaluated before the page is fully loaded. // This implies that the router is starting to listen before the DOM is completely ready. window.onpopstate = function (e) { var state = e.state || {}; var new_url = state.url; self.navigate(new_url || window.location.href, function (err) { if (err) { window.location.reload(); } }); }; } self.navigate(window.location.href, function (err) { self.initialized = true; self.emit('initialized'); ((typeof (callback) === 'function' ? callback(err) : null)); }); }, _setup: function (done) { this.controllers.init(done); } });