'use strict'; /** * @ngdoc module * @name ngRoute * @description * * # ngRoute * * The `ngRoute` module provides routing and deeplinking services and directives for angular apps. * * ## Example * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. * * *
*/ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). provider('$route', $RouteProvider); /** * @ngdoc provider * @name $routeProvider * @function * * @description * * Used for configuring routes. * * ## Example * See {@link ngRoute.$route#example $route} for an example of configuring and using `ngRoute`. * * ## Dependencies * Requires the {@link ngRoute `ngRoute`} module to be installed. */ function $RouteProvider(){ function inherit(parent, extra) { return angular.extend(new (angular.extend(function() {}, {prototype:parent}))(), extra); } var routes = {}; /** * @ngdoc method * @name $routeProvider#when * * @param {string} path Route path (matched against `$location.path`). If `$location.path` * contains redundant trailing slash or is missing one, the route will still match and the * `$location.path` will be updated to add or drop the trailing slash to exactly match the * route definition. * * * `path` can contain named groups starting with a colon: e.g. `:name`. All characters up * to the next slash are matched and stored in `$routeParams` under the given `name` * when the route matches. * * `path` can contain named groups starting with a colon and ending with a star: * e.g.`:name*`. All characters are eagerly stored in `$routeParams` under the given `name` * when the route matches. * * `path` can contain optional named groups with a question mark: e.g.`:name?`. * * For example, routes like `/color/:color/largecode/:largecode*\/edit` will match * `/color/brown/largecode/code/with/slashes/edit` and extract: * * * `color: brown` * * `largecode: code/with/slashes`. * * * @param {Object} route Mapping information to be assigned to `$route.current` on route * match. * * Object properties: * * - `controller` – `{(string|function()=}` – Controller fn that should be associated with * newly created scope or the name of a {@link angular.Module#controller registered * controller} if passed as a string. * - `controllerAs` – `{string=}` – A controller alias name. If present the controller will be * published to scope under the `controllerAs` name. * - `template` – `{string=|function()=}` – html template as a string or a function that * returns an html template as a string which should be used by {@link * ngRoute.directive:ngView ngView} or {@link ng.directive:ngInclude ngInclude} directives. * This property takes precedence over `templateUrl`. * * If `template` is a function, it will be called with the following parameters: * * - `{Array.<Object>}` - route parameters extracted from the current * `$location.path()` by applying the current route * * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html * template that should be used by {@link ngRoute.directive:ngView ngView}. * * If `templateUrl` is a function, it will be called with the following parameters: * * - `{Array.<Object>}` - route parameters extracted from the current * `$location.path()` by applying the current route * * - `resolve` - `{Object.=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, the router * will wait for them all to be resolved or one to be rejected before the controller is * instantiated. * If all the promises are resolved successfully, the values of the resolved promises are * injected and {@link ngRoute.$route#$routeChangeSuccess $routeChangeSuccess} event is * fired. If any of the promises are rejected the * {@link ngRoute.$route#$routeChangeError $routeChangeError} event is fired. The map object * is: * * - `key` – `{string}`: a name of a dependency to be injected into the controller. * - `factory` - `{string|function}`: If `string` then it is an alias for a service. * Otherwise if function, then it is {@link auto.$injector#invoke injected} * and the return value is treated as the dependency. If the result is a promise, it is * resolved before its value is injected into the controller. Be aware that * `ngRoute.$routeParams` will still refer to the previous route within these resolve * functions. Use `$route.current.params` to access the new route parameters, instead. * * - `redirectTo` – {(string|function())=} – value to update * {@link ng.$location $location} path with and trigger route redirection. * * If `redirectTo` is a function, it will be called with the following parameters: * * - `{Object.}` - route parameters extracted from the current * `$location.path()` by applying the current route templateUrl. * - `{string}` - current `$location.path()` * - `{Object}` - current `$location.search()` * * The custom `redirectTo` function is expected to return a string which will be used * to update `$location.path()` and `$location.search()`. * * - `[reloadOnSearch=true]` - {boolean=} - reload route when only `$location.search()` * or `$location.hash()` changes. * * If the option is set to `false` and url in the browser changes, then * `$routeUpdate` event is broadcasted on the root scope. * * - `[caseInsensitiveMatch=false]` - {boolean=} - match routes without being case sensitive * * If the option is set to `true`, then the particular route can be matched without being * case sensitive * * @returns {Object} self * * @description * Adds a new route definition to the `$route` service. */ this.when = function(path, route) { routes[path] = angular.extend( {reloadOnSearch: true}, route, path && pathRegExp(path, route) ); // create redirection for trailing slashes if (path) { var redirectPath = (path[path.length-1] == '/') ? path.substr(0, path.length-1) : path +'/'; routes[redirectPath] = angular.extend( {redirectTo: path}, pathRegExp(redirectPath, route) ); } return this; }; /** * @param path {string} path * @param opts {Object} options * @return {?Object} * * @description * Normalizes the given path, returning a regular expression * and the original path. * * Inspired by pathRexp in visionmedia/express/lib/utils.js. */ function pathRegExp(path, opts) { var insensitive = opts.caseInsensitiveMatch, ret = { originalPath: path, regexp: path }, keys = ret.keys = []; path = path .replace(/([().])/g, '\\$1') .replace(/(\/)?:(\w+)([\?\*])?/g, function(_, slash, key, option){ var optional = option === '?' ? option : null; var star = option === '*' ? option : null; keys.push({ name: key, optional: !!optional }); slash = slash || ''; return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (star && '(.+?)' || '([^/]+)') + (optional || '') + ')' + (optional || ''); }) .replace(/([\/$\*])/g, '\\$1'); ret.regexp = new RegExp('^' + path + '$', insensitive ? 'i' : ''); return ret; } /** * @ngdoc method * @name $routeProvider#otherwise * * @description * Sets route definition that will be used on route change when no other route definition * is matched. * * @param {Object} params Mapping information to be assigned to `$route.current`. * @returns {Object} self */ this.otherwise = function(params) { this.when(null, params); return this; }; this.$get = ['$rootScope', '$location', '$routeParams', '$q', '$injector', '$http', '$templateCache', '$sce', function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) { /** * @ngdoc service * @name $route * @requires $location * @requires $routeParams * * @property {Object} current Reference to the current route definition. * The route definition contains: * * - `controller`: The controller constructor as define in route definition. * - `locals`: A map of locals which is used by {@link ng.$controller $controller} service for * controller instantiation. The `locals` contain * the resolved values of the `resolve` map. Additionally the `locals` also contain: * * - `$scope` - The current route scope. * - `$template` - The current route template HTML. * * @property {Object} routes Object with all route configuration Objects as its properties. * * @description * `$route` is used for deep-linking URLs to controllers and views (HTML partials). * It watches `$location.url()` and tries to map the path to an existing route definition. * * Requires the {@link ngRoute `ngRoute`} module to be installed. * * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. * * The `$route` service is typically used in conjunction with the * {@link ngRoute.directive:ngView `ngView`} directive and the * {@link ngRoute.$routeParams `$routeParams`} service. * * @example * This example shows how changing the URL hash causes the `$route` to match a route against the * URL, and the `ngView` pulls in the partial. * * Note that this example is using {@link ng.directive:script inlined templates} * to get it working on jsfiddle as well. * * * *
* Choose: * Moby | * Moby: Ch1 | * Gatsby | * Gatsby: Ch4 | * Scarlet Letter
* *
*
* *
$location.path() = {{$location.path()}}
*
$route.current.templateUrl = {{$route.current.templateUrl}}
*
$route.current.params = {{$route.current.params}}
*
$route.current.scope.name = {{$route.current.scope.name}}
*
$routeParams = {{$routeParams}}
*
*
* * * controller: {{name}}
* Book Id: {{params.bookId}}
*
* * * controller: {{name}}
* Book Id: {{params.bookId}}
* Chapter Id: {{params.chapterId}} *
* * * angular.module('ngRouteExample', ['ngRoute']) * * .config(function($routeProvider, $locationProvider) { * $routeProvider.when('/Book/:bookId', { * templateUrl: 'book.html', * controller: BookCntl, * resolve: { * // I will cause a 1 second delay * delay: function($q, $timeout) { * var delay = $q.defer(); * $timeout(delay.resolve, 1000); * return delay.promise; * } * } * }); * $routeProvider.when('/Book/:bookId/ch/:chapterId', { * templateUrl: 'chapter.html', * controller: ChapterCntl * }); * * // configure html5 to get links working on jsfiddle * $locationProvider.html5Mode(true); * }); * * function MainCntl($scope, $route, $routeParams, $location) { * $scope.$route = $route; * $scope.$location = $location; * $scope.$routeParams = $routeParams; * } * * function BookCntl($scope, $routeParams) { * $scope.name = "BookCntl"; * $scope.params = $routeParams; * } * * function ChapterCntl($scope, $routeParams) { * $scope.name = "ChapterCntl"; * $scope.params = $routeParams; * } * * * * it('should load and compile correct template', function() { * element(by.linkText('Moby: Ch1')).click(); * var content = element(by.css('[ng-view]')).getText(); * expect(content).toMatch(/controller\: ChapterCntl/); * expect(content).toMatch(/Book Id\: Moby/); * expect(content).toMatch(/Chapter Id\: 1/); * * element(by.partialLinkText('Scarlet')).click(); * * content = element(by.css('[ng-view]')).getText(); * expect(content).toMatch(/controller\: BookCntl/); * expect(content).toMatch(/Book Id\: Scarlet/); * }); * *
*/ /** * @ngdoc event * @name $route#$routeChangeStart * @eventType broadcast on root scope * @description * Broadcasted before a route change. At this point the route services starts * resolving all of the dependencies needed for the route change to occur. * Typically this involves fetching the view template as well as any dependencies * defined in `resolve` route property. Once all of the dependencies are resolved * `$routeChangeSuccess` is fired. * * @param {Object} angularEvent Synthetic event object. * @param {Route} next Future route information. * @param {Route} current Current route information. */ /** * @ngdoc event * @name $route#$routeChangeSuccess * @eventType broadcast on root scope * @description * Broadcasted after a route dependencies are resolved. * {@link ngRoute.directive:ngView ngView} listens for the directive * to instantiate the controller and render the view. * * @param {Object} angularEvent Synthetic event object. * @param {Route} current Current route information. * @param {Route|Undefined} previous Previous route information, or undefined if current is * first route entered. */ /** * @ngdoc event * @name $route#$routeChangeError * @eventType broadcast on root scope * @description * Broadcasted if any of the resolve promises are rejected. * * @param {Object} angularEvent Synthetic event object * @param {Route} current Current route information. * @param {Route} previous Previous route information. * @param {Route} rejection Rejection of the promise. Usually the error of the failed promise. */ /** * @ngdoc event * @name $route#$routeUpdate * @eventType broadcast on root scope * @description * * The `reloadOnSearch` property has been set to false, and we are reusing the same * instance of the Controller. */ var forceReload = false, $route = { routes: routes, /** * @ngdoc method * @name $route#reload * * @description * Causes `$route` service to reload the current route even if * {@link ng.$location $location} hasn't changed. * * As a result of that, {@link ngRoute.directive:ngView ngView} * creates new scope, reinstantiates the controller. */ reload: function() { forceReload = true; $rootScope.$evalAsync(updateRoute); } }; $rootScope.$on('$locationChangeSuccess', updateRoute); return $route; ///////////////////////////////////////////////////// /** * @param on {string} current url * @param route {Object} route regexp to match the url against * @return {?Object} * * @description * Check if the route matches the current url. * * Inspired by match in * visionmedia/express/lib/router/router.js. */ function switchRouteMatcher(on, route) { var keys = route.keys, params = {}; if (!route.regexp) return null; var m = route.regexp.exec(on); if (!m) return null; for (var i = 1, len = m.length; i < len; ++i) { var key = keys[i - 1]; var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; if (key && val) { params[key.name] = val; } } return params; } function updateRoute() { var next = parseRoute(), last = $route.current; if (next && last && next.$$route === last.$$route && angular.equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { last.params = next.params; angular.copy(last.params, $routeParams); $rootScope.$broadcast('$routeUpdate', last); } else if (next || last) { forceReload = false; $rootScope.$broadcast('$routeChangeStart', next, last); $route.current = next; if (next) { if (next.redirectTo) { if (angular.isString(next.redirectTo)) { $location.path(interpolate(next.redirectTo, next.params)).search(next.params) .replace(); } else { $location.url(next.redirectTo(next.pathParams, $location.path(), $location.search())) .replace(); } } } $q.when(next). then(function() { if (next) { var locals = angular.extend({}, next.resolve), template, templateUrl; angular.forEach(locals, function(value, key) { locals[key] = angular.isString(value) ? $injector.get(value) : $injector.invoke(value); }); if (angular.isDefined(template = next.template)) { if (angular.isFunction(template)) { template = template(next.params); } } else if (angular.isDefined(templateUrl = next.templateUrl)) { if (angular.isFunction(templateUrl)) { templateUrl = templateUrl(next.params); } templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (angular.isDefined(templateUrl)) { next.loadedTemplateUrl = templateUrl; template = $http.get(templateUrl, {cache: $templateCache}). then(function(response) { return response.data; }); } } if (angular.isDefined(template)) { locals['$template'] = template; } return $q.all(locals); } }). // after route change then(function(locals) { if (next == $route.current) { if (next) { next.locals = locals; angular.copy(next.params, $routeParams); } $rootScope.$broadcast('$routeChangeSuccess', next, last); } }, function(error) { if (next == $route.current) { $rootScope.$broadcast('$routeChangeError', next, last, error); } }); } } /** * @returns {Object} the current active route, by matching it against the URL */ function parseRoute() { // Match a route var params, match; angular.forEach(routes, function(route, path) { if (!match && (params = switchRouteMatcher($location.path(), route))) { match = inherit(route, { params: angular.extend({}, $location.search(), params), pathParams: params}); match.$$route = route; } }); // No route matched; fallback to "otherwise" route return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); } /** * @returns {string} interpolation of the redirect path with the parameters */ function interpolate(string, params) { var result = []; angular.forEach((string||'').split(':'), function(segment, i) { if (i === 0) { result.push(segment); } else { var segmentMatch = segment.match(/(\w+)(.*)/); var key = segmentMatch[1]; result.push(params[key]); result.push(segmentMatch[2] || ''); delete params[key]; } }); return result.join(''); } }]; }