123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587 |
- 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;
|