router.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. Router = function () {
  2. var self = this;
  3. this.globals = [];
  4. this.subscriptions = Function.prototype;
  5. this._tracker = this._buildTracker();
  6. this._current = {};
  7. // tracks the current path change
  8. this._onEveryPath = new Tracker.Dependency();
  9. this._globalRoute = new Route(this);
  10. // holds onRoute callbacks
  11. this._onRouteCallbacks = [];
  12. // if _askedToWait is true. We don't automatically start the router
  13. // in Meteor.startup callback. (see client/_init.js)
  14. // Instead user need to call `.initialize()
  15. this._askedToWait = false;
  16. this._initialized = false;
  17. this._triggersEnter = [];
  18. this._triggersExit = [];
  19. this._routes = [];
  20. this._routesMap = {};
  21. this._updateCallbacks();
  22. this.notFound = this.notfound = null;
  23. // indicate it's okay (or not okay) to run the tracker
  24. // when doing subscriptions
  25. // using a number and increment it help us to support FlowRouter.go()
  26. // and legitimate reruns inside tracker on the same event loop.
  27. // this is a solution for #145
  28. this.safeToRun = 0;
  29. // Meteor exposes to the client the path prefix that was defined using the
  30. // ROOT_URL environement variable on the server using the global runtime
  31. // configuration. See #315.
  32. this._basePath = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
  33. // this is a chain contains a list of old routes
  34. // most of the time, there is only one old route
  35. // but when it's the time for a trigger redirect we've a chain
  36. this._oldRouteChain = [];
  37. this.env = {
  38. replaceState: new Meteor.EnvironmentVariable(),
  39. reload: new Meteor.EnvironmentVariable(),
  40. trailingSlash: new Meteor.EnvironmentVariable()
  41. };
  42. // redirect function used inside triggers
  43. this._redirectFn = function(pathDef, fields, queryParams) {
  44. if (/^http(s)?:\/\//.test(pathDef)) {
  45. var message = "Redirects to URLs outside of the app are not supported in this version of Flow Router. Use 'window.location = yourUrl' instead";
  46. throw new Error(message);
  47. }
  48. self.withReplaceState(function() {
  49. var path = FlowRouter.path(pathDef, fields, queryParams);
  50. self._page.redirect(path);
  51. });
  52. };
  53. this._initTriggersAPI();
  54. };
  55. Router.prototype.route = function(pathDef, options, group) {
  56. if (!/^\/.*/.test(pathDef)) {
  57. var message = "route's path must start with '/'";
  58. throw new Error(message);
  59. }
  60. options = options || {};
  61. var self = this;
  62. var route = new Route(this, pathDef, options, group);
  63. // calls when the page route being activates
  64. route._actionHandle = function (context, next) {
  65. var oldRoute = self._current.route;
  66. self._oldRouteChain.push(oldRoute);
  67. var queryParams = self._qs.parse(context.querystring);
  68. // _qs.parse() gives us a object without prototypes,
  69. // created with Object.create(null)
  70. // Meteor's check doesn't play nice with it.
  71. // So, we need to fix it by cloning it.
  72. // see more: https://github.com/meteorhacks/flow-router/issues/164
  73. queryParams = JSON.parse(JSON.stringify(queryParams));
  74. self._current = {
  75. path: context.path,
  76. context: context,
  77. params: context.params,
  78. queryParams: queryParams,
  79. route: route,
  80. oldRoute: oldRoute
  81. };
  82. // we need to invalidate if all the triggers have been completed
  83. // if not that means, we've been redirected to another path
  84. // then we don't need to invalidate
  85. var afterAllTriggersRan = function() {
  86. self._invalidateTracker();
  87. };
  88. var triggers = self._triggersEnter.concat(route._triggersEnter);
  89. Triggers.runTriggers(
  90. triggers,
  91. self._current,
  92. self._redirectFn,
  93. afterAllTriggersRan
  94. );
  95. };
  96. // calls when you exit from the page js route
  97. route._exitHandle = function(context, next) {
  98. var triggers = self._triggersExit.concat(route._triggersExit);
  99. Triggers.runTriggers(
  100. triggers,
  101. self._current,
  102. self._redirectFn,
  103. next
  104. );
  105. };
  106. this._routes.push(route);
  107. if (options.name) {
  108. this._routesMap[options.name] = route;
  109. }
  110. this._updateCallbacks();
  111. this._triggerRouteRegister(route);
  112. return route;
  113. };
  114. Router.prototype.group = function(options) {
  115. return new Group(this, options);
  116. };
  117. Router.prototype.path = function(pathDef, fields, queryParams) {
  118. if (this._routesMap[pathDef]) {
  119. pathDef = this._routesMap[pathDef].pathDef;
  120. }
  121. var path = "";
  122. // Prefix the path with the router global prefix
  123. if (this._basePath) {
  124. path += "/" + this._basePath + "/";
  125. }
  126. fields = fields || {};
  127. var regExp = /(:[\w\(\)\\\+\*\.\?]+)+/g;
  128. path += pathDef.replace(regExp, function(key) {
  129. var firstRegexpChar = key.indexOf("(");
  130. // get the content behind : and (\\d+/)
  131. key = key.substring(1, (firstRegexpChar > 0)? firstRegexpChar: undefined);
  132. // remove +?*
  133. key = key.replace(/[\+\*\?]+/g, "");
  134. // this is to allow page js to keep the custom characters as it is
  135. // we need to encode 2 times otherwise "/" char does not work properly
  136. // So, in that case, when I includes "/" it will think it's a part of the
  137. // route. encoding 2times fixes it
  138. return encodeURIComponent(encodeURIComponent(fields[key] || ""));
  139. });
  140. // Replace multiple slashes with single slash
  141. path = path.replace(/\/\/+/g, "/");
  142. // remove trailing slash
  143. // but keep the root slash if it's the only one
  144. path = path.match(/^\/{1}$/) ? path: path.replace(/\/$/, "");
  145. // explictly asked to add a trailing slash
  146. if(this.env.trailingSlash.get() && _.last(path) !== "/") {
  147. path += "/";
  148. }
  149. var strQueryParams = this._qs.stringify(queryParams || {});
  150. if(strQueryParams) {
  151. path += "?" + strQueryParams;
  152. }
  153. return path;
  154. };
  155. Router.prototype.go = function(pathDef, fields, queryParams) {
  156. var path = this.path(pathDef, fields, queryParams);
  157. var useReplaceState = this.env.replaceState.get();
  158. if(useReplaceState) {
  159. this._page.replace(path);
  160. } else {
  161. this._page(path);
  162. }
  163. };
  164. Router.prototype.reload = function() {
  165. var self = this;
  166. self.env.reload.withValue(true, function() {
  167. self._page.replace(self._current.path);
  168. });
  169. };
  170. Router.prototype.redirect = function(path) {
  171. this._page.redirect(path);
  172. };
  173. Router.prototype.setParams = function(newParams) {
  174. if(!this._current.route) {return false;}
  175. var pathDef = this._current.route.pathDef;
  176. var existingParams = this._current.params;
  177. var params = {};
  178. _.each(_.keys(existingParams), function(key) {
  179. params[key] = existingParams[key];
  180. });
  181. params = _.extend(params, newParams);
  182. var queryParams = this._current.queryParams;
  183. this.go(pathDef, params, queryParams);
  184. return true;
  185. };
  186. Router.prototype.setQueryParams = function(newParams) {
  187. if(!this._current.route) {return false;}
  188. var queryParams = _.clone(this._current.queryParams);
  189. _.extend(queryParams, newParams);
  190. for (var k in queryParams) {
  191. if (queryParams[k] === null || queryParams[k] === undefined) {
  192. delete queryParams[k];
  193. }
  194. }
  195. var pathDef = this._current.route.pathDef;
  196. var params = this._current.params;
  197. this.go(pathDef, params, queryParams);
  198. return true;
  199. };
  200. // .current is not reactive
  201. // This is by design. use .getParam() instead
  202. // If you really need to watch the path change, use .watchPathChange()
  203. Router.prototype.current = function() {
  204. // We can't trust outside, that's why we clone this
  205. // Anyway, we can't clone the whole object since it has non-jsonable values
  206. // That's why we clone what's really needed.
  207. var current = _.clone(this._current);
  208. current.queryParams = EJSON.clone(current.queryParams);
  209. current.params = EJSON.clone(current.params);
  210. return current;
  211. };
  212. // Implementing Reactive APIs
  213. var reactiveApis = [
  214. 'getParam', 'getQueryParam',
  215. 'getRouteName', 'watchPathChange'
  216. ];
  217. reactiveApis.forEach(function(api) {
  218. Router.prototype[api] = function(arg1) {
  219. // when this is calling, there may not be any route initiated
  220. // so we need to handle it
  221. var currentRoute = this._current.route;
  222. if(!currentRoute) {
  223. this._onEveryPath.depend();
  224. return;
  225. }
  226. // currently, there is only one argument. If we've more let's add more args
  227. // this is not clean code, but better in performance
  228. return currentRoute[api].call(currentRoute, arg1);
  229. };
  230. });
  231. Router.prototype.subsReady = function() {
  232. var callback = null;
  233. var args = _.toArray(arguments);
  234. if (typeof _.last(args) === "function") {
  235. callback = args.pop();
  236. }
  237. var currentRoute = this.current().route;
  238. var globalRoute = this._globalRoute;
  239. // we need to depend for every route change and
  240. // rerun subscriptions to check the ready state
  241. this._onEveryPath.depend();
  242. if(!currentRoute) {
  243. return false;
  244. }
  245. var subscriptions;
  246. if(args.length === 0) {
  247. subscriptions = _.values(globalRoute.getAllSubscriptions());
  248. subscriptions = subscriptions.concat(_.values(currentRoute.getAllSubscriptions()));
  249. } else {
  250. subscriptions = _.map(args, function(subName) {
  251. return globalRoute.getSubscription(subName) || currentRoute.getSubscription(subName);
  252. });
  253. }
  254. var isReady = function() {
  255. var ready = _.every(subscriptions, function(sub) {
  256. return sub && sub.ready();
  257. });
  258. return ready;
  259. };
  260. if (callback) {
  261. Tracker.autorun(function(c) {
  262. if (isReady()) {
  263. callback();
  264. c.stop();
  265. }
  266. });
  267. } else {
  268. return isReady();
  269. }
  270. };
  271. Router.prototype.withReplaceState = function(fn) {
  272. return this.env.replaceState.withValue(true, fn);
  273. };
  274. Router.prototype.withTrailingSlash = function(fn) {
  275. return this.env.trailingSlash.withValue(true, fn);
  276. };
  277. Router.prototype._notfoundRoute = function(context) {
  278. this._current = {
  279. path: context.path,
  280. context: context,
  281. params: [],
  282. queryParams: {},
  283. };
  284. // XXX this.notfound kept for backwards compatibility
  285. this.notFound = this.notFound || this.notfound;
  286. if(!this.notFound) {
  287. console.error("There is no route for the path:", context.path);
  288. return;
  289. }
  290. this._current.route = new Route(this, "*", this.notFound);
  291. this._invalidateTracker();
  292. };
  293. Router.prototype.initialize = function(options) {
  294. options = options || {};
  295. if(this._initialized) {
  296. throw new Error("FlowRouter is already initialized");
  297. }
  298. var self = this;
  299. this._updateCallbacks();
  300. // Implementing idempotent routing
  301. // by overriding page.js`s "show" method.
  302. // Why?
  303. // It is impossible to bypass exit triggers,
  304. // because they execute before the handler and
  305. // can not know what the next path is, inside exit trigger.
  306. //
  307. // we need override both show, replace to make this work
  308. // since we use redirect when we are talking about withReplaceState
  309. _.each(['show', 'replace'], function(fnName) {
  310. var original = self._page[fnName];
  311. self._page[fnName] = function(path, state, dispatch, push) {
  312. var reload = self.env.reload.get();
  313. if (!reload && self._current.path === path) {
  314. return;
  315. }
  316. original.call(this, path, state, dispatch, push);
  317. };
  318. });
  319. // this is very ugly part of pagejs and it does decoding few times
  320. // in unpredicatable manner. See #168
  321. // this is the default behaviour and we need keep it like that
  322. // we are doing a hack. see .path()
  323. this._page.base(this._basePath);
  324. this._page({
  325. decodeURLComponents: true,
  326. hashbang: !!options.hashbang
  327. });
  328. this._initialized = true;
  329. };
  330. Router.prototype._buildTracker = function() {
  331. var self = this;
  332. // main autorun function
  333. var tracker = Tracker.autorun(function () {
  334. if(!self._current || !self._current.route) {
  335. return;
  336. }
  337. // see the definition of `this._processingContexts`
  338. var currentContext = self._current;
  339. var route = currentContext.route;
  340. var path = currentContext.path;
  341. if(self.safeToRun === 0) {
  342. var message =
  343. "You can't use reactive data sources like Session" +
  344. " inside the `.subscriptions` method!";
  345. throw new Error(message);
  346. }
  347. // We need to run subscriptions inside a Tracker
  348. // to stop subs when switching between routes
  349. // But we don't need to run this tracker with
  350. // other reactive changes inside the .subscription method
  351. // We tackle this with the `safeToRun` variable
  352. self._globalRoute.clearSubscriptions();
  353. self.subscriptions.call(self._globalRoute, path);
  354. route.callSubscriptions(currentContext);
  355. // otherwise, computations inside action will trigger to re-run
  356. // this computation. which we do not need.
  357. Tracker.nonreactive(function() {
  358. var isRouteChange = currentContext.oldRoute !== currentContext.route;
  359. var isFirstRoute = !currentContext.oldRoute;
  360. // first route is not a route change
  361. if(isFirstRoute) {
  362. isRouteChange = false;
  363. }
  364. // Clear oldRouteChain just before calling the action
  365. // We still need to get a copy of the oldestRoute first
  366. // It's very important to get the oldest route and registerRouteClose() it
  367. // See: https://github.com/kadirahq/flow-router/issues/314
  368. var oldestRoute = self._oldRouteChain[0];
  369. self._oldRouteChain = [];
  370. currentContext.route.registerRouteChange(currentContext, isRouteChange);
  371. route.callAction(currentContext);
  372. Tracker.afterFlush(function() {
  373. self._onEveryPath.changed();
  374. if(isRouteChange) {
  375. // We need to trigger that route (definition itself) has changed.
  376. // So, we need to re-run all the register callbacks to current route
  377. // This is pretty important, otherwise tracker
  378. // can't identify new route's items
  379. // We also need to afterFlush, otherwise this will re-run
  380. // helpers on templates which are marked for destroying
  381. if(oldestRoute) {
  382. oldestRoute.registerRouteClose();
  383. }
  384. }
  385. });
  386. });
  387. self.safeToRun--;
  388. });
  389. return tracker;
  390. };
  391. Router.prototype._invalidateTracker = function() {
  392. var self = this;
  393. this.safeToRun++;
  394. this._tracker.invalidate();
  395. // After the invalidation we need to flush to make changes imediately
  396. // otherwise, we have face some issues context mix-maches and so on.
  397. // But there are some cases we can't flush. So we need to ready for that.
  398. // we clearly know, we can't flush inside an autorun
  399. // this may leads some issues on flow-routing
  400. // we may need to do some warning
  401. if(!Tracker.currentComputation) {
  402. // Still there are some cases where we can't flush
  403. // eg:- when there is a flush currently
  404. // But we've no public API or hacks to get that state
  405. // So, this is the only solution
  406. try {
  407. Tracker.flush();
  408. } catch(ex) {
  409. // only handling "while flushing" errors
  410. if(!/Tracker\.flush while flushing/.test(ex.message)) {
  411. return;
  412. }
  413. // XXX: fix this with a proper solution by removing subscription mgt.
  414. // from the router. Then we don't need to run invalidate using a tracker
  415. // this happens when we are trying to invoke a route change
  416. // with inside a route chnage. (eg:- Template.onCreated)
  417. // Since we use page.js and tracker, we don't have much control
  418. // over this process.
  419. // only solution is to defer route execution.
  420. // It's possible to have more than one path want to defer
  421. // But, we only need to pick the last one.
  422. // self._nextPath = self._current.path;
  423. Meteor.defer(function() {
  424. var path = self._nextPath;
  425. if(!path) {
  426. return;
  427. }
  428. delete self._nextPath;
  429. self.env.reload.withValue(true, function() {
  430. self.go(path);
  431. });
  432. });
  433. }
  434. }
  435. };
  436. Router.prototype._updateCallbacks = function () {
  437. var self = this;
  438. self._page.callbacks = [];
  439. self._page.exits = [];
  440. _.each(self._routes, function(route) {
  441. self._page(route.pathDef, route._actionHandle);
  442. self._page.exit(route.pathDef, route._exitHandle);
  443. });
  444. self._page("*", function(context) {
  445. self._notfoundRoute(context);
  446. });
  447. };
  448. Router.prototype._initTriggersAPI = function() {
  449. var self = this;
  450. this.triggers = {
  451. enter: function(triggers, filter) {
  452. triggers = Triggers.applyFilters(triggers, filter);
  453. if(triggers.length) {
  454. self._triggersEnter = self._triggersEnter.concat(triggers);
  455. }
  456. },
  457. exit: function(triggers, filter) {
  458. triggers = Triggers.applyFilters(triggers, filter);
  459. if(triggers.length) {
  460. self._triggersExit = self._triggersExit.concat(triggers);
  461. }
  462. }
  463. };
  464. };
  465. Router.prototype.wait = function() {
  466. if(this._initialized) {
  467. throw new Error("can't wait after FlowRouter has been initialized");
  468. }
  469. this._askedToWait = true;
  470. };
  471. Router.prototype.onRouteRegister = function(cb) {
  472. this._onRouteCallbacks.push(cb);
  473. };
  474. Router.prototype._triggerRouteRegister = function(currentRoute) {
  475. // We should only need to send a safe set of fields on the route
  476. // object.
  477. // This is not to hide what's inside the route object, but to show
  478. // these are the public APIs
  479. var routePublicApi = _.pick(currentRoute, 'name', 'pathDef', 'path');
  480. var omittingOptionFields = [
  481. 'triggersEnter', 'triggersExit', 'action', 'subscriptions', 'name'
  482. ];
  483. routePublicApi.options = _.omit(currentRoute.options, omittingOptionFields);
  484. _.each(this._onRouteCallbacks, function(cb) {
  485. cb(routePublicApi);
  486. });
  487. };
  488. Router.prototype._page = page;
  489. Router.prototype._qs = qs;