diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js
index f0e6c19b9079..6902a3e654d2 100644
--- a/src/ngRoute/route.js
+++ b/src/ngRoute/route.js
@@ -183,11 +183,22 @@ function $RouteProvider() {
* `redirectTo` takes precedence over `resolveRedirectTo`, so specifying both on the same
* route definition, will cause the latter to be ignored.
*
+ * - `[reloadOnUrl=true]` - `{boolean=}` - reload route when any part of the URL changes
+ * (inluding the path) even if the new URL maps to the same route.
+ *
+ * If the option is set to `false` and the URL in the browser changes, but the new URL maps
+ * to the same route, then a `$routeUpdate` event is broadcasted on the root scope (without
+ * reloading the route).
+ *
* - `[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.
+ * If the option is set to `false` and the URL in the browser changes, then a `$routeUpdate`
+ * event is broadcasted on the root scope (without reloading the route).
+ *
+ *
+ * **Note:** This option has no effect if `reloadOnUrl` is set to `false`.
+ *
*
* - `[caseInsensitiveMatch=false]` - `{boolean=}` - match routes without being case sensitive
*
@@ -202,6 +213,9 @@ function $RouteProvider() {
this.when = function(path, route) {
//copy original route object to preserve params inherited from proto chain
var routeCopy = shallowCopy(route);
+ if (angular.isUndefined(routeCopy.reloadOnUrl)) {
+ routeCopy.reloadOnUrl = true;
+ }
if (angular.isUndefined(routeCopy.reloadOnSearch)) {
routeCopy.reloadOnSearch = true;
}
@@ -544,8 +558,9 @@ function $RouteProvider() {
* @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.
+ * Broadcasted if the same instance of a route (including template, controller instance,
+ * resolved dependencies, etc.) is being reused. This can happen if either `reloadOnSearch` or
+ * `reloadOnUrl` has been set to `false`.
*
* @param {Object} angularEvent Synthetic event object
* @param {Route} current Current/previous route information.
@@ -653,9 +668,7 @@ function $RouteProvider() {
var lastRoute = $route.current;
preparedRoute = parseRoute();
- preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route
- && angular.equals(preparedRoute.pathParams, lastRoute.pathParams)
- && !preparedRoute.reloadOnSearch && !forceReload;
+ preparedRouteIsUpdateOnly = isNavigationUpdateOnly(preparedRoute, lastRoute);
if (!preparedRouteIsUpdateOnly && (lastRoute || preparedRoute)) {
if ($rootScope.$broadcast('$routeChangeStart', preparedRoute, lastRoute).defaultPrevented) {
@@ -835,6 +848,29 @@ function $RouteProvider() {
return match || routes[null] && inherit(routes[null], {params: {}, pathParams:{}});
}
+ /**
+ * @param {Object} newRoute - The new route configuration (as returned by `parseRoute()`).
+ * @param {Object} oldRoute - The previous route configuration (as returned by `parseRoute()`).
+ * @returns {boolean} Whether this is an "update-only" navigation, i.e. the URL maps to the same
+ * route and it can be reused (based on the config and the type of change).
+ */
+ function isNavigationUpdateOnly(newRoute, oldRoute) {
+ // IF this is not a forced reload
+ return !forceReload
+ // AND both `newRoute`/`oldRoute` are defined
+ && newRoute && oldRoute
+ // AND they map to the same Route Definition Object
+ && (newRoute.$$route === oldRoute.$$route)
+ // AND `reloadOnUrl` is disabled
+ && (!newRoute.reloadOnUrl
+ // OR `reloadOnSearch` is disabled
+ || (!newRoute.reloadOnSearch
+ // AND both routes have the same path params
+ && angular.equals(newRoute.pathParams, oldRoute.pathParams)
+ )
+ );
+ }
+
/**
* @returns {string} interpolation of the redirect path with the parameters
*/
diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js
index 36832ab57884..14d655af83e9 100644
--- a/test/ngRoute/routeSpec.js
+++ b/test/ngRoute/routeSpec.js
@@ -65,8 +65,8 @@ describe('$route', function() {
$httpBackend.when('GET', 'Chapter.html').respond('chapter');
$httpBackend.when('GET', 'test.html').respond('test');
$httpBackend.when('GET', 'foo.html').respond('foo');
- $httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'bar.html').respond('bar');
+ $httpBackend.when('GET', 'baz.html').respond('baz');
$httpBackend.when('GET', 'http://example.com/trusted-template.html').respond('cross domain trusted template');
$httpBackend.when('GET', '404.html').respond('not found');
};
@@ -76,6 +76,7 @@ describe('$route', function() {
dealoc(element);
});
+
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
module(function($routeProvider) {
$routeProvider.when('/Edit', {
@@ -1677,95 +1678,413 @@ describe('$route', function() {
});
- describe('reloadOnSearch', function() {
- it('should reload a route when reloadOnSearch is enabled and .search() changes', function() {
- var reloaded = jasmine.createSpy('route reload');
+ describe('reloadOnUrl', function() {
+ it('should reload when `reloadOnUrl` is true and `.url()` changes', function() {
+ var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
- $routeProvider.when('/foo', {controller: angular.noop});
+ $routeProvider.when('/path/:param', {});
});
- inject(function($route, $location, $rootScope, $routeParams) {
- $rootScope.$on('$routeChangeStart', reloaded);
- $location.path('/foo');
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+
+ // Initial load
+ $location.path('/path/foo');
$rootScope.$digest();
- expect(reloaded).toHaveBeenCalled();
- expect($routeParams).toEqual({});
- reloaded.calls.reset();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'foo'});
- // trigger reload
- $location.search({foo: 'bar'});
+ routeChange.calls.reset();
+
+ // Reload on `path` change
+ $location.path('/path/bar');
$rootScope.$digest();
- expect(reloaded).toHaveBeenCalled();
- expect($routeParams).toEqual({foo:'bar'});
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar'});
+
+ routeChange.calls.reset();
+
+ // Reload on `search` change
+ $location.search('foo', 'bar');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
+
+ routeChange.calls.reset();
+
+ // Reload on `hash` change
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
});
});
- it('should not reload a route when reloadOnSearch is disabled and only .search() changes', function() {
- var routeChange = jasmine.createSpy('route change'),
- routeUpdate = jasmine.createSpy('route update');
+ it('should reload when `reloadOnUrl` is false and URL maps to different route',
+ function() {
+ var routeChange = jasmine.createSpy('routeChange');
+ var routeUpdate = jasmine.createSpy('routeUpdate');
+
+ module(function($routeProvider) {
+ $routeProvider.
+ when('/path/:param', {reloadOnUrl: false}).
+ otherwise({});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Route change
+ $location.path('/other/path/bar');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({});
+ });
+ }
+ );
+
+
+ it('should not reload when `reloadOnUrl` is false and URL maps to the same route',
+ function() {
+ var routeChange = jasmine.createSpy('routeChange');
+ var routeUpdate = jasmine.createSpy('routeUpdate');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {reloadOnUrl: false});
+ });
+
+ inject(function($location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ // Initial load
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChange.calls.reset();
+
+ // Route update (no reload)
+ $location.path('/path/bar').search('foo', 'bar').hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({param: 'bar', foo: 'bar'});
+ });
+ }
+ );
+
+
+ it('should update `$routeParams` even when not reloading a route', function() {
+ var routeChange = jasmine.createSpy('routeChange');
module(function($routeProvider) {
- $routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
+ $routeProvider.when('/path/:param', {reloadOnUrl: false});
});
- inject(function($route, $location, $rootScope) {
+ inject(function($location, $rootScope, $routeParams) {
$rootScope.$on('$routeChangeStart', routeChange);
$rootScope.$on('$routeChangeSuccess', routeChange);
- $rootScope.$on('$routeUpdate', routeUpdate);
expect(routeChange).not.toHaveBeenCalled();
- $location.path('/foo');
+ // Initial load
+ $location.path('/path/foo');
$rootScope.$digest();
- expect(routeChange).toHaveBeenCalled();
expect(routeChange).toHaveBeenCalledTimes(2);
- expect(routeUpdate).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'foo'});
+
routeChange.calls.reset();
- // don't trigger reload
- $location.search({foo: 'bar'});
+ // Route update (no reload)
+ $location.path('/path/bar');
$rootScope.$digest();
expect(routeChange).not.toHaveBeenCalled();
- expect(routeUpdate).toHaveBeenCalled();
+ expect($routeParams).toEqual({param: 'bar'});
});
});
- it('should reload reloadOnSearch route when url differs only in route path param', function() {
- var routeChange = jasmine.createSpy('route change');
+ describe('with `$route.reload()`', function() {
+ var $location;
+ var $log;
+ var $rootScope;
+ var $route;
+ var routeChangeStart;
+ var routeChangeSuccess;
- module(function($routeProvider) {
- $routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
+ beforeEach(module(function($routeProvider) {
+ $routeProvider.when('/path/:param', {
+ template: '',
+ reloadOnUrl: false,
+ controller: function Controller($log) {
+ $log.debug('initialized');
+ }
+ });
+ }));
+
+ beforeEach(inject(function($compile, _$location_, _$log_, _$rootScope_, _$route_) {
+ $location = _$location_;
+ $log = _$log_;
+ $rootScope = _$rootScope_;
+ $route = _$route_;
+
+ routeChangeStart = jasmine.createSpy('routeChangeStart');
+ routeChangeSuccess = jasmine.createSpy('routeChangeSuccess');
+
+ $rootScope.$on('$routeChangeStart', routeChangeStart);
+ $rootScope.$on('$routeChangeSuccess', routeChangeSuccess);
+
+ element = $compile('
')($rootScope);
+ }));
+
+
+ it('should reload the current route', function() {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
+
+ $route.reload();
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ $log.reset();
});
- inject(function($route, $location, $rootScope) {
- $rootScope.$on('$routeChangeStart', routeChange);
- $rootScope.$on('$routeChangeSuccess', routeChange);
- expect(routeChange).not.toHaveBeenCalled();
+ it('should support preventing a route reload', function() {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
- $location.path('/foo/aaa');
+ routeChangeStart.and.callFake(function(evt) { evt.preventDefault(); });
+
+ $route.reload();
$rootScope.$digest();
- expect(routeChange).toHaveBeenCalled();
- expect(routeChange).toHaveBeenCalledTimes(2);
- routeChange.calls.reset();
+ expect($location.path()).toBe('/path/foo');
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+ });
+
+
+ it('should reload the current route even if `reloadOnUrl` is disabled',
+ inject(function($routeParams) {
+ $location.path('/path/foo');
+ $rootScope.$digest();
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+ expect($routeParams).toEqual({param: 'foo'});
+
+ routeChangeStart.calls.reset();
+ routeChangeSuccess.calls.reset();
+ $log.reset();
+
+ $location.path('/path/bar');
+ $rootScope.$digest();
+ expect(routeChangeStart).not.toHaveBeenCalled();
+ expect(routeChangeSuccess).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+ expect($routeParams).toEqual({param: 'bar'});
+
+ $route.reload();
+ $rootScope.$digest();
+ expect(routeChangeStart).toHaveBeenCalledOnce();
+ expect(routeChangeSuccess).toHaveBeenCalledOnce();
+ expect($log.debug.logs).toEqual([['initialized']]);
+ expect($routeParams).toEqual({param: 'bar'});
+
+ $log.reset();
+ })
+ );
+ });
+ });
+
+ describe('reloadOnSearch', function() {
+ it('should not have any effect if `reloadOnUrl` is false', function() {
+ var reloaded = jasmine.createSpy('route reload');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {
+ reloadOnUrl: false,
+ reloadOnSearch: true
+ });
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', reloaded);
- $location.path('/foo/bbb');
+ $location.path('/foo');
$rootScope.$digest();
- expect(routeChange).toHaveBeenCalled();
- expect(routeChange).toHaveBeenCalledTimes(2);
- routeChange.calls.reset();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({});
+ reloaded.calls.reset();
+
+ // trigger reload (via .search())
$location.search({foo: 'bar'});
$rootScope.$digest();
- expect(routeChange).not.toHaveBeenCalled();
+ expect(reloaded).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({foo: 'bar'});
+
+ // trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(reloaded).not.toHaveBeenCalled();
+ expect($routeParams).toEqual({foo: 'bar'});
});
});
- it('should update params when reloadOnSearch is disabled and .search() changes', function() {
+ it('should reload when `reloadOnSearch` is true and `.search()`/`.hash()` changes',
+ function() {
+ var reloaded = jasmine.createSpy('route reload');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop});
+ });
+
+ inject(function($route, $location, $rootScope, $routeParams) {
+ $rootScope.$on('$routeChangeStart', reloaded);
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({});
+
+ reloaded.calls.reset();
+
+ // trigger reload (via .search())
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({foo: 'bar'});
+
+ reloaded.calls.reset();
+
+ // trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(reloaded).toHaveBeenCalledOnce();
+ expect($routeParams).toEqual({foo: 'bar'});
+ });
+ }
+ );
+
+
+ it('should not reload when `reloadOnSearch` is false and `.search()`/`.hash()` changes',
+ function() {
+ var routeChange = jasmine.createSpy('route change'),
+ routeUpdate = jasmine.createSpy('route update');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo', {controller: angular.noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+ $rootScope.$on('$routeUpdate', routeUpdate);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ expect(routeUpdate).not.toHaveBeenCalled();
+
+ routeChange.calls.reset();
+
+ // don't trigger reload (via .search())
+ $location.search({foo: 'bar'});
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalledOnce();
+
+ routeUpdate.calls.reset();
+
+ // don't trigger reload (via .hash())
+ $location.hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ expect(routeUpdate).toHaveBeenCalled();
+ });
+ }
+ );
+
+
+ it('should reload when `reloadOnSearch` is false and url differs only in route path param',
+ function() {
+ var routeChange = jasmine.createSpy('route change');
+
+ module(function($routeProvider) {
+ $routeProvider.when('/foo/:fooId', {controller: angular.noop, reloadOnSearch: false});
+ });
+
+ inject(function($route, $location, $rootScope) {
+ $rootScope.$on('$routeChangeStart', routeChange);
+ $rootScope.$on('$routeChangeSuccess', routeChange);
+
+ expect(routeChange).not.toHaveBeenCalled();
+
+ $location.path('/foo/aaa');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ routeChange.calls.reset();
+
+ $location.path('/foo/bbb');
+ $rootScope.$digest();
+ expect(routeChange).toHaveBeenCalledTimes(2);
+ routeChange.calls.reset();
+
+ $location.search({foo: 'bar'}).hash('baz');
+ $rootScope.$digest();
+ expect(routeChange).not.toHaveBeenCalled();
+ });
+ }
+ );
+
+
+ it('should update params when `reloadOnSearch` is false and `.search()` changes', function() {
var routeParamsWatcher = jasmine.createSpy('routeParamsWatcher');
module(function($routeProvider) {
@@ -1852,7 +2171,8 @@ describe('$route', function() {
});
});
- describe('reload', function() {
+
+ describe('with `$route.reload()`', function() {
var $location;
var $log;
var $rootScope;
@@ -1886,6 +2206,7 @@ describe('$route', function() {
element = $compile('')($rootScope);
}));
+
it('should reload the current route', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1908,6 +2229,7 @@ describe('$route', function() {
$log.reset();
});
+
it('should support preventing a route reload', function() {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1930,6 +2252,7 @@ describe('$route', function() {
expect($log.debug.logs).toEqual([]);
});
+
it('should reload even if reloadOnSearch is false', inject(function($routeParams) {
$location.path('/bar/123');
$rootScope.$digest();
@@ -1946,6 +2269,15 @@ describe('$route', function() {
expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
expect($log.debug.logs).toEqual([]);
+ routeChangeSuccessSpy.calls.reset();
+ $log.reset();
+
+ $location.hash('c');
+ $rootScope.$digest();
+ expect($routeParams).toEqual({barId: '123', a: 'b'});
+ expect(routeChangeSuccessSpy).not.toHaveBeenCalled();
+ expect($log.debug.logs).toEqual([]);
+
$route.reload();
$rootScope.$digest();
expect($routeParams).toEqual({barId: '123', a: 'b'});