diff options
| author | Misko Hevery | 2012-03-23 14:03:24 -0700 |
|---|---|---|
| committer | Misko Hevery | 2012-03-28 11:16:35 -0700 |
| commit | 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch) | |
| tree | e7529b741d70199f36d52090b430510bad07f233 /src/ng/route.js | |
| parent | 944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff) | |
| download | angular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2 | |
chore(module): move files around in preparation for more modules
Diffstat (limited to 'src/ng/route.js')
| -rw-r--r-- | src/ng/route.js | 351 |
1 files changed, 351 insertions, 0 deletions
diff --git a/src/ng/route.js b/src/ng/route.js new file mode 100644 index 00000000..2b9d187a --- /dev/null +++ b/src/ng/route.js @@ -0,0 +1,351 @@ +'use strict'; + + +/** + * @ngdoc object + * @name angular.module.ng.$routeProvider + * @function + * + * @description + * + * Used for configuring routes. See {@link angular.module.ng.$route $route} for an example. + */ +function $RouteProvider(){ + var routes = {}; + + /** + * @ngdoc method + * @name angular.module.ng.$routeProvider#when + * @methodOf angular.module.ng.$routeProvider + * + * @param {string} path Route path (matched against `$location.path`). If `$location.path` + * contains redudant 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 exacly match the + * route definition. + * @param {Object} route Mapping information to be assigned to `$route.current` on route + * match. + * + * Object properties: + * + * - `controller` – `{function()=}` – Controller fn that should be associated with newly + * created scope. + * - `template` – `{string=}` – path to an html template that should be used by + * {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} or + * {@link angular.module.ng.$compileProvider.directive.ng-include ng-include} directives. + * - `redirectTo` – {(string|function())=} – value to update + * {@link angular.module.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 template. + * - `{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. + * + * @returns {Object} route object + * + * @description + * Adds a new route definition to the `$route` service. + */ + this.when = function(path, route) { + var routeDef = routes[path]; + if (!routeDef) routeDef = routes[path] = {reloadOnSearch: true}; + if (route) extend(routeDef, route); // TODO(im): what the heck? merge two route definitions? + + // 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 routeDef; + }; + + /** + * @ngdoc method + * @name angular.module.ng.$routeProvider#otherwise + * @methodOf angular.module.ng.$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`. + */ + this.otherwise = function(params) { + this.when(null, params); + }; + + + this.$get = ['$rootScope', '$location', '$routeParams', + function( $rootScope, $location, $routeParams) { + + /** + * @ngdoc object + * @name angular.module.ng.$route + * @requires $location + * @requires $routeParams + * + * @property {Object} current Reference to the current route definition. + * @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 angular.module.ng.$routeProvider $routeProvider}'s API. + * + * The `$route` service is typically used in conjunction with {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} + * directive and the {@link angular.module.ng.$routeParams $routeParams} service. + * + * @example + This example shows how changing the URL hash causes the `$route` to match a route against the + URL, and the `ng-view` pulls in the partial. + + Note that this example is using {@link angular.module.ng.$compileProvider.directive.script inlined templates} + to get it working on jsfiddle as well. + + <doc:example module="route"> + <doc:source> + <script type="text/ng-template" id="examples/book.html"> + controller: {{name}}<br /> + Book Id: {{params.bookId}}<br /> + </script> + + <script type="text/ng-template" id="examples/chapter.html"> + controller: {{name}}<br /> + Book Id: {{params.bookId}}<br /> + Chapter Id: {{params.chapterId}} + </script> + + <script> + angular.module('route', [], function($routeProvider, $locationProvider) { + $routeProvider.when('/Book/:bookId', {template: 'examples/book.html', controller: BookCntl}); + $routeProvider.when('/Book/:bookId/ch/:chapterId', {template: 'examples/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; + } + </script> + + <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.template = {{$route.current.template}}</pre> + <pre>$route.current.params = {{$route.current.params}}</pre> + <pre>$route.current.scope.name = {{$route.current.scope.name}}</pre> + <pre>$routeParams = {{$routeParams}}</pre> + </div> + </doc:source> + <doc:scenario> + 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(); + content = element('.doc-example-live [ng-view]').text(); + expect(content).toMatch(/controller\: BookCntl/); + expect(content).toMatch(/Book Id\: Scarlet/); + }); + </doc:scenario> + </doc:example> + */ + + /** + * @ngdoc event + * @name angular.module.ng.$route#$beforeRouteChange + * @eventOf angular.module.ng.$route + * @eventType broadcast on root scope + * @description + * Broadcasted before a route change. + * + * @param {Route} next Future route information. + * @param {Route} current Current route information. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$route#$afterRouteChange + * @eventOf angular.module.ng.$route + * @eventType broadcast on root scope + * @description + * Broadcasted after a route change. + * + * @param {Route} current Current route information. + * @param {Route} previous Previous route information. + */ + + /** + * @ngdoc event + * @name angular.module.ng.$route#$routeUpdate + * @eventOf angular.module.ng.$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 matcher = switchRouteMatcher, + dirty = 0, + forceReload = false, + $route = { + routes: routes, + + /** + * @ngdoc method + * @name angular.module.ng.$route#reload + * @methodOf angular.module.ng.$route + * + * @description + * Causes `$route` service to reload the current route even if + * {@link angular.module.ng.$location $location} hasn't changed. + * + * As a result of that, {@link angular.module.ng.$compileProvider.directive.ng-view ng-view} + * creates new scope, reinstantiates the controller. + */ + reload: function() { + dirty++; + forceReload = true; + } + }; + + $rootScope.$watch(function() { return dirty + $location.url(); }, updateRoute); + + return $route; + + ///////////////////////////////////////////////////// + + function switchRouteMatcher(on, when) { + // TODO(i): this code is convoluted and inefficient, we should construct the route matching + // regex only once and then reuse it + var regex = '^' + when.replace(/([\.\\\(\)\^\$])/g, "\\$1") + '$', + params = [], + dst = {}; + forEach(when.split(/\W/), function(param) { + if (param) { + var paramRegExp = new RegExp(":" + param + "([\\W])"); + if (regex.match(paramRegExp)) { + regex = regex.replace(paramRegExp, "([^\\/]*)$1"); + params.push(param); + } + } + }); + var match = on.match(new RegExp(regex)); + 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('$beforeRouteChange', 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(); + } + } else { + copy(next.params, $routeParams); + } + } + $rootScope.$broadcast('$afterRouteChange', next, last); + } + } + + + /** + * @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 = matcher($location.path(), path))) { + 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 parametrs + */ + 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(''); + } + }]; +} |
