@optimizely/nuclear-router
Version:
NuclearJS Router
353 lines (289 loc) • 11.3 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
* @module
*/
/**
* Input to Client Decision Engine
*
* @typedef Route
* @property {String|RegExp} match
* @property {RouterHandler[]} handlers
*/
/**
* @callback RouteHandler
* @param {Context} ctx
* @param {Function} nextFn
*/
/**
* @typedef {"push" | "replace" | "pop"} Mode
*/
var _objectAssign = require('object-assign');
var _objectAssign2 = _interopRequireDefault(_objectAssign);
var _Context = require('./Context');
var _Context2 = _interopRequireDefault(_Context);
var _fns = require('./fns');
var _fns2 = _interopRequireDefault(_fns);
var _Route = require('./Route');
var _Route2 = _interopRequireDefault(_Route);
var _WindowEnv = require('./WindowEnv');
var _WindowEnv2 = _interopRequireDefault(_WindowEnv);
var _HistoryEnv = require('./HistoryEnv');
var _HistoryEnv2 = _interopRequireDefault(_HistoryEnv);
var _DocumentEnv = require('./DocumentEnv');
var _DocumentEnv2 = _interopRequireDefault(_DocumentEnv);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Router = function () {
function Router(opts) {
_classCallCheck(this, Router);
this.opts = opts || {};
this.opts = (0, _objectAssign2.default)({
pushstate: true,
base: ''
}, opts);
this.onRouteStart = this.opts.onRouteStart;
this.onRouteComplete = this.opts.onRouteComplete;
this.__onpopstate = this.__onpopstate.bind(this);
this.__filterPopstateEvent = this.opts.filterPopstateEvent || function () {
return true;
};
}
_createClass(Router, [{
key: 'initialize',
value: function initialize() {
this.setInitialState();
_WindowEnv2.default.addEventListener('popstate', this.__onpopstate);
}
}, {
key: 'reset',
value: function reset() {
this.setInitialState();
_WindowEnv2.default.removeEventListener('popstate', this.__onpopstate);
}
}, {
key: 'setInitialState',
value: function setInitialState() {
this.__fromPath = null;
this.__routes = [];
this.__currentCanonicalPath = null;
this.__catchallPath = null;
this.__dispatchId = 0;
this.__startTime = null;
this.__shouldHandlePopstateEvents = true;
}
/**
* Registers Routes for the application
*
* ```
* var router = new Router(reactor)
* router.registerRoutes([
* {
* match: '/foo',
* handlers: [
* (ctx, next) => {
* fetchDataForFoo()
* next()
* }
* ]
* }
* ```
*
* @param {Route[]} routes
*/
}, {
key: 'registerRoutes',
value: function registerRoutes(routes) {
var _this = this;
if (!Array.isArray(routes)) {
throw new Error('Router#registerRoutes must be passed an array of Routes');
}
routes.forEach(function (route) {
_this.__routes.push(new _Route2.default(route));
});
}
/**
* @param {String} path
*/
}, {
key: 'registerCatchallPath',
value: function registerCatchallPath(path) {
this.__catchallPath = path;
}
/**
* @param {String} canonicalPath
* @param {Mode} mode
*/
}, {
key: 'go',
value: function go(canonicalPath) {
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'push';
this.__dispatch(canonicalPath, mode);
}
/**
* @param {String} canonicalPath
*/
}, {
key: 'replace',
value: function replace(canonicalPath) {
this.__dispatch(canonicalPath, 'replace');
}
}, {
key: 'catchall',
value: function catchall() {
if (typeof this.__catchallPath === 'string') {
_WindowEnv2.default.navigate(this.__catchallPath);
}
}
/**
* @param {function} fn - Async function to execute w/o popstate listener
*/
}, {
key: 'executeWithoutPopstateListener',
value: function executeWithoutPopstateListener(fn) {
var _this2 = this;
this.__shouldHandlePopstateEvents = false;
// Execute the function, then re-enable the popstate listener
return fn().then(function (result) {
_this2.__shouldHandlePopstateEvents = true;
return Promise.resolve(result);
}, function (error) {
_this2.__shouldHandlePopstateEvents = true;
return Promise.reject(error);
});
}
/**
* Execute a single route, which consists of:
* - Updating the url via pushstate/replacestate (if specified)
* - Running the route's handlers
* - Invoking the onRouteStart and onRouteComplete handlers (if specified)
* @param {String} path
* @param {String} canonicalPath
* @param {Mode} mode
* @param {Object} routeData
* @param {Router} routeData.route
* @param {Object} routeData.params
* @private
*/
}, {
key: '__executeRoute',
value: function __executeRoute(path, canonicalPath, mode, _ref) {
var _this3 = this;
var route = _ref.route,
params = _ref.params;
var title = _DocumentEnv2.default.getTitle();
var ctx = new _Context2.default({ canonicalPath: canonicalPath, path: path, title: title, params: params, dispatchId: this.__dispatchId });
if (mode === 'replace') {
_HistoryEnv2.default.replaceState.apply(null, ctx.getHistoryArgs());
} else if (mode === 'push') {
_HistoryEnv2.default.pushState.apply(null, ctx.getHistoryArgs());
}
this.__currentCanonicalPath = canonicalPath;
var routeMetadata = route.metadata || {};
if (this.onRouteStart && mode !== 'replace') {
this.onRouteStart({
routeMetadata: routeMetadata,
fromPath: this.__fromPath || 'PAGE LOAD',
startTime: this.__startTime,
context: ctx
});
}
this.__runHandlers(route.handlers, ctx, function () {
var startTime = _this3.__startTime;
var endTime = _fns2.default.getNow();
var duration = endTime - startTime;
var fromPath = _this3.__fromPath || 'PAGE LOAD';
var toPath = canonicalPath;
if (_this3.onRouteComplete) {
_this3.onRouteComplete({
fromPath: fromPath,
toPath: toPath,
duration: duration,
startTime: startTime,
endTime: endTime,
routeMetadata: routeMetadata
});
}
_this3.__fromPath = canonicalPath;
});
}
}, {
key: '__runHandlers',
/**
* @param {RouterHandler[]} handlers
* @param {Context} ctx
*/
value: function __runHandlers(handlers, ctx, callback) {
var _this4 = this;
var index = 0;
var next = function next() {
if (_this4.__dispatchId !== ctx.dispatchId) {
return;
}
var handlerFnOrArray = handlers[index];
index++;
// capture handler index in closure since index could be modified by another thread
var handlerIndex = index;
if (handlerFnOrArray) {
if (Array.isArray(handlerFnOrArray)) {
var parallelHandlersComplete = 0;
// for parallel handlers we use a custom next callback that tracks completion of all handlers in the group
var parallelNext = function parallelNext() {
parallelHandlersComplete++;
if (parallelHandlersComplete === handlerFnOrArray.length) {
next(); // all grouped parallel handlers complete, so invoke the next top-level handler or handler array
}
};
// execute all handlers in parallel group
handlerFnOrArray.map(function (handlerFn) {
return handlerFn(ctx, parallelNext);
});
} else {
handlerFnOrArray(ctx, next);
if (callback && handlerIndex === handlers.length && _this4.__dispatchId === ctx.dispatchId) {
callback();
}
}
}
};
next();
}
/**
* @param {String} canonicalPath
* @param {Mode} mode
*/
}, {
key: '__dispatch',
value: function __dispatch(canonicalPath) {
var _this5 = this;
var mode = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'push';
this.__dispatchId++;
if (mode !== 'replace') {
this.__startTime = _fns2.default.getNow();
}
var path = _fns2.default.extractPath(this.opts.base, canonicalPath);
var matches = _fns2.default.matchRoute(this.__routes, path);
_fns2.default.filterMatches(matches).then(function (match) {
return _this5.__executeRoute(path, canonicalPath, mode, match);
}, function () {
return _this5.catchall();
});
}
}, {
key: '__onpopstate',
value: function __onpopstate(e) {
if (e.state && this.__shouldHandlePopstateEvents) {
// Make sure this popstate event passes the filter fn and wasn't triggered
// as a result of the currently in progress dispatch.
if (this.__filterPopstateEvent(e) && e.state.nuclearDispatchId !== this.__dispatchId) {
this.__dispatch(e.state.path, 'pop');
}
}
}
}]);
return Router;
}();
exports.default = Router;
module.exports = exports['default'];