Router = function () { var self = this; this.globals = []; this.subscriptions = Function.prototype; this._tracker = this._buildTracker(); this._current = {}; // tracks the current path change this._onEveryPath = new Tracker.Dependency(); this._globalRoute = new Route(this); // holds onRoute callbacks this._onRouteCallbacks = []; // if _askedToWait is true. We don't automatically start the router // in Meteor.startup callback. (see client/_init.js) // Instead user need to call `.initialize() this._askedToWait = false; this._initialized = false; this._triggersEnter = []; this._triggersExit = []; this._routes = []; this._routesMap = {}; this._updateCallbacks(); this.notFound = this.notfound = null; // indicate it's okay (or not okay) to run the tracker // when doing subscriptions // using a number and increment it help us to support FlowRouter.go() // and legitimate reruns inside tracker on the same event loop. // this is a solution for #145 this.safeToRun = 0; // Meteor exposes to the client the path prefix that was defined using the // ROOT_URL environement variable on the server using the global runtime // configuration. See #315. this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ''; // this is a chain contains a list of old routes // most of the time, there is only one old route // but when it's the time for a trigger redirect we've a chain this._oldRouteChain = []; this.env = { replaceState: new Meteor.EnvironmentVariable(), reload: new Meteor.EnvironmentVariable(), trailingSlash: new Meteor.EnvironmentVariable() }; // redirect function used inside triggers this._redirectFn = function(pathDef, fields, queryParams) { if (/^http(s)?:\/\//.test(pathDef)) { var message = "Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead"; throw new Error(message); } self.withReplaceState(function() { var path = FlowRouter.path(pathDef, fields, queryParams); self._page.redirect(path); }); }; this._initTriggersAPI(); }; Router.prototype.route = function(pathDef, options, group) { if (!/^\/.*/.test(pathDef)) { var message = "route's path must start with '/'"; throw new Error(message); } options = options || {}; var self = this; var route = new Route(this, pathDef, options, group); // calls when the page route being activates route._actionHandle = function (context, next) { var oldRoute = self._current.route; self._oldRouteChain.push(oldRoute); var queryParams = self._qs.parse(context.querystring); // _qs.parse() gives us a object without prototypes, // created with Object.create(null) // Meteor's check doesn't play nice with it. // So, we need to fix it by cloning it. // see more: https://github.com/meteorhacks/flow-router/issues/164 queryParams = JSON.parse(JSON.stringify(queryParams)); self._current = { path: context.path, context: context, params: context.params, queryParams: queryParams, route: route, oldRoute: oldRoute }; // we need to invalidate if all the triggers have been completed // if not that means, we've been redirected to another path // then we don't need to invalidate var afterAllTriggersRan = function() { self._invalidateTracker(); }; var triggers = self._triggersEnter.concat(route._triggersEnter); Triggers.runTriggers( triggers, self._current, self._redirectFn, afterAllTriggersRan ); }; // calls when you exit from the page js route route._exitHandle = function(context, next) { var triggers = self._triggersExit.concat(route._triggersExit); Triggers.runTriggers( triggers, self._current, self._redirectFn, next ); }; this._routes.push(route); if (options.name) { this._routesMap[options.name] = route; } this._updateCallbacks(); this._triggerRouteRegister(route); return route; }; Router.prototype.group = function(options) { return new Group(this, options); }; Router.prototype.path = function(pathDef, fields, queryParams) { if (this._routesMap[pathDef]) { pathDef = this._routesMap[pathDef].pathDef; } var path = ""; // Prefix the path with the router global prefix if (this._basePath) { path += "/" + this._basePath + "/"; } fields = fields || {}; var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g; path += pathDef.replace(regExp, function(key) { var firstRegexpChar = key.indexOf("("); // get the content behind : and (\\d+/) key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined); // remove +?* key = key.replace(/[\+\*\?]+/g, ""); // this is to allow page js to keep the custom characters as it is // we need to encode 2 times otherwise "/" char does not work properly // So, in that case, when I includes "/" it will think it's a part of the // route. encoding 2times fixes it return encodeURIComponent(encodeURIComponent(fields[key] || "")); }); // Replace multiple slashes with single slash path = path.replace(/\/\/+/g, "/"); // remove trailing slash // but keep the root slash if it's the only one path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, ""); // explictly asked to add a trailing slash if(this.env.trailingSlash.get() && _.last(path) !== "/") { path += "/"; } var strQueryParams = this._qs.stringify(queryParams || {}); if(strQueryParams) { path += "?" + strQueryParams; } return path; }; Router.prototype.go = function(pathDef, fields, queryParams) { var path = this.path(pathDef, fields, queryParams); var useReplaceState = this.env.replaceState.get(); if(useReplaceState) { this._page.replace(path); } else { this._page(path); } }; Router.prototype.reload = function() { var self = this; self.env.reload.withValue(true, function() { self._page.replace(self._current.path); }); }; Router.prototype.redirect = function(path) { this._page.redirect(path); }; Router.prototype.setParams = function(newParams) { if(!this._current.route) {return false;} var pathDef = this._current.route.pathDef; var existingParams = this._current.params; var params = {}; _.each(_.keys(existingParams), function(key) { params[key] = existingParams[key]; }); params = _.extend(params, newParams); var queryParams = this._current.queryParams; this.go(pathDef, params, queryParams); return true; }; Router.prototype.setQueryParams = function(newParams) { if(!this._current.route) {return false;} var queryParams = _.clone(this._current.queryParams); _.extend(queryParams, newParams); for (var k in queryParams) { if (queryParams[k] === null || queryParams[k] === undefined) { delete queryParams[k]; } } var pathDef = this._current.route.pathDef; var params = this._current.params; this.go(pathDef, params, queryParams); return true; }; // .current is not reactive // This is by design. use .getParam() instead // If you really need to watch the path change, use .watchPathChange() Router.prototype.current = function() { // We can't trust outside, that's why we clone this // Anyway, we can't clone the whole object since it has non-jsonable values // That's why we clone what's really needed. var current = _.clone(this._current); current.queryParams = EJSON.clone(current.queryParams); current.params = EJSON.clone(current.params); return current; }; // Implementing Reactive APIs var reactiveApis = [ 'getParam', 'getQueryParam', 'getRouteName', 'watchPathChange' ]; reactiveApis.forEach(function(api) { Router.prototype[api] = function(arg1) { // when this is calling, there may not be any route initiated // so we need to handle it var currentRoute = this._current.route; if(!currentRoute) { this._onEveryPath.depend(); return; } // currently, there is only one argument. If we've more let's add more args // this is not clean code, but better in performance return currentRoute[api].call(currentRoute, arg1); }; }); Router.prototype.subsReady = function() { var callback = null; var args = _.toArray(arguments); if (typeof _.last(args) === "function") { callback = args.pop(); } var currentRoute = this.current().route; var globalRoute = this._globalRoute; // we need to depend for every route change and // rerun subscriptions to check the ready state this._onEveryPath.depend(); if(!currentRoute) { return false; } var subscriptions; if(args.length === 0) { subscriptions = _.values(globalRoute.getAllSubscriptions()); subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions())); } else { subscriptions = _.map(args, function(subName) { return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName); }); } var isReady = function() { var ready = _.every(subscriptions, function(sub) { return sub && sub.ready(); }); return ready; }; if (callback) { Tracker.autorun(function(c) { if (isReady()) { callback(); c.stop(); } }); } else { return isReady(); } }; Router.prototype.withReplaceState = function(fn) { return this.env.replaceState.withValue(true, fn); }; Router.prototype.withTrailingSlash = function(fn) { return this.env.trailingSlash.withValue(true, fn); }; Router.prototype._notfoundRoute = function(context) { this._current = { path: context.path, context: context, params: [], queryParams: {}, }; // XXX this.notfound kept for backwards compatibility this.notFound = this.notFound || this.notfound; if(!this.notFound) { console.error("There is no route for the path:", context.path); return; } this._current.route = new Route(this, "*", this.notFound); this._invalidateTracker(); }; Router.prototype.initialize = function(options) { options = options || {}; if(this._initialized) { throw new Error("FlowRouter is already initialized"); } var self = this; this._updateCallbacks(); // Implementing idempotent routing // by overriding page.js`s "show" method. // Why? // It is impossible to bypass exit triggers, // because they execute before the handler and // can not know what the next path is, inside exit trigger. // // we need override both show, replace to make this work // since we use redirect when we are talking about withReplaceState _.each(['show', 'replace'], function(fnName) { var original = self._page[fnName]; self._page[fnName] = function(path, state, dispatch, push) { var reload = self.env.reload.get(); if (!reload && self._current.path === path) { return; } original.call(this, path, state, dispatch, push); }; }); // this is very ugly part of pagejs and it does decoding few times // in unpredicatable manner. See #168 // this is the default behaviour and we need keep it like that // we are doing a hack. see .path() this._page.base(this._basePath); this._page({ decodeURLComponents: true, hashbang: !!options.hashbang }); this._initialized = true; }; Router.prototype._buildTracker = function() { var self = this; // main autorun function var tracker = Tracker.autorun(function () { if(!self._current || !self._current.route) { return; } // see the definition of `this._processingContexts` var currentContext = self._current; var route = currentContext.route; var path = currentContext.path; if(self.safeToRun === 0) { var message = "You can't use reactive data sources like Session" + " inside the `.subscriptions` method!"; throw new Error(message); } // We need to run subscriptions inside a Tracker // to stop subs when switching between routes // But we don't need to run this tracker with // other reactive changes inside the .subscription method // We tackle this with the `safeToRun` variable self._globalRoute.clearSubscriptions(); self.subscriptions.call(self._globalRoute, path); route.callSubscriptions(currentContext); // otherwise, computations inside action will trigger to re-run // this computation. which we do not need. Tracker.nonreactive(function() { var isRouteChange = currentContext.oldRoute !== currentContext.route; var isFirstRoute = !currentContext.oldRoute; // first route is not a route change if(isFirstRoute) { isRouteChange = false; } // Clear oldRouteChain just before calling the action // We still need to get a copy of the oldestRoute first // It's very important to get the oldest route and registerRouteClose() it // See: https://github.com/kadirahq/flow-router/issues/314 var oldestRoute = self._oldRouteChain[0]; self._oldRouteChain = []; currentContext.route.registerRouteChange(currentContext, isRouteChange); route.callAction(currentContext); Tracker.afterFlush(function() { self._onEveryPath.changed(); if(isRouteChange) { // We need to trigger that route (definition itself) has changed. // So, we need to re-run all the register callbacks to current route // This is pretty important, otherwise tracker // can't identify new route's items // We also need to afterFlush, otherwise this will re-run // helpers on templates which are marked for destroying if(oldestRoute) { oldestRoute.registerRouteClose(); } } }); }); self.safeToRun--; }); return tracker; }; Router.prototype._invalidateTracker = function() { var self = this; this.safeToRun++; this._tracker.invalidate(); // After the invalidation we need to flush to make changes imediately // otherwise, we have face some issues context mix-maches and so on. // But there are some cases we can't flush. So we need to ready for that. // we clearly know, we can't flush inside an autorun // this may leads some issues on flow-routing // we may need to do some warning if(!Tracker.currentComputation) { // Still there are some cases where we can't flush // eg:- when there is a flush currently // But we've no public API or hacks to get that state // So, this is the only solution try { Tracker.flush(); } catch(ex) { // only handling "while flushing" errors if(!/Tracker\.flush while flushing/.test(ex.message)) { return; } // XXX: fix this with a proper solution by removing subscription mgt. // from the router. Then we don't need to run invalidate using a tracker // this happens when we are trying to invoke a route change // with inside a route chnage. (eg:- Template.onCreated) // Since we use page.js and tracker, we don't have much control // over this process. // only solution is to defer route execution. // It's possible to have more than one path want to defer // But, we only need to pick the last one. // self._nextPath = self._current.path; Meteor.defer(function() { var path = self._nextPath; if(!path) { return; } delete self._nextPath; self.env.reload.withValue(true, function() { self.go(path); }); }); } } }; Router.prototype._updateCallbacks = function () { var self = this; self._page.callbacks = []; self._page.exits = []; _.each(self._routes, function(route) { self._page(route.pathDef, route._actionHandle); self._page.exit(route.pathDef, route._exitHandle); }); self._page("*", function(context) { self._notfoundRoute(context); }); }; Router.prototype._initTriggersAPI = function() { var self = this; this.triggers = { enter: function(triggers, filter) { triggers = Triggers.applyFilters(triggers, filter); if(triggers.length) { self._triggersEnter = self._triggersEnter.concat(triggers); } }, exit: function(triggers, filter) { triggers = Triggers.applyFilters(triggers, filter); if(triggers.length) { self._triggersExit = self._triggersExit.concat(triggers); } } }; }; Router.prototype.wait = function() { if(this._initialized) { throw new Error("can't wait after FlowRouter has been initialized"); } this._askedToWait = true; }; Router.prototype.onRouteRegister = function(cb) { this._onRouteCallbacks.push(cb); }; Router.prototype._triggerRouteRegister = function(currentRoute) { // We should only need to send a safe set of fields on the route // object. // This is not to hide what's inside the route object, but to show // these are the public APIs var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path'); var omittingOptionFields = [ 'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name' ]; routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields); _.each(this._onRouteCallbacks, function(cb) { cb(routePublicApi); }); }; Router.prototype._page = page; Router.prototype._qs = qs;