From 537d418ec027349bd0dc12403752b8860edb86f7 Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 00:34:02 +0100 Subject: [PATCH 1/7] feat(ngRoute): add method for changing url params Add a $route#update method for changing the current route parameters without having to build a URL and call $location#path. Useful for apps with a structure involving programmatically moving between pages on the current route, but with different :param values. --- src/ngRoute/route.js | 32 +++++++++++++++++++ test/ngRoute/routeSpec.js | 67 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 4ecc932b427e..a0a5a1a66e8a 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -436,6 +436,21 @@ function $RouteProvider(){ reload: function() { forceReload = true; $rootScope.$evalAsync(updateRoute); + }, + + /** + * @ngdoc method + * @name $route#update + * + * @description + * Causes `$route` service to update the current URL, replacing + * current route parameters with those specified in `newParams`. + * + * @param {Object} newParams mapping of URL parameter names to values + */ + update: function(newParams) { + newParams = defaults(newParams, this.current.params); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); } }; @@ -589,5 +604,22 @@ function $RouteProvider(){ }); return result.join(''); } + + /** + * @returns {Object} object composed of all keys in `object` and `defaults`, + * where keys missing in `object` have the value from the + * same key in `defaults` + */ + function defaults(object, other) { + var o = angular.copy(object); + + for (var key in other) { + if (other.hasOwnProperty(key)) { + o[key] = o[key] || other[key]; + } + } + + return o; + } }]; } diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 072cf6bb8ab7..2f91fe73351c 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1046,6 +1046,73 @@ describe('$route', function() { }); }); + describe('update', function() { + it('should support single-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/bar/:barId', {controller: angular.noop}); + }); + + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $location.path('/bar/1'); + $rootScope.$digest(); + routeChangeSpy.reset(); + + $route.update({barId: '2'}); + $rootScope.$digest(); + + expect($routeParams).toEqual({barId: '2'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + }); + }); + + it('should support total multi-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); + }); + + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $location.path('/bar/1/1/1/1'); + $rootScope.$digest(); + routeChangeSpy.reset(); + + $route.update({barId: '2', fooId: '2', spamId: '2', eggId: '2'}); + $rootScope.$digest(); + + expect($routeParams).toEqual({barId: '2', fooId: '2', spamId: '2', eggId: '2'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + }); + }); + + it('should support partial multi-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); + }); + + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $location.path('/bar/1/1/1/1'); + $rootScope.$digest(); + routeChangeSpy.reset(); + + $route.update({barId: '2', fooId: '2'}); + $rootScope.$digest(); + + expect($routeParams).toEqual({barId: '2', fooId: '2', spamId: '1', eggId: '1'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + }); + }); + }); describe('reload', function() { From 7c65c5e859d749e21d1c4f73be522c3938bc4d17 Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 02:02:47 +0100 Subject: [PATCH 2/7] test(ngRoute): check distinct param values This ensures that parameters are matched to the correct value when being updated. --- test/ngRoute/routeSpec.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 2f91fe73351c..38042f01a761 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1079,14 +1079,14 @@ describe('$route', function() { inject(function($route, $routeParams, $location, $rootScope) { $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/1/1/1/1'); + $location.path('/bar/1/2/3/4'); $rootScope.$digest(); routeChangeSpy.reset(); - $route.update({barId: '2', fooId: '2', spamId: '2', eggId: '2'}); + $route.update({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); $rootScope.$digest(); - expect($routeParams).toEqual({barId: '2', fooId: '2', spamId: '2', eggId: '2'}); + expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); }); }); @@ -1101,14 +1101,14 @@ describe('$route', function() { inject(function($route, $routeParams, $location, $rootScope) { $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/1/1/1/1'); + $location.path('/bar/1/2/3/4'); $rootScope.$digest(); routeChangeSpy.reset(); - $route.update({barId: '2', fooId: '2'}); + $route.update({barId: '5', fooId: '6'}); $rootScope.$digest(); - expect($routeParams).toEqual({barId: '2', fooId: '2', spamId: '1', eggId: '1'}); + expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); }); }); From 70f179fbff1546e32cd416b2afbd87fb86cce67e Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 02:04:03 +0100 Subject: [PATCH 3/7] test(ngRoute): check $location.path() updates Adds a check to ensure the $location.path() changes to an expected URL when the route changes. --- test/ngRoute/routeSpec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 38042f01a761..c7763324c779 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1066,6 +1066,7 @@ describe('$route', function() { expect($routeParams).toEqual({barId: '2'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/2'); }); }); @@ -1088,6 +1089,7 @@ describe('$route', function() { expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/5/6/7/8'); }); }); @@ -1110,6 +1112,7 @@ describe('$route', function() { expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/5/6/3/4'); }); }); }); From 2966ab52289a6b76f2560252abf9c327d9945e51 Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 02:07:46 +0100 Subject: [PATCH 4/7] refactor(ngRoute): rename update -> updateParams Change the name of the method to make clearer its purpose. --- src/ngRoute/route.js | 4 ++-- test/ngRoute/routeSpec.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index a0a5a1a66e8a..8b0c08e16147 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -440,7 +440,7 @@ function $RouteProvider(){ /** * @ngdoc method - * @name $route#update + * @name $route#updateParams * * @description * Causes `$route` service to update the current URL, replacing @@ -448,7 +448,7 @@ function $RouteProvider(){ * * @param {Object} newParams mapping of URL parameter names to values */ - update: function(newParams) { + updateParams: function(newParams) { newParams = defaults(newParams, this.current.params); $location.path(interpolate(this.current.$$route.originalPath, newParams)); } diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index c7763324c779..526fe0fe4385 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1061,7 +1061,7 @@ describe('$route', function() { $rootScope.$digest(); routeChangeSpy.reset(); - $route.update({barId: '2'}); + $route.updateParams({barId: '2'}); $rootScope.$digest(); expect($routeParams).toEqual({barId: '2'}); @@ -1084,7 +1084,7 @@ describe('$route', function() { $rootScope.$digest(); routeChangeSpy.reset(); - $route.update({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); + $route.updateParams({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); $rootScope.$digest(); expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); @@ -1107,7 +1107,7 @@ describe('$route', function() { $rootScope.$digest(); routeChangeSpy.reset(); - $route.update({barId: '5', fooId: '6'}); + $route.updateParams({barId: '5', fooId: '6'}); $rootScope.$digest(); expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'}); From 16787ee636923e1229ec9aeba6e7fc0950b6bf62 Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 02:10:53 +0100 Subject: [PATCH 5/7] refactor(ngRoute): swap custom method for extend Better to use angular methods than my clunky code. --- src/ngRoute/route.js | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 8b0c08e16147..8e772f5cb5b5 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -449,7 +449,7 @@ function $RouteProvider(){ * @param {Object} newParams mapping of URL parameter names to values */ updateParams: function(newParams) { - newParams = defaults(newParams, this.current.params); + newParams = angular.extend({}, this.current.params, newParams); $location.path(interpolate(this.current.$$route.originalPath, newParams)); } }; @@ -604,22 +604,5 @@ function $RouteProvider(){ }); return result.join(''); } - - /** - * @returns {Object} object composed of all keys in `object` and `defaults`, - * where keys missing in `object` have the value from the - * same key in `defaults` - */ - function defaults(object, other) { - var o = angular.copy(object); - - for (var key in other) { - if (other.hasOwnProperty(key)) { - o[key] = o[key] || other[key]; - } - } - - return o; - } }]; } From e547cb86eb2de3b3d8f0ad185860bb1086ef2dfc Mon Sep 17 00:00:00 2001 From: Jack Wearden Date: Sun, 27 Jul 2014 16:33:57 +0100 Subject: [PATCH 6/7] test(ngRoute): move updateParams tests to own unit I accidentally added the updateParams tests inside of the tests for `reloadOnSearch` - This commit extracts them to their own describe block. --- test/ngRoute/routeSpec.js | 126 +++++++++++++++++++------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 526fe0fe4385..81f031655dde 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1046,105 +1046,105 @@ describe('$route', function() { }); }); - describe('update', function() { - it('should support single-parameter route updating', function() { + describe('reload', function() { + + it('should reload even if reloadOnSearch is false', function() { var routeChangeSpy = jasmine.createSpy('route change'); module(function($routeProvider) { - $routeProvider.when('/bar/:barId', {controller: angular.noop}); + $routeProvider.when('/bar/:barId', {controller: angular.noop, reloadOnSearch: false}); }); - inject(function($route, $routeParams, $location, $rootScope) { + inject(function($route, $location, $rootScope, $routeParams) { $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/1'); + $location.path('/bar/123'); $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); routeChangeSpy.reset(); - $route.updateParams({barId: '2'}); + $location.path('/bar/123').search('a=b'); $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); + expect(routeChangeSpy).not.toHaveBeenCalled(); - expect($routeParams).toEqual({barId: '2'}); + $route.reload(); + $rootScope.$digest(); + expect($routeParams).toEqual({barId:'123', a:'b'}); expect(routeChangeSpy).toHaveBeenCalledOnce(); - expect($location.path()).toEqual('/bar/2'); }); }); + }); + }); - it('should support total multi-parameter route updating', function() { - var routeChangeSpy = jasmine.createSpy('route change'); + describe('update', function() { + it('should support single-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); - module(function($routeProvider) { - $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); - }); + module(function($routeProvider) { + $routeProvider.when('/bar/:barId', {controller: angular.noop}); + }); - inject(function($route, $routeParams, $location, $rootScope) { - $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/1/2/3/4'); - $rootScope.$digest(); - routeChangeSpy.reset(); + $location.path('/bar/1'); + $rootScope.$digest(); + routeChangeSpy.reset(); - $route.updateParams({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); - $rootScope.$digest(); + $route.updateParams({barId: '2'}); + $rootScope.$digest(); - expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - expect($location.path()).toEqual('/bar/5/6/7/8'); - }); + expect($routeParams).toEqual({barId: '2'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/2'); }); + }); - it('should support partial multi-parameter route updating', function() { - var routeChangeSpy = jasmine.createSpy('route change'); + it('should support total multi-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); - module(function($routeProvider) { - $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); - }); + module(function($routeProvider) { + $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); + }); - inject(function($route, $routeParams, $location, $rootScope) { - $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/1/2/3/4'); - $rootScope.$digest(); - routeChangeSpy.reset(); + $location.path('/bar/1/2/3/4'); + $rootScope.$digest(); + routeChangeSpy.reset(); - $route.updateParams({barId: '5', fooId: '6'}); - $rootScope.$digest(); + $route.updateParams({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); + $rootScope.$digest(); - expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - expect($location.path()).toEqual('/bar/5/6/3/4'); - }); + expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '7', eggId: '8'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/5/6/7/8'); }); }); - describe('reload', function() { - - it('should reload even if reloadOnSearch is false', function() { - var routeChangeSpy = jasmine.createSpy('route change'); + it('should support partial multi-parameter route updating', function() { + var routeChangeSpy = jasmine.createSpy('route change'); - module(function($routeProvider) { - $routeProvider.when('/bar/:barId', {controller: angular.noop, reloadOnSearch: false}); - }); + module(function($routeProvider) { + $routeProvider.when('/bar/:barId/:fooId/:spamId/:eggId', {controller: angular.noop}); + }); - inject(function($route, $location, $rootScope, $routeParams) { - $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); - $location.path('/bar/123'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - routeChangeSpy.reset(); + $location.path('/bar/1/2/3/4'); + $rootScope.$digest(); + routeChangeSpy.reset(); - $location.path('/bar/123').search('a=b'); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123', a:'b'}); - expect(routeChangeSpy).not.toHaveBeenCalled(); + $route.updateParams({barId: '5', fooId: '6'}); + $rootScope.$digest(); - $route.reload(); - $rootScope.$digest(); - expect($routeParams).toEqual({barId:'123', a:'b'}); - expect(routeChangeSpy).toHaveBeenCalledOnce(); - }); + expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/5/6/3/4'); }); }); }); From 4590a80266d89976c171424e883893220c5ac09c Mon Sep 17 00:00:00 2001 From: Jeff Cross Date: Wed, 13 Aug 2014 11:30:22 -0700 Subject: [PATCH 7/7] feat($route): add support for query param updating via updateParams Properties in the object passed to $route.updateParams() will be added to the location as queryParams if not contained within the route's path definition. --- src/ngRoute/route.js | 22 +++++++++++++++++++--- test/ngRoute/routeSpec.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 8e772f5cb5b5..53b1927d6ad5 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -17,7 +17,8 @@ */ /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider); + provider('$route', $RouteProvider), + $routeMinErr = angular.$$minErr('ngRoute'); /** * @ngdoc provider @@ -445,12 +446,27 @@ function $RouteProvider(){ * @description * Causes `$route` service to update the current URL, replacing * current route parameters with those specified in `newParams`. + * Provided property names that match the route's path segment + * definitions will be interpolated into the location's path, while + * remaining properties will be treated as query params. * * @param {Object} newParams mapping of URL parameter names to values */ updateParams: function(newParams) { - newParams = angular.extend({}, this.current.params, newParams); - $location.path(interpolate(this.current.$$route.originalPath, newParams)); + if (this.current && this.current.$$route) { + var searchParams = {}, self=this; + + angular.forEach(Object.keys(newParams), function(key) { + if (!self.current.pathParams[key]) searchParams[key] = newParams[key]; + }); + + newParams = angular.extend({}, this.current.params, newParams); + $location.path(interpolate(this.current.$$route.originalPath, newParams)); + $location.search(angular.extend({}, $location.search(), searchParams)); + } + else { + throw $routeMinErr('norout', 'Tried updating route when with no current route'); + } } }; diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 81f031655dde..5dcf96edcb32 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -1147,5 +1147,36 @@ describe('$route', function() { expect($location.path()).toEqual('/bar/5/6/3/4'); }); }); + + + it('should update query params when new properties are not in path', function() { + var routeChangeSpy = jasmine.createSpy('route change'); + + module(function($routeProvider) { + $routeProvider.when('/bar/:barId/:fooId/:spamId/', {controller: angular.noop}); + }); + + inject(function($route, $routeParams, $location, $rootScope) { + $rootScope.$on('$routeChangeSuccess', routeChangeSpy); + + $location.path('/bar/1/2/3'); + $location.search({initial: 'true'}); + $rootScope.$digest(); + routeChangeSpy.reset(); + + $route.updateParams({barId: '5', fooId: '6', eggId: '4'}); + $rootScope.$digest(); + + expect($routeParams).toEqual({barId: '5', fooId: '6', spamId: '3', eggId: '4', initial: 'true'}); + expect(routeChangeSpy).toHaveBeenCalledOnce(); + expect($location.path()).toEqual('/bar/5/6/3/'); + expect($location.search()).toEqual({eggId: '4', initial: 'true'}); + }); + }); + + + it('should complain if called without an existing route', inject(function($route) { + expect($route.updateParams).toThrowMinErr('ngRoute', 'norout'); + })); }); });