diff options
| -rw-r--r-- | Rakefile | 1 | ||||
| -rw-r--r-- | jsTestDriver-jquery.conf | 1 | ||||
| -rw-r--r-- | jsTestDriver-perf.conf | 1 | ||||
| -rw-r--r-- | jsTestDriver.conf | 1 | ||||
| -rw-r--r-- | src/angular-bootstrap.js | 1 | ||||
| -rw-r--r-- | src/service/route.js | 242 | ||||
| -rw-r--r-- | src/service/routeParams.js | 31 | ||||
| -rw-r--r-- | src/widgets.js | 5 | ||||
| -rw-r--r-- | test/service/routeParamsSpec.js | 41 | ||||
| -rw-r--r-- | test/service/routeSpec.js | 94 |
10 files changed, 286 insertions, 132 deletions
@@ -27,6 +27,7 @@ ANGULAR = [ 'src/service/log.js', 'src/service/resource.js', 'src/service/route.js', + 'src/service/routeParams.js', 'src/service/window.js', 'src/service/xhr.bulk.js', 'src/service/xhr.cache.js', diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 018e836c..705ade10 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -30,6 +30,7 @@ load: - src/service/log.js - src/service/resource.js - src/service/route.js + - src/service/routeParams.js - src/service/window.js - src/service/xhr.bulk.js - src/service/xhr.cache.js diff --git a/jsTestDriver-perf.conf b/jsTestDriver-perf.conf index a46cbf71..6ddc018e 100644 --- a/jsTestDriver-perf.conf +++ b/jsTestDriver-perf.conf @@ -28,6 +28,7 @@ load: - src/service/log.js - src/service/resource.js - src/service/route.js + - src/service/routeParams.js - src/service/updateView.js - src/service/window.js - src/service/xhr.bulk.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index b55a387c..24349094 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -30,6 +30,7 @@ load: - src/service/log.js - src/service/resource.js - src/service/route.js + - src/service/routeParams.js - src/service/window.js - src/service/xhr.bulk.js - src/service/xhr.cache.js diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index fdf07c3e..f88973e0 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -114,6 +114,7 @@ 'service/log.js', 'service/resource.js', 'service/route.js', + 'service/routeParams.js', 'service/window.js', 'service/xhr.bulk.js', 'service/xhr.cache.js', diff --git a/src/service/route.js b/src/service/route.js index b7e4814b..523a244a 100644 --- a/src/service/route.js +++ b/src/service/route.js @@ -5,6 +5,7 @@ * @ngdoc service * @name angular.service.$route * @requires $location + * @requires $routeParams * * @property {Object} current Reference to the current route definition. * @property {Array.<Object>} routes Array of all configured routes. @@ -14,7 +15,7 @@ * definition. It is used for deep-linking URLs to controllers and views (HTML partials). * * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} - * widget. + * widget and the {@link angular.service.$routeParams $routeParams} service. * * @example This example shows how changing the URL hash causes the <tt>$route</tt> @@ -24,23 +25,23 @@ <doc:example> <doc:source jsfiddle="false"> <script> - function MainCntl($route, $location) { + function MainCntl($route, $routeParams, $location) { this.$route = $route; this.$location = $location; + this.$routeParams = $routeParams; $route.when('/Book/:bookId', {template: 'examples/book.html', controller: BookCntl}); $route.when('/Book/:bookId/ch/:chapterId', {template: 'examples/chapter.html', controller: ChapterCntl}); - $route.onChange(function() { - $route.current.scope.params = $route.current.params; - }); } - function BookCntl() { + function BookCntl($routeParams) { this.name = "BookCntl"; + this.params = $routeParams; } - function ChapterCntl() { + function ChapterCntl($routeParams) { this.name = "ChapterCntl"; + this.params = $routeParams; } </script> @@ -54,6 +55,7 @@ <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> <hr /> <ng:view></ng:view> </div> @@ -62,37 +64,69 @@ </doc:scenario> </doc:example> */ -angularServiceInject('$route', function($location) { +angularServiceInject('$route', function($location, $routeParams) { + /** + * @workInProgress + * @ngdoc event + * @name angular.service.$route#$beforeRouteChange + * @eventOf angular.service.$route + * @eventType Broadcast on root scope + * @description + * Broadcasted before a route change. + * + * @param {Route} next Future route information. + * @param {Route} current Current route information. + * + * The `Route` object extends the route definition with the following properties. + * + * * `scope` - The instance of the route controller. + * * `params` - The current {@link angular.service.$routeParams params}. + * + */ + + /** + * @workInProgress + * @ngdoc event + * @name angular.service.$route#$afterRouteChange + * @eventOf angular.service.$route + * @eventType Broadcast on root scope + * @description + * Broadcasted after a route change. + * + * @param {Route} current Current route information. + * @param {Route} previous Previous route information. + * + * The `Route` object extends the route definition with the following properties. + * + * * `scope` - The instance of the route controller. + * * `params` - The current {@link angular.service.$routeParams params}. + * + */ + + /** + * @workInProgress + * @ngdoc event + * @name angular.service.$route#$routeUpdate + * @eventOf angular.service.$route + * @eventType Emit on the current route scope. + * @description + * + * The `reloadOnSearch` property has been set to false, and we are reusing the same + * instance of the Controller. + */ + var routes = {}, - onChange = [], matcher = switchRouteMatcher, parentScope = this, + rootScope = this, dirty = 0, - lastHashPath, - lastRouteParams, + allowReload = true, $route = { routes: routes, /** * @workInProgress * @ngdoc method - * @name angular.service.$route#onChange - * @methodOf angular.service.$route - * - * @param {function()} fn Function that will be called when `$route.current` changes. - * @returns {function()} The registered function. - * - * @description - * Register a handler function that will be called when route changes - */ - onChange: function(fn) { - onChange.push(fn); - return fn; - }, - - /** - * @workInProgress - * @ngdoc method * @name angular.service.$route#parent * @methodOf angular.service.$route * @@ -114,7 +148,7 @@ angularServiceInject('$route', function($location) { * @methodOf angular.service.$route * * @param {string} path Route path (matched against `$location.hash`) - * @param {Object} params Mapping information to be assigned to `$route.current` on route + * @param {Object} route Mapping information to be assigned to `$route.current` on route * match. * * Object properties: @@ -139,14 +173,15 @@ angularServiceInject('$route', function($location) { * to update `$location.hash`. * * - `[reloadOnSearch=true]` - {boolean=} - reload route when $location.hashSearch - * changes. If this option is disabled, you should set up a $watch to be notified of - * param (hashSearch) changes as follows: + * changes. + * + * If the option is set to false and url in the browser changes, then + * $routeUpdate event is emited on the current route scope. You can use this event to + * react to {@link angular.service.$routeParams} changes: * - * function MyCtrl($route) { - * this.$watch(function() { - * return $route.current.params; - * }, function(scope, params) { - * //do stuff with params + * function MyCtrl($route, $routeParams) { + * this.$on('$routeUpdate', function() { + * // do stuff with $routeParams * }); * } * @@ -155,13 +190,13 @@ angularServiceInject('$route', function($location) { * @description * Adds a new route definition to the `$route` service. */ - when:function (path, params) { + when:function (path, route) { if (isUndefined(path)) return routes; //TODO(im): remove - not needed! - var route = routes[path]; - if (!route) route = routes[path] = {reloadOnSearch: true}; - if (params) extend(route, params); //TODO(im): what the heck? merge two route definitions? + 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? dirty++; - return route; + return routeDef; }, /** @@ -192,10 +227,18 @@ angularServiceInject('$route', function($location) { */ reload: function() { dirty++; + allowReload = false; } }; + + this.$watch(function(){ return dirty + $location.hash; }, updateRoute); + + return $route; + + ///////////////////////////////////////////////////// + function switchRouteMatcher(on, when, dstName) { var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', params = [], @@ -219,79 +262,72 @@ angularServiceInject('$route', function($location) { return match ? dst : null; } - function updateRoute(){ - var selectedRoute, pathParams, segmentMatch, key, redir; + var next = parseRoute(), + last = $route.current; - if ($route.current) { - if (!$route.current.reloadOnSearch && (lastHashPath == $location.hashPath)) { - $route.current.params = extend($location.hashSearch, lastRouteParams); - return; - } - - if ($route.current.scope) { - $route.current.scope.$destroy(); + if (next && last && next.$route === last.$route + && equals(next.pathParams, last.pathParams) && !next.reloadOnSearch && allowReload) { + $route.current = next; + copy(next.params, $routeParams); + last.scope && last.scope.$emit('$routeUpdate'); + } else { + allowReload = true; + rootScope.$broadcast('$beforeRouteChange', next, last); + last && last.scope && last.scope.$destroy(); + $route.current = next; + if (next) { + if (next.redirectTo) { + $location.update(isString(next.redirectTo) + ? {hashSearch: next.params, hashPath: interpolate(next.redirectTo, next.params)} + : {hash: next.redirectTo(next.pathParams, + $location.hash, $location.hashPath, $location.hashSearch)}); + } else { + copy(next.params, $routeParams); + next.scope = parentScope.$new(next.controller); + } } + rootScope.$broadcast('$afterRouteChange', next, last); } + } + - lastHashPath = $location.hashPath; - $route.current = null; + /** + * @returns the current active route, by matching it against the URL + */ + function parseRoute(){ // Match a route - forEach(routes, function(rParams, rPath) { - if (!pathParams) { - if ((pathParams = matcher($location.hashPath, rPath))) { - selectedRoute = rParams; - } + var params, match; + forEach(routes, function(route, path) { + if (!match && (params = matcher($location.hashPath, path))) { + match = inherit(route, { + params: extend({}, $location.hashSearch, params), + pathParams: params}); + match.$route = route; } }); - // No route matched; fallback to "otherwise" route - selectedRoute = selectedRoute || routes[null]; - - if(selectedRoute) { - if (selectedRoute.redirectTo) { - if (isString(selectedRoute.redirectTo)) { - // interpolate the redirectTo string - redir = {hashPath: '', - hashSearch: extend({}, $location.hashSearch, pathParams)}; - - forEach(selectedRoute.redirectTo.split(':'), function(segment, i) { - if (i==0) { - redir.hashPath += segment; - } else { - segmentMatch = segment.match(/(\w+)(.*)/); - key = segmentMatch[1]; - redir.hashPath += pathParams[key] || $location.hashSearch[key]; - redir.hashPath += segmentMatch[2] || ''; - delete redir.hashSearch[key]; - } - }); - } else { - // call custom redirectTo function - redir = {hash: selectedRoute.redirectTo(pathParams, $location.hash, $location.hashPath, - $location.hashSearch)}; - } + return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}}); + } - $location.update(redir); - return; + /** + * @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]; } - - $route.current = extend({}, selectedRoute); - $route.current.params = extend({}, $location.hashSearch, pathParams); - lastRouteParams = pathParams; - } - - //fire onChange callbacks - forEach(onChange, parentScope.$eval, parentScope); - - // Create the scope if we have matched a route - if ($route.current) { - $route.current.scope = parentScope.$new($route.current.controller); - } + }); + return result.join(''); } - this.$watch(function(){ return dirty + $location.hash; }, updateRoute); - - return $route; -}, ['$location']); +}, ['$location', '$routeParams']); diff --git a/src/service/routeParams.js b/src/service/routeParams.js new file mode 100644 index 00000000..8a69903f --- /dev/null +++ b/src/service/routeParams.js @@ -0,0 +1,31 @@ +'use strict'; + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$routeParams + * @requires $route + * + * @description + * Current set of route parameters. The route parameters are a combination of the + * {@link angular.service.$location $location} `hashSearch`, and `path`. The `path` parameters + * are extracted when the {@link angular.service.$route $route} path is matched. + * + * In case of parameter name collision, `path` params take precedence over `hashSearch` params. + * + * The service guarantees that the identity of the `$routeParams` object will remain unchanged + * (but its properties will likely change) even when a route change occurs. + * + * @example + * <pre> + * // Given: + * // URL: http://server.com/index.html#/Chapter/1/Section/2?search=moby + * // Route: /Chapter/:chapterId/Section/:sectionId + * // + * // Then + * $routeParams ==> {chapterId:1, sectionId:2, search:'moby'} + * </pre> + */ +angularService('$routeParams', function(){ + return {}; +}); diff --git a/src/widgets.js b/src/widgets.js index a1c4c5d6..42e608dd 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -1422,10 +1422,9 @@ angularWidget('ng:view', function(element) { var template; var changeCounter = 0; - $route.onChange(function(){ + this.$on('$afterRouteChange', function(){ changeCounter++; - })(); //initialize the state forcefully, it's possible that we missed the initial - //$route#onChange already + }); this.$watch(function(){return changeCounter;}, function() { var template = $route.current && $route.current.template; diff --git a/test/service/routeParamsSpec.js b/test/service/routeParamsSpec.js new file mode 100644 index 00000000..58a37f2e --- /dev/null +++ b/test/service/routeParamsSpec.js @@ -0,0 +1,41 @@ +'use strict'; + +describe('$routeParams', function(){ + it('should publish the params into a service', function(){ + var scope = angular.scope(), + $location = scope.$service('$location'), + $route = scope.$service('$route'), + $routeParams = scope.$service('$routeParams'); + + $route.when('/foo'); + $route.when('/bar/:barId'); + + $location.hash = '/foo?a=b'; + scope.$digest(); + expect($routeParams).toEqual({a:'b'}); + + $location.hash = '/bar/123?x=abc'; + scope.$digest(); + expect($routeParams).toEqual({barId:'123', x:'abc'}); + }); + + + it('should preserve object identity during route reloads', function(){ + var scope = angular.scope(), + $location = scope.$service('$location'), + $route = scope.$service('$route'), + $routeParams = scope.$service('$routeParams'), + firstRouteParams = $routeParams; + + $route.when('/foo'); + $route.when('/bar/:barId'); + + $location.hash = '/foo?a=b'; + scope.$digest(); + expect(scope.$service('$routeParams')).toBe(firstRouteParams); + + $location.hash = '/bar/123?x=abc'; + scope.$digest(); + expect(scope.$service('$routeParams')).toBe(firstRouteParams); + }); +}); diff --git a/test/service/routeSpec.js b/test/service/routeSpec.js index 72c7745c..b1dde915 100644 --- a/test/service/routeSpec.js +++ b/test/service/routeSpec.js @@ -15,37 +15,48 @@ describe('$route', function() { it('should route and fire change event', function(){ var log = '', - $location, $route; + $location, $route, + lastRoute, + nextRoute; function BookChapter() { - log += '<init>'; + log += '<init>;'; } scope = compile('<div></div>')(); $location = scope.$service('$location'); $route = scope.$service('$route'); $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'}); $route.when('/Blank'); - $route.onChange(function(){ - log += 'onChange();'; + scope.$on('$beforeRouteChange', function(event, next, current){ + log += 'before();'; + expect(current).toBe($route.current); + lastRoute = current; + nextRoute = next; + }); + scope.$on('$afterRouteChange', function(event, current, last){ + log += 'after();'; + expect(current).toBe($route.current); + expect(lastRoute).toBe(last); + expect(nextRoute).toBe(current); }); $location.update('http://server#/Book/Moby/Chapter/Intro?p=123'); scope.$digest(); - expect(log).toEqual('onChange();<init>'); + expect(log).toEqual('before();<init>;after();'); expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); var lastId = $route.current.scope.$id; log = ''; $location.update('http://server#/Blank?ignore'); scope.$digest(); - expect(log).toEqual('onChange();'); + expect(log).toEqual('before();after();'); expect($route.current.params).toEqual({ignore:true}); expect($route.current.scope.$id).not.toEqual(lastId); log = ''; $location.update('http://server#/NONE'); scope.$digest(); - expect(log).toEqual('onChange();'); + expect(log).toEqual('before();after();'); expect($route.current).toEqual(null); $route.when('/NONE', {template:'instant update'}); @@ -54,15 +65,6 @@ describe('$route', function() { }); - it('should return fn registered with onChange()', function() { - var scope = angular.scope(), - $route = scope.$service('$route'), - fn = function() {}; - - expect($route.onChange(fn)).toBe(fn); - }); - - it('should allow routes to be defined with just templates without controllers', function() { var scope = angular.scope(), $location = scope.$service('$location'), @@ -70,7 +72,7 @@ describe('$route', function() { onChangeSpy = jasmine.createSpy('onChange'); $route.when('/foo', {template: 'foo.html'}); - $route.onChange(onChangeSpy); + scope.$on('$beforeRouteChange', onChangeSpy); expect($route.current).toBeUndefined(); expect(onChangeSpy).not.toHaveBeenCalled(); @@ -93,7 +95,7 @@ describe('$route', function() { $route.when('/foo', {template: 'foo.html'}); $route.otherwise({template: '404.html', controller: NotFoundCtrl}); - $route.onChange(onChangeSpy); + scope.$on('$beforeRouteChange', onChangeSpy); expect($route.current).toBeUndefined(); expect(onChangeSpy).not.toHaveBeenCalled(); @@ -163,7 +165,7 @@ describe('$route', function() { $route.when('/bar', {template: 'bar.html'}); $route.when('/baz', {redirectTo: '/bar'}); $route.otherwise({template: '404.html'}); - $route.onChange(onChangeSpy); + scope.$on('$beforeRouteChange', onChangeSpy); expect($route.current).toBeUndefined(); expect(onChangeSpy).not.toHaveBeenCalled(); @@ -172,7 +174,8 @@ describe('$route', function() { expect($location.hash).toBe('/foo'); expect($route.current.template).toBe('foo.html'); - expect(onChangeSpy.callCount).toBe(1); + expect(onChangeSpy.callCount).toBe(2); + onChangeSpy.reset(); $location.updateHash(''); @@ -181,7 +184,7 @@ describe('$route', function() { expect($location.hash).toBe('/foo'); expect($route.current.template).toBe('foo.html'); - expect(onChangeSpy.callCount).toBe(1); + expect(onChangeSpy.callCount).toBe(2); onChangeSpy.reset(); $location.updateHash('/baz'); @@ -190,7 +193,7 @@ describe('$route', function() { expect($location.hash).toBe('/bar'); expect($route.current.template).toBe('bar.html'); - expect(onChangeSpy.callCount).toBe(1); + expect(onChangeSpy.callCount).toBe(2); }); @@ -267,10 +270,11 @@ describe('$route', function() { var scope = angular.scope(), $location = scope.$service('$location'), $route = scope.$service('$route'), + $rouetParams = scope.$service('$routeParams'), reloaded = jasmine.createSpy('route reload'); $route.when('/foo', {controller: FooCtrl}); - $route.onChange(reloaded); + scope.$on('$beforeRouteChange', reloaded); function FooCtrl() { reloaded(); @@ -279,12 +283,14 @@ describe('$route', function() { $location.updateHash('/foo'); scope.$digest(); expect(reloaded).toHaveBeenCalled(); + expect($rouetParams).toEqual({}); reloaded.reset(); // trigger reload $location.hashSearch.foo = 'bar'; scope.$digest(); expect(reloaded).toHaveBeenCalled(); + expect($rouetParams).toEqual({foo:'bar'}); }); @@ -293,13 +299,15 @@ describe('$route', function() { var scope = angular.scope(), $location = scope.$service('$location'), $route = scope.$service('$route'), - reloaded = jasmine.createSpy('route reload'); + reloaded = jasmine.createSpy('route reload'), + routeUpdateEvent = jasmine.createSpy('route reload'); $route.when('/foo', {controller: FooCtrl, reloadOnSearch: false}); - $route.onChange(reloaded); + scope.$on('$beforeRouteChange', reloaded); function FooCtrl() { reloaded(); + this.$on('$routeUpdate', routeUpdateEvent); } expect(reloaded).not.toHaveBeenCalled(); @@ -307,12 +315,14 @@ describe('$route', function() { $location.updateHash('/foo'); scope.$digest(); expect(reloaded).toHaveBeenCalled(); + expect(routeUpdateEvent).not.toHaveBeenCalled(); reloaded.reset(); // don't trigger reload $location.hashSearch.foo = 'bar'; scope.$digest(); expect(reloaded).not.toHaveBeenCalled(); + expect(routeUpdateEvent).toHaveBeenCalled(); }); @@ -324,7 +334,7 @@ describe('$route', function() { onRouteChange = jasmine.createSpy('onRouteChange'); $route.when('/foo/:fooId', {controller: FooCtrl, reloadOnSearch: false}); - $route.onChange(onRouteChange); + scope.$on('$beforeRouteChange', onRouteChange); function FooCtrl() { reloaded(); @@ -394,5 +404,37 @@ describe('$route', function() { scope.$digest(); expect(routeParams).toHaveBeenCalledWith({barId: '123', foo: 'bar'}); }); + + + describe('reload', function(){ + + it('should reload even if reloadOnSearch is false', function(){ + var scope = angular.scope(), + $location = scope.$service('$location'), + $route = scope.$service('$route'), + $routeParams = scope.$service('$routeParams'), + count = 0; + + $route.when('/bar/:barId', {controller: FooCtrl, reloadOnSearch: false}); + + function FooCtrl() { count ++; } + + $location.updateHash('/bar/123'); + scope.$digest(); + expect($routeParams).toEqual({barId:'123'}); + expect(count).toEqual(1); + + $location.hash = '/bar/123?a=b'; + scope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); + expect(count).toEqual(1); + + $route.reload(); + scope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); + expect(count).toEqual(2); + }); + }); + }); }); |
