urlRouter.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /**
  2. * @ngdoc object
  3. * @name ui.router.router.$urlRouterProvider
  4. *
  5. * @requires ui.router.util.$urlMatcherFactoryProvider
  6. * @requires $locationProvider
  7. *
  8. * @description
  9. * `$urlRouterProvider` has the responsibility of watching `$location`.
  10. * When `$location` changes it runs through a list of rules one by one until a
  11. * match is found. `$urlRouterProvider` is used behind the scenes anytime you specify
  12. * a url in a state configuration. All urls are compiled into a UrlMatcher object.
  13. *
  14. * There are several methods on `$urlRouterProvider` that make it useful to use directly
  15. * in your module config.
  16. */
  17. $UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider'];
  18. function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
  19. var rules = [], otherwise = null, interceptDeferred = false, listener;
  20. // Returns a string that is a prefix of all strings matching the RegExp
  21. function regExpPrefix(re) {
  22. var prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source);
  23. return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : '';
  24. }
  25. // Interpolates matched values into a String.replace()-style pattern
  26. function interpolate(pattern, match) {
  27. return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) {
  28. return match[what === '$' ? 0 : Number(what)];
  29. });
  30. }
  31. /**
  32. * @ngdoc function
  33. * @name ui.router.router.$urlRouterProvider#rule
  34. * @methodOf ui.router.router.$urlRouterProvider
  35. *
  36. * @description
  37. * Defines rules that are used by `$urlRouterProvider` to find matches for
  38. * specific URLs.
  39. *
  40. * @example
  41. * <pre>
  42. * var app = angular.module('app', ['ui.router.router']);
  43. *
  44. * app.config(function ($urlRouterProvider) {
  45. * // Here's an example of how you might allow case insensitive urls
  46. * $urlRouterProvider.rule(function ($injector, $location) {
  47. * var path = $location.path(),
  48. * normalized = path.toLowerCase();
  49. *
  50. * if (path !== normalized) {
  51. * return normalized;
  52. * }
  53. * });
  54. * });
  55. * </pre>
  56. *
  57. * @param {object} rule Handler function that takes `$injector` and `$location`
  58. * services as arguments. You can use them to return a valid path as a string.
  59. *
  60. * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
  61. */
  62. this.rule = function (rule) {
  63. if (!isFunction(rule)) throw new Error("'rule' must be a function");
  64. rules.push(rule);
  65. return this;
  66. };
  67. /**
  68. * @ngdoc object
  69. * @name ui.router.router.$urlRouterProvider#otherwise
  70. * @methodOf ui.router.router.$urlRouterProvider
  71. *
  72. * @description
  73. * Defines a path that is used when an invalid route is requested.
  74. *
  75. * @example
  76. * <pre>
  77. * var app = angular.module('app', ['ui.router.router']);
  78. *
  79. * app.config(function ($urlRouterProvider) {
  80. * // if the path doesn't match any of the urls you configured
  81. * // otherwise will take care of routing the user to the
  82. * // specified url
  83. * $urlRouterProvider.otherwise('/index');
  84. *
  85. * // Example of using function rule as param
  86. * $urlRouterProvider.otherwise(function ($injector, $location) {
  87. * return '/a/valid/url';
  88. * });
  89. * });
  90. * </pre>
  91. *
  92. * @param {string|object} rule The url path you want to redirect to or a function
  93. * rule that returns the url path. The function version is passed two params:
  94. * `$injector` and `$location` services, and must return a url string.
  95. *
  96. * @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
  97. */
  98. this.otherwise = function (rule) {
  99. if (isString(rule)) {
  100. var redirect = rule;
  101. rule = function () { return redirect; };
  102. }
  103. else if (!isFunction(rule)) throw new Error("'rule' must be a function");
  104. otherwise = rule;
  105. return this;
  106. };
  107. function handleIfMatch($injector, handler, match) {
  108. if (!match) return false;
  109. var result = $injector.invoke(handler, handler, { $match: match });
  110. return isDefined(result) ? result : true;
  111. }
  112. /**
  113. * @ngdoc function
  114. * @name ui.router.router.$urlRouterProvider#when
  115. * @methodOf ui.router.router.$urlRouterProvider
  116. *
  117. * @description
  118. * Registers a handler for a given url matching. if handle is a string, it is
  119. * treated as a redirect, and is interpolated according to the syntax of match
  120. * (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise).
  121. *
  122. * If the handler is a function, it is injectable. It gets invoked if `$location`
  123. * matches. You have the option of inject the match object as `$match`.
  124. *
  125. * The handler can return
  126. *
  127. * - **falsy** to indicate that the rule didn't match after all, then `$urlRouter`
  128. * will continue trying to find another one that matches.
  129. * - **string** which is treated as a redirect and passed to `$location.url()`
  130. * - **void** or any **truthy** value tells `$urlRouter` that the url was handled.
  131. *
  132. * @example
  133. * <pre>
  134. * var app = angular.module('app', ['ui.router.router']);
  135. *
  136. * app.config(function ($urlRouterProvider) {
  137. * $urlRouterProvider.when($state.url, function ($match, $stateParams) {
  138. * if ($state.$current.navigable !== state ||
  139. * !equalForKeys($match, $stateParams) {
  140. * $state.transitionTo(state, $match, false);
  141. * }
  142. * });
  143. * });
  144. * </pre>
  145. *
  146. * @param {string|object} what The incoming path that you want to redirect.
  147. * @param {string|object} handler The path you want to redirect your user to.
  148. */
  149. this.when = function (what, handler) {
  150. var redirect, handlerIsString = isString(handler);
  151. if (isString(what)) what = $urlMatcherFactory.compile(what);
  152. if (!handlerIsString && !isFunction(handler) && !isArray(handler))
  153. throw new Error("invalid 'handler' in when()");
  154. var strategies = {
  155. matcher: function (what, handler) {
  156. if (handlerIsString) {
  157. redirect = $urlMatcherFactory.compile(handler);
  158. handler = ['$match', function ($match) { return redirect.format($match); }];
  159. }
  160. return extend(function ($injector, $location) {
  161. return handleIfMatch($injector, handler, what.exec($location.path(), $location.search()));
  162. }, {
  163. prefix: isString(what.prefix) ? what.prefix : ''
  164. });
  165. },
  166. regex: function (what, handler) {
  167. if (what.global || what.sticky) throw new Error("when() RegExp must not be global or sticky");
  168. if (handlerIsString) {
  169. redirect = handler;
  170. handler = ['$match', function ($match) { return interpolate(redirect, $match); }];
  171. }
  172. return extend(function ($injector, $location) {
  173. return handleIfMatch($injector, handler, what.exec($location.path()));
  174. }, {
  175. prefix: regExpPrefix(what)
  176. });
  177. }
  178. };
  179. var check = { matcher: $urlMatcherFactory.isMatcher(what), regex: what instanceof RegExp };
  180. for (var n in check) {
  181. if (check[n]) return this.rule(strategies[n](what, handler));
  182. }
  183. throw new Error("invalid 'what' in when()");
  184. };
  185. /**
  186. * @ngdoc function
  187. * @name ui.router.router.$urlRouterProvider#deferIntercept
  188. * @methodOf ui.router.router.$urlRouterProvider
  189. *
  190. * @description
  191. * Disables (or enables) deferring location change interception.
  192. *
  193. * If you wish to customize the behavior of syncing the URL (for example, if you wish to
  194. * defer a transition but maintain the current URL), call this method at configuration time.
  195. * Then, at run time, call `$urlRouter.listen()` after you have configured your own
  196. * `$locationChangeSuccess` event handler.
  197. *
  198. * @example
  199. * <pre>
  200. * var app = angular.module('app', ['ui.router.router']);
  201. *
  202. * app.config(function ($urlRouterProvider) {
  203. *
  204. * // Prevent $urlRouter from automatically intercepting URL changes;
  205. * // this allows you to configure custom behavior in between
  206. * // location changes and route synchronization:
  207. * $urlRouterProvider.deferIntercept();
  208. *
  209. * }).run(function ($rootScope, $urlRouter, UserService) {
  210. *
  211. * $rootScope.$on('$locationChangeSuccess', function(e) {
  212. * // UserService is an example service for managing user state
  213. * if (UserService.isLoggedIn()) return;
  214. *
  215. * // Prevent $urlRouter's default handler from firing
  216. * e.preventDefault();
  217. *
  218. * UserService.handleLogin().then(function() {
  219. * // Once the user has logged in, sync the current URL
  220. * // to the router:
  221. * $urlRouter.sync();
  222. * });
  223. * });
  224. *
  225. * // Configures $urlRouter's listener *after* your custom listener
  226. * $urlRouter.listen();
  227. * });
  228. * </pre>
  229. *
  230. * @param {boolean} defer Indicates whether to defer location change interception. Passing
  231. no parameter is equivalent to `true`.
  232. */
  233. this.deferIntercept = function (defer) {
  234. if (defer === undefined) defer = true;
  235. interceptDeferred = defer;
  236. };
  237. /**
  238. * @ngdoc object
  239. * @name ui.router.router.$urlRouter
  240. *
  241. * @requires $location
  242. * @requires $rootScope
  243. * @requires $injector
  244. * @requires $browser
  245. *
  246. * @description
  247. *
  248. */
  249. this.$get = $get;
  250. $get.$inject = ['$location', '$rootScope', '$injector', '$browser'];
  251. function $get( $location, $rootScope, $injector, $browser) {
  252. var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl;
  253. function appendBasePath(url, isHtml5, absolute) {
  254. if (baseHref === '/') return url;
  255. if (isHtml5) return baseHref.slice(0, -1) + url;
  256. if (absolute) return baseHref.slice(1) + url;
  257. return url;
  258. }
  259. // TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
  260. function update(evt) {
  261. if (evt && evt.defaultPrevented) return;
  262. var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl;
  263. lastPushedUrl = undefined;
  264. if (ignoreUpdate) return true;
  265. function check(rule) {
  266. var handled = rule($injector, $location);
  267. if (!handled) return false;
  268. if (isString(handled)) $location.replace().url(handled);
  269. return true;
  270. }
  271. var n = rules.length, i;
  272. for (i = 0; i < n; i++) {
  273. if (check(rules[i])) return;
  274. }
  275. // always check otherwise last to allow dynamic updates to the set of rules
  276. if (otherwise) check(otherwise);
  277. }
  278. function listen() {
  279. listener = listener || $rootScope.$on('$locationChangeSuccess', update);
  280. return listener;
  281. }
  282. if (!interceptDeferred) listen();
  283. return {
  284. /**
  285. * @ngdoc function
  286. * @name ui.router.router.$urlRouter#sync
  287. * @methodOf ui.router.router.$urlRouter
  288. *
  289. * @description
  290. * Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`.
  291. * This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event,
  292. * perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed
  293. * with the transition by calling `$urlRouter.sync()`.
  294. *
  295. * @example
  296. * <pre>
  297. * angular.module('app', ['ui.router'])
  298. * .run(function($rootScope, $urlRouter) {
  299. * $rootScope.$on('$locationChangeSuccess', function(evt) {
  300. * // Halt state change from even starting
  301. * evt.preventDefault();
  302. * // Perform custom logic
  303. * var meetsRequirement = ...
  304. * // Continue with the update and state transition if logic allows
  305. * if (meetsRequirement) $urlRouter.sync();
  306. * });
  307. * });
  308. * </pre>
  309. */
  310. sync: function() {
  311. update();
  312. },
  313. listen: function() {
  314. return listen();
  315. },
  316. update: function(read) {
  317. if (read) {
  318. location = $location.url();
  319. return;
  320. }
  321. if ($location.url() === location) return;
  322. $location.url(location);
  323. $location.replace();
  324. },
  325. push: function(urlMatcher, params, options) {
  326. $location.url(urlMatcher.format(params || {}));
  327. lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined;
  328. if (options && options.replace) $location.replace();
  329. },
  330. /**
  331. * @ngdoc function
  332. * @name ui.router.router.$urlRouter#href
  333. * @methodOf ui.router.router.$urlRouter
  334. *
  335. * @description
  336. * A URL generation method that returns the compiled URL for a given
  337. * {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters.
  338. *
  339. * @example
  340. * <pre>
  341. * $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
  342. * person: "bob"
  343. * });
  344. * // $bob == "/about/bob";
  345. * </pre>
  346. *
  347. * @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate.
  348. * @param {object=} params An object of parameter values to fill the matcher's required parameters.
  349. * @param {object=} options Options object. The options are:
  350. *
  351. * - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl".
  352. *
  353. * @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher`
  354. */
  355. href: function(urlMatcher, params, options) {
  356. if (!urlMatcher.validates(params)) return null;
  357. var isHtml5 = $locationProvider.html5Mode();
  358. if (angular.isObject(isHtml5)) {
  359. isHtml5 = isHtml5.enabled;
  360. }
  361. var url = urlMatcher.format(params);
  362. options = options || {};
  363. if (!isHtml5 && url !== null) {
  364. url = "#" + $locationProvider.hashPrefix() + url;
  365. }
  366. url = appendBasePath(url, isHtml5, options.absolute);
  367. if (!options.absolute || !url) {
  368. return url;
  369. }
  370. var slash = (!isHtml5 && url ? '/' : ''), port = $location.port();
  371. port = (port === 80 || port === 443 ? '' : ':' + port);
  372. return [$location.protocol(), '://', $location.host(), port, slash, url].join('');
  373. }
  374. };
  375. }
  376. }
  377. angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider);