Simon Hunt | 07adc48 | 2014-12-12 16:21:08 -0800 | [diff] [blame] | 1 | /** |
| 2 | * @license AngularJS v1.3.5 |
| 3 | * (c) 2010-2014 Google, Inc. http://angularjs.org |
| 4 | * License: MIT |
| 5 | */ |
| 6 | (function(window, angular, undefined) {'use strict'; |
| 7 | |
| 8 | /** |
| 9 | * @ngdoc module |
| 10 | * @name ngRoute |
| 11 | * @description |
| 12 | * |
| 13 | * # ngRoute |
| 14 | * |
| 15 | * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. |
| 16 | * |
| 17 | * ## Example |
| 18 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. |
| 19 | * |
| 20 | * |
| 21 | * <div doc-module-components="ngRoute"></div> |
| 22 | */ |
| 23 | /* global -ngRouteModule */ |
| 24 | var ngRouteModule = angular.module('ngRoute', ['ng']). |
| 25 | provider('$route', $RouteProvider), |
| 26 | $routeMinErr = angular.$$minErr('ngRoute'); |
| 27 | |
| 28 | /** |
| 29 | * @ngdoc provider |
| 30 | * @name $routeProvider |
| 31 | * |
| 32 | * @description |
| 33 | * |
| 34 | * Used for configuring routes. |
| 35 | * |
| 36 | * ## Example |
| 37 | * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. |
| 38 | * |
| 39 | * ## Dependencies |
| 40 | * Requires the {@link ngRoute `ngRoute`} module to be installed. |
| 41 | */ |
| 42 | function $RouteProvider() { |
| 43 | function inherit(parent, extra) { |
| 44 | return angular.extend(Object.create(parent), extra); |
| 45 | } |
| 46 | |
| 47 | var routes = {}; |
| 48 | |
| 49 | /** |
| 50 | * @ngdoc method |
| 51 | * @name $routeProvider#when |
| 52 | * |
| 53 | * @param {string} path Route path (matched against `$location.path`). If `$location.path` |
| 54 | * contains redundant trailing slash or is missing one, the route will still match and the |
| 55 | * `$location.path` will be updated to add or drop the trailing slash to exactly match the |
| 56 | * route definition. |
| 57 | * |
| 58 | * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up |
| 59 | * to the next slash are matched and stored in `$routeParams` under the given `name` |
| 60 | * when the route matches. |
| 61 | * * `path` can contain named groups starting with a colon and ending with a star: |
| 62 | * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` |
| 63 | * when the route matches. |
| 64 | * * `path` can contain optional named groups with a question mark: e.g.`:name?`. |
| 65 | * |
| 66 | * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match |
| 67 | * `/color/brown/largecode/code/with/slashes/edit` and extract: |
| 68 | * |
| 69 | * * `color: brown` |
| 70 | * * `largecode: code/with/slashes`. |
| 71 | * |
| 72 | * |
| 73 | * @param {Object} route Mapping information to be assigned to `$route.current` on route |
| 74 | * match. |
| 75 | * |
| 76 | * Object properties: |
| 77 | * |
| 78 | * - `controller` – `{(string|function()=}` – Controller fn that should be associated with |
| 79 | * newly created scope or the name of a {@link angular.Module#controller registered |
| 80 | * controller} if passed as a string. |
| 81 | * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be |
| 82 | * published to scope under the `controllerAs` name. |
| 83 | * - `template` – `{string=|function()=}` – html template as a string or a function that |
| 84 | * returns an html template as a string which should be used by {@link |
| 85 | * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. |
| 86 | * This property takes precedence over `templateUrl`. |
| 87 | * |
| 88 | * If `template` is a function, it will be called with the following parameters: |
| 89 | * |
| 90 | * - `{Array.<Object>}` - route parameters extracted from the current |
| 91 | * `$location.path()` by applying the current route |
| 92 | * |
| 93 | * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html |
| 94 | * template that should be used by {@link ngRoute.directive:ngView ngView}. |
| 95 | * |
| 96 | * If `templateUrl` is a function, it will be called with the following parameters: |
| 97 | * |
| 98 | * - `{Array.<Object>}` - route parameters extracted from the current |
| 99 | * `$location.path()` by applying the current route |
| 100 | * |
| 101 | * - `resolve` - `{Object.<string, function>=}` - An optional map of dependencies which should |
| 102 | * be injected into the controller. If any of these dependencies are promises, the router |
| 103 | * will wait for them all to be resolved or one to be rejected before the controller is |
| 104 | * instantiated. |
| 105 | * If all the promises are resolved successfully, the values of the resolved promises are |
| 106 | * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is |
| 107 | * fired. If any of the promises are rejected the |
| 108 | * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object |
| 109 | * is: |
| 110 | * |
| 111 | * - `key` – `{string}`: a name of a dependency to be injected into the controller. |
| 112 | * - `factory` - `{string|function}`: If `string` then it is an alias for a service. |
| 113 | * Otherwise if function, then it is {@link auto.$injector#invoke injected} |
| 114 | * and the return value is treated as the dependency. If the result is a promise, it is |
| 115 | * resolved before its value is injected into the controller. Be aware that |
| 116 | * `ngRoute.$routeParams` will still refer to the previous route within these resolve |
| 117 | * functions. Use `$route.current.params` to access the new route parameters, instead. |
| 118 | * |
| 119 | * - `redirectTo` – {(string|function())=} – value to update |
| 120 | * {@link ng.$location $location} path with and trigger route redirection. |
| 121 | * |
| 122 | * If `redirectTo` is a function, it will be called with the following parameters: |
| 123 | * |
| 124 | * - `{Object.<string>}` - route parameters extracted from the current |
| 125 | * `$location.path()` by applying the current route templateUrl. |
| 126 | * - `{string}` - current `$location.path()` |
| 127 | * - `{Object}` - current `$location.search()` |
| 128 | * |
| 129 | * The custom `redirectTo` function is expected to return a string which will be used |
| 130 | * to update `$location.path()` and `$location.search()`. |
| 131 | * |
| 132 | * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` |
| 133 | * or `$location.hash()` changes. |
| 134 | * |
| 135 | * If the option is set to `false` and url in the browser changes, then |
| 136 | * `$routeUpdate` event is broadcasted on the root scope. |
| 137 | * |
| 138 | * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive |
| 139 | * |
| 140 | * If the option is set to `true`, then the particular route can be matched without being |
| 141 | * case sensitive |
| 142 | * |
| 143 | * @returns {Object} self |
| 144 | * |
| 145 | * @description |
| 146 | * Adds a new route definition to the `$route` service. |
| 147 | */ |
| 148 | this.when = function(path, route) { |
| 149 | //copy original route object to preserve params inherited from proto chain |
| 150 | var routeCopy = angular.copy(route); |
| 151 | if (angular.isUndefined(routeCopy.reloadOnSearch)) { |
| 152 | routeCopy.reloadOnSearch = true; |
| 153 | } |
| 154 | if (angular.isUndefined(routeCopy.caseInsensitiveMatch)) { |
| 155 | routeCopy.caseInsensitiveMatch = this.caseInsensitiveMatch; |
| 156 | } |
| 157 | routes[path] = angular.extend( |
| 158 | routeCopy, |
| 159 | path && pathRegExp(path, routeCopy) |
| 160 | ); |
| 161 | |
| 162 | // create redirection for trailing slashes |
| 163 | if (path) { |
| 164 | var redirectPath = (path[path.length - 1] == '/') |
| 165 | ? path.substr(0, path.length - 1) |
| 166 | : path + '/'; |
| 167 | |
| 168 | routes[redirectPath] = angular.extend( |
| 169 | {redirectTo: path}, |
| 170 | pathRegExp(redirectPath, routeCopy) |
| 171 | ); |
| 172 | } |
| 173 | |
| 174 | return this; |
| 175 | }; |
| 176 | |
| 177 | /** |
| 178 | * @ngdoc property |
| 179 | * @name $routeProvider#caseInsensitiveMatch |
| 180 | * @description |
| 181 | * |
| 182 | * A boolean property indicating if routes defined |
| 183 | * using this provider should be matched using a case insensitive |
| 184 | * algorithm. Defaults to `false`. |
| 185 | */ |
| 186 | this.caseInsensitiveMatch = false; |
| 187 | |
| 188 | /** |
| 189 | * @param path {string} path |
| 190 | * @param opts {Object} options |
| 191 | * @return {?Object} |
| 192 | * |
| 193 | * @description |
| 194 | * Normalizes the given path, returning a regular expression |
| 195 | * and the original path. |
| 196 | * |
| 197 | * Inspired by pathRexp in visionmedia/express/lib/utils.js. |
| 198 | */ |
| 199 | function pathRegExp(path, opts) { |
| 200 | var insensitive = opts.caseInsensitiveMatch, |
| 201 | ret = { |
| 202 | originalPath: path, |
| 203 | regexp: path |
| 204 | }, |
| 205 | keys = ret.keys = []; |
| 206 | |
| 207 | path = path |
| 208 | .replace(/([().])/g, '\\$1') |
| 209 | .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option) { |
| 210 | var optional = option === '?' ? option : null; |
| 211 | var star = option === '*' ? option : null; |
| 212 | keys.push({ name: key, optional: !!optional }); |
| 213 | slash = slash || ''; |
| 214 | return '' |
| 215 | + (optional ? '' : slash) |
| 216 | + '(?:' |
| 217 | + (optional ? slash : '') |
| 218 | + (star && '(.+?)' || '([^/]+)') |
| 219 | + (optional || '') |
| 220 | + ')' |
| 221 | + (optional || ''); |
| 222 | }) |
| 223 | .replace(/([\/$\*])/g, '\\$1'); |
| 224 | |
| 225 | ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); |
| 226 | return ret; |
| 227 | } |
| 228 | |
| 229 | /** |
| 230 | * @ngdoc method |
| 231 | * @name $routeProvider#otherwise |
| 232 | * |
| 233 | * @description |
| 234 | * Sets route definition that will be used on route change when no other route definition |
| 235 | * is matched. |
| 236 | * |
| 237 | * @param {Object|string} params Mapping information to be assigned to `$route.current`. |
| 238 | * If called with a string, the value maps to `redirectTo`. |
| 239 | * @returns {Object} self |
| 240 | */ |
| 241 | this.otherwise = function(params) { |
| 242 | if (typeof params === 'string') { |
| 243 | params = {redirectTo: params}; |
| 244 | } |
| 245 | this.when(null, params); |
| 246 | return this; |
| 247 | }; |
| 248 | |
| 249 | |
| 250 | this.$get = ['$rootScope', |
| 251 | '$location', |
| 252 | '$routeParams', |
| 253 | '$q', |
| 254 | '$injector', |
| 255 | '$templateRequest', |
| 256 | '$sce', |
| 257 | function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { |
| 258 | |
| 259 | /** |
| 260 | * @ngdoc service |
| 261 | * @name $route |
| 262 | * @requires $location |
| 263 | * @requires $routeParams |
| 264 | * |
| 265 | * @property {Object} current Reference to the current route definition. |
| 266 | * The route definition contains: |
| 267 | * |
| 268 | * - `controller`: The controller constructor as define in route definition. |
| 269 | * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for |
| 270 | * controller instantiation. The `locals` contain |
| 271 | * the resolved values of the `resolve` map. Additionally the `locals` also contain: |
| 272 | * |
| 273 | * - `$scope` - The current route scope. |
| 274 | * - `$template` - The current route template HTML. |
| 275 | * |
| 276 | * @property {Object} routes Object with all route configuration Objects as its properties. |
| 277 | * |
| 278 | * @description |
| 279 | * `$route` is used for deep-linking URLs to controllers and views (HTML partials). |
| 280 | * It watches `$location.url()` and tries to map the path to an existing route definition. |
| 281 | * |
| 282 | * Requires the {@link ngRoute `ngRoute`} module to be installed. |
| 283 | * |
| 284 | * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. |
| 285 | * |
| 286 | * The `$route` service is typically used in conjunction with the |
| 287 | * {@link ngRoute.directive:ngView `ngView`} directive and the |
| 288 | * {@link ngRoute.$routeParams `$routeParams`} service. |
| 289 | * |
| 290 | * @example |
| 291 | * This example shows how changing the URL hash causes the `$route` to match a route against the |
| 292 | * URL, and the `ngView` pulls in the partial. |
| 293 | * |
| 294 | * <example name="$route-service" module="ngRouteExample" |
| 295 | * deps="angular-route.js" fixBase="true"> |
| 296 | * <file name="index.html"> |
| 297 | * <div ng-controller="MainController"> |
| 298 | * Choose: |
| 299 | * <a href="Book/Moby">Moby</a> | |
| 300 | * <a href="Book/Moby/ch/1">Moby: Ch1</a> | |
| 301 | * <a href="Book/Gatsby">Gatsby</a> | |
| 302 | * <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | |
| 303 | * <a href="Book/Scarlet">Scarlet Letter</a><br/> |
| 304 | * |
| 305 | * <div ng-view></div> |
| 306 | * |
| 307 | * <hr /> |
| 308 | * |
| 309 | * <pre>$location.path() = {{$location.path()}}</pre> |
| 310 | * <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre> |
| 311 | * <pre>$route.current.params = {{$route.current.params}}</pre> |
| 312 | * <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> |
| 313 | * <pre>$routeParams = {{$routeParams}}</pre> |
| 314 | * </div> |
| 315 | * </file> |
| 316 | * |
| 317 | * <file name="book.html"> |
| 318 | * controller: {{name}}<br /> |
| 319 | * Book Id: {{params.bookId}}<br /> |
| 320 | * </file> |
| 321 | * |
| 322 | * <file name="chapter.html"> |
| 323 | * controller: {{name}}<br /> |
| 324 | * Book Id: {{params.bookId}}<br /> |
| 325 | * Chapter Id: {{params.chapterId}} |
| 326 | * </file> |
| 327 | * |
| 328 | * <file name="script.js"> |
| 329 | * angular.module('ngRouteExample', ['ngRoute']) |
| 330 | * |
| 331 | * .controller('MainController', function($scope, $route, $routeParams, $location) { |
| 332 | * $scope.$route = $route; |
| 333 | * $scope.$location = $location; |
| 334 | * $scope.$routeParams = $routeParams; |
| 335 | * }) |
| 336 | * |
| 337 | * .controller('BookController', function($scope, $routeParams) { |
| 338 | * $scope.name = "BookController"; |
| 339 | * $scope.params = $routeParams; |
| 340 | * }) |
| 341 | * |
| 342 | * .controller('ChapterController', function($scope, $routeParams) { |
| 343 | * $scope.name = "ChapterController"; |
| 344 | * $scope.params = $routeParams; |
| 345 | * }) |
| 346 | * |
| 347 | * .config(function($routeProvider, $locationProvider) { |
| 348 | * $routeProvider |
| 349 | * .when('/Book/:bookId', { |
| 350 | * templateUrl: 'book.html', |
| 351 | * controller: 'BookController', |
| 352 | * resolve: { |
| 353 | * // I will cause a 1 second delay |
| 354 | * delay: function($q, $timeout) { |
| 355 | * var delay = $q.defer(); |
| 356 | * $timeout(delay.resolve, 1000); |
| 357 | * return delay.promise; |
| 358 | * } |
| 359 | * } |
| 360 | * }) |
| 361 | * .when('/Book/:bookId/ch/:chapterId', { |
| 362 | * templateUrl: 'chapter.html', |
| 363 | * controller: 'ChapterController' |
| 364 | * }); |
| 365 | * |
| 366 | * // configure html5 to get links working on jsfiddle |
| 367 | * $locationProvider.html5Mode(true); |
| 368 | * }); |
| 369 | * |
| 370 | * </file> |
| 371 | * |
| 372 | * <file name="protractor.js" type="protractor"> |
| 373 | * it('should load and compile correct template', function() { |
| 374 | * element(by.linkText('Moby: Ch1')).click(); |
| 375 | * var content = element(by.css('[ng-view]')).getText(); |
| 376 | * expect(content).toMatch(/controller\: ChapterController/); |
| 377 | * expect(content).toMatch(/Book Id\: Moby/); |
| 378 | * expect(content).toMatch(/Chapter Id\: 1/); |
| 379 | * |
| 380 | * element(by.partialLinkText('Scarlet')).click(); |
| 381 | * |
| 382 | * content = element(by.css('[ng-view]')).getText(); |
| 383 | * expect(content).toMatch(/controller\: BookController/); |
| 384 | * expect(content).toMatch(/Book Id\: Scarlet/); |
| 385 | * }); |
| 386 | * </file> |
| 387 | * </example> |
| 388 | */ |
| 389 | |
| 390 | /** |
| 391 | * @ngdoc event |
| 392 | * @name $route#$routeChangeStart |
| 393 | * @eventType broadcast on root scope |
| 394 | * @description |
| 395 | * Broadcasted before a route change. At this point the route services starts |
| 396 | * resolving all of the dependencies needed for the route change to occur. |
| 397 | * Typically this involves fetching the view template as well as any dependencies |
| 398 | * defined in `resolve` route property. Once all of the dependencies are resolved |
| 399 | * `$routeChangeSuccess` is fired. |
| 400 | * |
| 401 | * The route change (and the `$location` change that triggered it) can be prevented |
| 402 | * by calling `preventDefault` method of the event. See {@link ng.$rootScope.Scope#$on} |
| 403 | * for more details about event object. |
| 404 | * |
| 405 | * @param {Object} angularEvent Synthetic event object. |
| 406 | * @param {Route} next Future route information. |
| 407 | * @param {Route} current Current route information. |
| 408 | */ |
| 409 | |
| 410 | /** |
| 411 | * @ngdoc event |
| 412 | * @name $route#$routeChangeSuccess |
| 413 | * @eventType broadcast on root scope |
| 414 | * @description |
| 415 | * Broadcasted after a route dependencies are resolved. |
| 416 | * {@link ngRoute.directive:ngView ngView} listens for the directive |
| 417 | * to instantiate the controller and render the view. |
| 418 | * |
| 419 | * @param {Object} angularEvent Synthetic event object. |
| 420 | * @param {Route} current Current route information. |
| 421 | * @param {Route|Undefined} previous Previous route information, or undefined if current is |
| 422 | * first route entered. |
| 423 | */ |
| 424 | |
| 425 | /** |
| 426 | * @ngdoc event |
| 427 | * @name $route#$routeChangeError |
| 428 | * @eventType broadcast on root scope |
| 429 | * @description |
| 430 | * Broadcasted if any of the resolve promises are rejected. |
| 431 | * |
| 432 | * @param {Object} angularEvent Synthetic event object |
| 433 | * @param {Route} current Current route information. |
| 434 | * @param {Route} previous Previous route information. |
| 435 | * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. |
| 436 | */ |
| 437 | |
| 438 | /** |
| 439 | * @ngdoc event |
| 440 | * @name $route#$routeUpdate |
| 441 | * @eventType broadcast on root scope |
| 442 | * @description |
| 443 | * |
| 444 | * The `reloadOnSearch` property has been set to false, and we are reusing the same |
| 445 | * instance of the Controller. |
| 446 | */ |
| 447 | |
| 448 | var forceReload = false, |
| 449 | preparedRoute, |
| 450 | preparedRouteIsUpdateOnly, |
| 451 | $route = { |
| 452 | routes: routes, |
| 453 | |
| 454 | /** |
| 455 | * @ngdoc method |
| 456 | * @name $route#reload |
| 457 | * |
| 458 | * @description |
| 459 | * Causes `$route` service to reload the current route even if |
| 460 | * {@link ng.$location $location} hasn't changed. |
| 461 | * |
| 462 | * As a result of that, {@link ngRoute.directive:ngView ngView} |
| 463 | * creates new scope and reinstantiates the controller. |
| 464 | */ |
| 465 | reload: function() { |
| 466 | forceReload = true; |
| 467 | $rootScope.$evalAsync(function() { |
| 468 | // Don't support cancellation of a reload for now... |
| 469 | prepareRoute(); |
| 470 | commitRoute(); |
| 471 | }); |
| 472 | }, |
| 473 | |
| 474 | /** |
| 475 | * @ngdoc method |
| 476 | * @name $route#updateParams |
| 477 | * |
| 478 | * @description |
| 479 | * Causes `$route` service to update the current URL, replacing |
| 480 | * current route parameters with those specified in `newParams`. |
| 481 | * Provided property names that match the route's path segment |
| 482 | * definitions will be interpolated into the location's path, while |
| 483 | * remaining properties will be treated as query params. |
| 484 | * |
| 485 | * @param {Object} newParams mapping of URL parameter names to values |
| 486 | */ |
| 487 | updateParams: function(newParams) { |
| 488 | if (this.current && this.current.$$route) { |
| 489 | var searchParams = {}, self=this; |
| 490 | |
| 491 | angular.forEach(Object.keys(newParams), function(key) { |
| 492 | if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; |
| 493 | }); |
| 494 | |
| 495 | newParams = angular.extend({}, this.current.params, newParams); |
| 496 | $location.path(interpolate(this.current.$$route.originalPath, newParams)); |
| 497 | $location.search(angular.extend({}, $location.search(), searchParams)); |
| 498 | } |
| 499 | else { |
| 500 | throw $routeMinErr('norout', 'Tried updating route when with no current route'); |
| 501 | } |
| 502 | } |
| 503 | }; |
| 504 | |
| 505 | $rootScope.$on('$locationChangeStart', prepareRoute); |
| 506 | $rootScope.$on('$locationChangeSuccess', commitRoute); |
| 507 | |
| 508 | return $route; |
| 509 | |
| 510 | ///////////////////////////////////////////////////// |
| 511 | |
| 512 | /** |
| 513 | * @param on {string} current url |
| 514 | * @param route {Object} route regexp to match the url against |
| 515 | * @return {?Object} |
| 516 | * |
| 517 | * @description |
| 518 | * Check if the route matches the current url. |
| 519 | * |
| 520 | * Inspired by match in |
| 521 | * visionmedia/express/lib/router/router.js. |
| 522 | */ |
| 523 | function switchRouteMatcher(on, route) { |
| 524 | var keys = route.keys, |
| 525 | params = {}; |
| 526 | |
| 527 | if (!route.regexp) return null; |
| 528 | |
| 529 | var m = route.regexp.exec(on); |
| 530 | if (!m) return null; |
| 531 | |
| 532 | for (var i = 1, len = m.length; i < len; ++i) { |
| 533 | var key = keys[i - 1]; |
| 534 | |
| 535 | var val = m[i]; |
| 536 | |
| 537 | if (key && val) { |
| 538 | params[key.name] = val; |
| 539 | } |
| 540 | } |
| 541 | return params; |
| 542 | } |
| 543 | |
| 544 | function prepareRoute($locationEvent) { |
| 545 | var lastRoute = $route.current; |
| 546 | |
| 547 | preparedRoute = parseRoute(); |
| 548 | preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route |
| 549 | && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) |
| 550 | && !preparedRoute.reloadOnSearch && !forceReload; |
| 551 | |
| 552 | if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) { |
| 553 | if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) { |
| 554 | if ($locationEvent) { |
| 555 | $locationEvent.preventDefault(); |
| 556 | } |
| 557 | } |
| 558 | } |
| 559 | } |
| 560 | |
| 561 | function commitRoute() { |
| 562 | var lastRoute = $route.current; |
| 563 | var nextRoute = preparedRoute; |
| 564 | |
| 565 | if (preparedRouteIsUpdateOnly) { |
| 566 | lastRoute.params = nextRoute.params; |
| 567 | angular.copy(lastRoute.params, $routeParams); |
| 568 | $rootScope.$broadcast('$routeUpdate', lastRoute); |
| 569 | } else if (nextRoute || lastRoute) { |
| 570 | forceReload = false; |
| 571 | $route.current = nextRoute; |
| 572 | if (nextRoute) { |
| 573 | if (nextRoute.redirectTo) { |
| 574 | if (angular.isString(nextRoute.redirectTo)) { |
| 575 | $location.path(interpolate(nextRoute.redirectTo, nextRoute.params)).search(nextRoute.params) |
| 576 | .replace(); |
| 577 | } else { |
| 578 | $location.url(nextRoute.redirectTo(nextRoute.pathParams, $location.path(), $location.search())) |
| 579 | .replace(); |
| 580 | } |
| 581 | } |
| 582 | } |
| 583 | |
| 584 | $q.when(nextRoute). |
| 585 | then(function() { |
| 586 | if (nextRoute) { |
| 587 | var locals = angular.extend({}, nextRoute.resolve), |
| 588 | template, templateUrl; |
| 589 | |
| 590 | angular.forEach(locals, function(value, key) { |
| 591 | locals[key] = angular.isString(value) ? |
| 592 | $injector.get(value) : $injector.invoke(value, null, null, key); |
| 593 | }); |
| 594 | |
| 595 | if (angular.isDefined(template = nextRoute.template)) { |
| 596 | if (angular.isFunction(template)) { |
| 597 | template = template(nextRoute.params); |
| 598 | } |
| 599 | } else if (angular.isDefined(templateUrl = nextRoute.templateUrl)) { |
| 600 | if (angular.isFunction(templateUrl)) { |
| 601 | templateUrl = templateUrl(nextRoute.params); |
| 602 | } |
| 603 | templateUrl = $sce.getTrustedResourceUrl(templateUrl); |
| 604 | if (angular.isDefined(templateUrl)) { |
| 605 | nextRoute.loadedTemplateUrl = templateUrl; |
| 606 | template = $templateRequest(templateUrl); |
| 607 | } |
| 608 | } |
| 609 | if (angular.isDefined(template)) { |
| 610 | locals['$template'] = template; |
| 611 | } |
| 612 | return $q.all(locals); |
| 613 | } |
| 614 | }). |
| 615 | // after route change |
| 616 | then(function(locals) { |
| 617 | if (nextRoute == $route.current) { |
| 618 | if (nextRoute) { |
| 619 | nextRoute.locals = locals; |
| 620 | angular.copy(nextRoute.params, $routeParams); |
| 621 | } |
| 622 | $rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute); |
| 623 | } |
| 624 | }, function(error) { |
| 625 | if (nextRoute == $route.current) { |
| 626 | $rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error); |
| 627 | } |
| 628 | }); |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | |
| 633 | /** |
| 634 | * @returns {Object} the current active route, by matching it against the URL |
| 635 | */ |
| 636 | function parseRoute() { |
| 637 | // Match a route |
| 638 | var params, match; |
| 639 | angular.forEach(routes, function(route, path) { |
| 640 | if (!match && (params = switchRouteMatcher($location.path(), route))) { |
| 641 | match = inherit(route, { |
| 642 | params: angular.extend({}, $location.search(), params), |
| 643 | pathParams: params}); |
| 644 | match.$$route = route; |
| 645 | } |
| 646 | }); |
| 647 | // No route matched; fallback to "otherwise" route |
| 648 | return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); |
| 649 | } |
| 650 | |
| 651 | /** |
| 652 | * @returns {string} interpolation of the redirect path with the parameters |
| 653 | */ |
| 654 | function interpolate(string, params) { |
| 655 | var result = []; |
| 656 | angular.forEach((string || '').split(':'), function(segment, i) { |
| 657 | if (i === 0) { |
| 658 | result.push(segment); |
| 659 | } else { |
| 660 | var segmentMatch = segment.match(/(\w+)(?:[?*])?(.*)/); |
| 661 | var key = segmentMatch[1]; |
| 662 | result.push(params[key]); |
| 663 | result.push(segmentMatch[2] || ''); |
| 664 | delete params[key]; |
| 665 | } |
| 666 | }); |
| 667 | return result.join(''); |
| 668 | } |
| 669 | }]; |
| 670 | } |
| 671 | |
| 672 | ngRouteModule.provider('$routeParams', $RouteParamsProvider); |
| 673 | |
| 674 | |
| 675 | /** |
| 676 | * @ngdoc service |
| 677 | * @name $routeParams |
| 678 | * @requires $route |
| 679 | * |
| 680 | * @description |
| 681 | * The `$routeParams` service allows you to retrieve the current set of route parameters. |
| 682 | * |
| 683 | * Requires the {@link ngRoute `ngRoute`} module to be installed. |
| 684 | * |
| 685 | * The route parameters are a combination of {@link ng.$location `$location`}'s |
| 686 | * {@link ng.$location#search `search()`} and {@link ng.$location#path `path()`}. |
| 687 | * The `path` parameters are extracted when the {@link ngRoute.$route `$route`} path is matched. |
| 688 | * |
| 689 | * In case of parameter name collision, `path` params take precedence over `search` params. |
| 690 | * |
| 691 | * The service guarantees that the identity of the `$routeParams` object will remain unchanged |
| 692 | * (but its properties will likely change) even when a route change occurs. |
| 693 | * |
| 694 | * Note that the `$routeParams` are only updated *after* a route change completes successfully. |
| 695 | * This means that you cannot rely on `$routeParams` being correct in route resolve functions. |
| 696 | * Instead you can use `$route.current.params` to access the new route's parameters. |
| 697 | * |
| 698 | * @example |
| 699 | * ```js |
| 700 | * // Given: |
| 701 | * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby |
| 702 | * // Route: /Chapter/:chapterId/Section/:sectionId |
| 703 | * // |
| 704 | * // Then |
| 705 | * $routeParams ==> {chapterId:'1', sectionId:'2', search:'moby'} |
| 706 | * ``` |
| 707 | */ |
| 708 | function $RouteParamsProvider() { |
| 709 | this.$get = function() { return {}; }; |
| 710 | } |
| 711 | |
| 712 | ngRouteModule.directive('ngView', ngViewFactory); |
| 713 | ngRouteModule.directive('ngView', ngViewFillContentFactory); |
| 714 | |
| 715 | |
| 716 | /** |
| 717 | * @ngdoc directive |
| 718 | * @name ngView |
| 719 | * @restrict ECA |
| 720 | * |
| 721 | * @description |
| 722 | * # Overview |
| 723 | * `ngView` is a directive that complements the {@link ngRoute.$route $route} service by |
| 724 | * including the rendered template of the current route into the main layout (`index.html`) file. |
| 725 | * Every time the current route changes, the included view changes with it according to the |
| 726 | * configuration of the `$route` service. |
| 727 | * |
| 728 | * Requires the {@link ngRoute `ngRoute`} module to be installed. |
| 729 | * |
| 730 | * @animations |
| 731 | * enter - animation is used to bring new content into the browser. |
| 732 | * leave - animation is used to animate existing content away. |
| 733 | * |
| 734 | * The enter and leave animation occur concurrently. |
| 735 | * |
| 736 | * @scope |
| 737 | * @priority 400 |
| 738 | * @param {string=} onload Expression to evaluate whenever the view updates. |
| 739 | * |
| 740 | * @param {string=} autoscroll Whether `ngView` should call {@link ng.$anchorScroll |
| 741 | * $anchorScroll} to scroll the viewport after the view is updated. |
| 742 | * |
| 743 | * - If the attribute is not set, disable scrolling. |
| 744 | * - If the attribute is set without value, enable scrolling. |
| 745 | * - Otherwise enable scrolling only if the `autoscroll` attribute value evaluated |
| 746 | * as an expression yields a truthy value. |
| 747 | * @example |
| 748 | <example name="ngView-directive" module="ngViewExample" |
| 749 | deps="angular-route.js;angular-animate.js" |
| 750 | animations="true" fixBase="true"> |
| 751 | <file name="index.html"> |
| 752 | <div ng-controller="MainCtrl as main"> |
| 753 | Choose: |
| 754 | <a href="Book/Moby">Moby</a> | |
| 755 | <a href="Book/Moby/ch/1">Moby: Ch1</a> | |
| 756 | <a href="Book/Gatsby">Gatsby</a> | |
| 757 | <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | |
| 758 | <a href="Book/Scarlet">Scarlet Letter</a><br/> |
| 759 | |
| 760 | <div class="view-animate-container"> |
| 761 | <div ng-view class="view-animate"></div> |
| 762 | </div> |
| 763 | <hr /> |
| 764 | |
| 765 | <pre>$location.path() = {{main.$location.path()}}</pre> |
| 766 | <pre>$route.current.templateUrl = {{main.$route.current.templateUrl}}</pre> |
| 767 | <pre>$route.current.params = {{main.$route.current.params}}</pre> |
| 768 | <pre>$routeParams = {{main.$routeParams}}</pre> |
| 769 | </div> |
| 770 | </file> |
| 771 | |
| 772 | <file name="book.html"> |
| 773 | <div> |
| 774 | controller: {{book.name}}<br /> |
| 775 | Book Id: {{book.params.bookId}}<br /> |
| 776 | </div> |
| 777 | </file> |
| 778 | |
| 779 | <file name="chapter.html"> |
| 780 | <div> |
| 781 | controller: {{chapter.name}}<br /> |
| 782 | Book Id: {{chapter.params.bookId}}<br /> |
| 783 | Chapter Id: {{chapter.params.chapterId}} |
| 784 | </div> |
| 785 | </file> |
| 786 | |
| 787 | <file name="animations.css"> |
| 788 | .view-animate-container { |
| 789 | position:relative; |
| 790 | height:100px!important; |
| 791 | position:relative; |
| 792 | background:white; |
| 793 | border:1px solid black; |
| 794 | height:40px; |
| 795 | overflow:hidden; |
| 796 | } |
| 797 | |
| 798 | .view-animate { |
| 799 | padding:10px; |
| 800 | } |
| 801 | |
| 802 | .view-animate.ng-enter, .view-animate.ng-leave { |
| 803 | -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; |
| 804 | transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 1.5s; |
| 805 | |
| 806 | display:block; |
| 807 | width:100%; |
| 808 | border-left:1px solid black; |
| 809 | |
| 810 | position:absolute; |
| 811 | top:0; |
| 812 | left:0; |
| 813 | right:0; |
| 814 | bottom:0; |
| 815 | padding:10px; |
| 816 | } |
| 817 | |
| 818 | .view-animate.ng-enter { |
| 819 | left:100%; |
| 820 | } |
| 821 | .view-animate.ng-enter.ng-enter-active { |
| 822 | left:0; |
| 823 | } |
| 824 | .view-animate.ng-leave.ng-leave-active { |
| 825 | left:-100%; |
| 826 | } |
| 827 | </file> |
| 828 | |
| 829 | <file name="script.js"> |
| 830 | angular.module('ngViewExample', ['ngRoute', 'ngAnimate']) |
| 831 | .config(['$routeProvider', '$locationProvider', |
| 832 | function($routeProvider, $locationProvider) { |
| 833 | $routeProvider |
| 834 | .when('/Book/:bookId', { |
| 835 | templateUrl: 'book.html', |
| 836 | controller: 'BookCtrl', |
| 837 | controllerAs: 'book' |
| 838 | }) |
| 839 | .when('/Book/:bookId/ch/:chapterId', { |
| 840 | templateUrl: 'chapter.html', |
| 841 | controller: 'ChapterCtrl', |
| 842 | controllerAs: 'chapter' |
| 843 | }); |
| 844 | |
| 845 | $locationProvider.html5Mode(true); |
| 846 | }]) |
| 847 | .controller('MainCtrl', ['$route', '$routeParams', '$location', |
| 848 | function($route, $routeParams, $location) { |
| 849 | this.$route = $route; |
| 850 | this.$location = $location; |
| 851 | this.$routeParams = $routeParams; |
| 852 | }]) |
| 853 | .controller('BookCtrl', ['$routeParams', function($routeParams) { |
| 854 | this.name = "BookCtrl"; |
| 855 | this.params = $routeParams; |
| 856 | }]) |
| 857 | .controller('ChapterCtrl', ['$routeParams', function($routeParams) { |
| 858 | this.name = "ChapterCtrl"; |
| 859 | this.params = $routeParams; |
| 860 | }]); |
| 861 | |
| 862 | </file> |
| 863 | |
| 864 | <file name="protractor.js" type="protractor"> |
| 865 | it('should load and compile correct template', function() { |
| 866 | element(by.linkText('Moby: Ch1')).click(); |
| 867 | var content = element(by.css('[ng-view]')).getText(); |
| 868 | expect(content).toMatch(/controller\: ChapterCtrl/); |
| 869 | expect(content).toMatch(/Book Id\: Moby/); |
| 870 | expect(content).toMatch(/Chapter Id\: 1/); |
| 871 | |
| 872 | element(by.partialLinkText('Scarlet')).click(); |
| 873 | |
| 874 | content = element(by.css('[ng-view]')).getText(); |
| 875 | expect(content).toMatch(/controller\: BookCtrl/); |
| 876 | expect(content).toMatch(/Book Id\: Scarlet/); |
| 877 | }); |
| 878 | </file> |
| 879 | </example> |
| 880 | */ |
| 881 | |
| 882 | |
| 883 | /** |
| 884 | * @ngdoc event |
| 885 | * @name ngView#$viewContentLoaded |
| 886 | * @eventType emit on the current ngView scope |
| 887 | * @description |
| 888 | * Emitted every time the ngView content is reloaded. |
| 889 | */ |
| 890 | ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; |
| 891 | function ngViewFactory($route, $anchorScroll, $animate) { |
| 892 | return { |
| 893 | restrict: 'ECA', |
| 894 | terminal: true, |
| 895 | priority: 400, |
| 896 | transclude: 'element', |
| 897 | link: function(scope, $element, attr, ctrl, $transclude) { |
| 898 | var currentScope, |
| 899 | currentElement, |
| 900 | previousLeaveAnimation, |
| 901 | autoScrollExp = attr.autoscroll, |
| 902 | onloadExp = attr.onload || ''; |
| 903 | |
| 904 | scope.$on('$routeChangeSuccess', update); |
| 905 | update(); |
| 906 | |
| 907 | function cleanupLastView() { |
| 908 | if (previousLeaveAnimation) { |
| 909 | $animate.cancel(previousLeaveAnimation); |
| 910 | previousLeaveAnimation = null; |
| 911 | } |
| 912 | |
| 913 | if (currentScope) { |
| 914 | currentScope.$destroy(); |
| 915 | currentScope = null; |
| 916 | } |
| 917 | if (currentElement) { |
| 918 | previousLeaveAnimation = $animate.leave(currentElement); |
| 919 | previousLeaveAnimation.then(function() { |
| 920 | previousLeaveAnimation = null; |
| 921 | }); |
| 922 | currentElement = null; |
| 923 | } |
| 924 | } |
| 925 | |
| 926 | function update() { |
| 927 | var locals = $route.current && $route.current.locals, |
| 928 | template = locals && locals.$template; |
| 929 | |
| 930 | if (angular.isDefined(template)) { |
| 931 | var newScope = scope.$new(); |
| 932 | var current = $route.current; |
| 933 | |
| 934 | // Note: This will also link all children of ng-view that were contained in the original |
| 935 | // html. If that content contains controllers, ... they could pollute/change the scope. |
| 936 | // However, using ng-view on an element with additional content does not make sense... |
| 937 | // Note: We can't remove them in the cloneAttchFn of $transclude as that |
| 938 | // function is called before linking the content, which would apply child |
| 939 | // directives to non existing elements. |
| 940 | var clone = $transclude(newScope, function(clone) { |
| 941 | $animate.enter(clone, null, currentElement || $element).then(function onNgViewEnter() { |
| 942 | if (angular.isDefined(autoScrollExp) |
| 943 | && (!autoScrollExp || scope.$eval(autoScrollExp))) { |
| 944 | $anchorScroll(); |
| 945 | } |
| 946 | }); |
| 947 | cleanupLastView(); |
| 948 | }); |
| 949 | |
| 950 | currentElement = clone; |
| 951 | currentScope = current.scope = newScope; |
| 952 | currentScope.$emit('$viewContentLoaded'); |
| 953 | currentScope.$eval(onloadExp); |
| 954 | } else { |
| 955 | cleanupLastView(); |
| 956 | } |
| 957 | } |
| 958 | } |
| 959 | }; |
| 960 | } |
| 961 | |
| 962 | // This directive is called during the $transclude call of the first `ngView` directive. |
| 963 | // It will replace and compile the content of the element with the loaded template. |
| 964 | // We need this directive so that the element content is already filled when |
| 965 | // the link function of another directive on the same element as ngView |
| 966 | // is called. |
| 967 | ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; |
| 968 | function ngViewFillContentFactory($compile, $controller, $route) { |
| 969 | return { |
| 970 | restrict: 'ECA', |
| 971 | priority: -400, |
| 972 | link: function(scope, $element) { |
| 973 | var current = $route.current, |
| 974 | locals = current.locals; |
| 975 | |
| 976 | $element.html(locals.$template); |
| 977 | |
| 978 | var link = $compile($element.contents()); |
| 979 | |
| 980 | if (current.controller) { |
| 981 | locals.$scope = scope; |
| 982 | var controller = $controller(current.controller, locals); |
| 983 | if (current.controllerAs) { |
| 984 | scope[current.controllerAs] = controller; |
| 985 | } |
| 986 | $element.data('$ngControllerController', controller); |
| 987 | $element.children().data('$ngControllerController', controller); |
| 988 | } |
| 989 | |
| 990 | link(scope); |
| 991 | } |
| 992 | }; |
| 993 | } |
| 994 | |
| 995 | |
| 996 | })(window, window.angular); |