'use strict'; /** * @ngdoc overview * @name ngRoute * @description * * Module that provides routing and deeplinking services and directives for angular apps. */ var ngRouteModule = angular.module('ngRoute', ['ng']). provider('$route', $RouteProvider); /** * @ngdoc object * @name ngRoute.$routeProvider * @function * * @description * * Used for configuring routes. See {@link ngRoute.$route $route} for an example. */ function $RouteProvider(){ var routes = {}; /** * @ngdoc method * @name ngRoute.$routeProvider#when * @methodOf ngRoute.$routeProvider * * @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 (`:name`). All characters up * to the next slash are matched and stored in `$routeParams` under the given `name` * after the route is resolved. * * `path` can contain named groups starting with a star (`*name`). All characters are * eagerly stored in `$routeParams` under the given `name` after the route is resolved. * * For example, routes like `/color/:color/largecode/*largecode/edit` will match * `/color/brown/largecode/code/with/slashs/edit` and extract: * * * `color: brown` * * `largecode: code/with/slashs`. * * * @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.<string, function>=}` - An optional map of dependencies which should * be injected into the controller. If any of these dependencies are promises, they will be * resolved and converted to a value before the controller is instantiated and the * `$routeChangeSuccess` 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 api/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.<string>}` - 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() * 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] = extend({reloadOnSearch: true, caseInsensitiveMatch: false}, route); // create redirection for trailing slashes if (path) { var redirectPath = (path[path.length-1] == '/') ? path.substr(0, path.length-1) : path +'/'; routes[redirectPath] = {redirectTo: path}; } return this; }; /** * @ngdoc method * @name ngRoute.$routeProvider#otherwise * @methodOf ngRoute.$routeProvider * * @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 object * @name ngRoute.$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 {Array.<Object>} routes Array of all configured routes. * * @description * 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. * * You can define routes through {@link ngRoute.$routeProvider $routeProvider}'s API. * * The `$route` service is typically used in conjunction with {@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. <example module="ngView" deps="angular-route.js"> <file name="index.html"> <div ng-controller="MainCntl"> Choose: <a href="Book/Moby">Moby</a> | <a href="Book/Moby/ch/1">Moby: Ch1</a> | <a href="Book/Gatsby">Gatsby</a> | <a href="Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a> | <a href="Book/Scarlet">Scarlet Letter</a><br/> <div ng-view></div> <hr /> <pre>$location.path() = {{$location.path()}}</pre> <pre>$route.current.templateUrl = {{$route.current.templateUrl}}</pre> <pre>$route.current.params = {{$route.current.params}}</pre> <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> <pre>$routeParams = {{$routeParams}}</pre> </div> </file> <file name="book.html"> controller: {{name}}<br /> Book Id: {{params.bookId}}<br /> </file> <file name="chapter.html"> controller: {{name}}<br /> Book Id: {{params.bookId}}<br /> Chapter Id: {{params.chapterId}} </file> <file name="script.js"> angular.module('ngView', ['ngRoute'], 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; } </file> <file name="scenario.js"> it('should load and compile correct template', function() { element('a:contains("Moby: Ch1")').click(); var content = element('.doc-example-live [ng-view]').text(); expect(content).toMatch(/controller\: ChapterCntl/); expect(content).toMatch(/Book Id\: Moby/); expect(content).toMatch(/Chapter Id\: 1/); element('a:contains("Scarlet")').click(); sleep(2); // promises are not part of scenario waiting content = element('.doc-example-live [ng-view]').text(); expect(content).toMatch(/controller\: BookCntl/); expect(content).toMatch(/Book Id\: Scarlet/); }); </file> </example> */ /** * @ngdoc event * @name ngRoute.$route#$routeChangeStart * @eventOf ngRoute.$route * @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 occurs. * 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 {Route} next Future route information. * @param {Route} current Current route information. */ /** * @ngdoc event * @name ngRoute.$route#$routeChangeSuccess * @eventOf ngRoute.$route * @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 ngRoute.$route#$routeChangeError * @eventOf ngRoute.$route * @eventType broadcast on root scope * @description * Broadcasted if any of the resolve promises are rejected. * * @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 ngRoute.$route#$routeUpdate * @eventOf ngRoute.$route * @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 ngRoute.$route#reload * @methodOf ngRoute.$route * * @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 when {string} route when template to match the url against * @param whenProperties {Object} properties to define when's matching behavior * @return {?Object} */ function switchRouteMatcher(on, when, whenProperties) { // TODO(i): this code is convoluted and inefficient, we should construct the route matching // regex only once and then reuse it // Escape regexp special characters. when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, "\\$&") + '$'; var regex = '', params = [], dst = {}; var re = /\\([:*])(\w+)/g, paramMatch, lastMatchedIndex = 0; while ((paramMatch = re.exec(when)) !== null) { // Find each :param in `when` and replace it with a capturing group. // Append all other sections of when unchanged. regex += when.slice(lastMatchedIndex, paramMatch.index); switch(paramMatch[1]) { case ':': regex += '([^\\/]*)'; break; case '*': regex += '(.*)'; break; } params.push(paramMatch[2]); lastMatchedIndex = re.lastIndex; } // Append trailing path part. regex += when.substr(lastMatchedIndex); var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : '')); if (match) { forEach(params, function(name, index) { dst[name] = match[index + 1]; }); } return match ? dst : null; } function updateRoute() { var next = parseRoute(), last = $route.current; if (next && last && next.$$route === last.$$route && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && !forceReload) { last.params = next.params; 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 (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 = extend({}, next.resolve), template, templateUrl; forEach(locals, function(value, key) { locals[key] = isString(value) ? $injector.get(value) : $injector.invoke(value); }); if (isDefined(template = next.template)) { if (isFunction(template)) { template = template(next.params); } } else if (isDefined(templateUrl = next.templateUrl)) { if (isFunction(templateUrl)) { templateUrl = templateUrl(next.params); } templateUrl = $sce.getTrustedResourceUrl(templateUrl); if (isDefined(templateUrl)) { next.loadedTemplateUrl = templateUrl; template = $http.get(templateUrl, {cache: $templateCache}). then(function(response) { return response.data; }); } } if (isDefined(template)) { locals['$template'] = template; } return $q.all(locals); } }). // after route change then(function(locals) { if (next == $route.current) { if (next) { next.locals = locals; copy(next.params, $routeParams); } $rootScope.$broadcast('$routeChangeSuccess', next, last); } }, function(error) { if (next == $route.current) { $rootScope.$broadcast('$routeChangeError', next, last, error); } }); } } /** * @returns the current active route, by matching it against the URL */ function parseRoute() { // Match a route var params, match; forEach(routes, function(route, path) { if (!match && (params = switchRouteMatcher($location.path(), path, route))) { match = inherit(route, { params: 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 interpolation of the redirect path with the parameters */ function interpolate(string, params) { var result = []; 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(''); } }]; }