diff --git a/src/ng/location.js b/src/ng/location.js index c315551006ac..d5dd54dfe83a 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -829,11 +829,21 @@ function $LocationProvider() { $rootScope.$evalAsync(function() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; + var defaultPrevented; $location.$$parse(newUrl); $location.$$state = newState; - if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, - newState, oldState).defaultPrevented) { + + defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + newState, oldState).defaultPrevented; + + // only process the location change if the location Url was not changed by a + // `$locationChangeStart` event handler. + if ($location.absUrl() !== newUrl) { + return; + } + + if (defaultPrevented) { $location.$$parse(oldUrl); $location.$$state = oldState; setBrowserUrlWithFallback(oldUrl, false, oldState); @@ -857,13 +867,22 @@ function $LocationProvider() { initializing = false; $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, - $location.$$state, oldState).defaultPrevented) { + var newUrl = $location.absUrl(); + var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, + $location.$$state, oldState).defaultPrevented; + + // only process the location change if the location Url was not changed by a + // `$locationChangeStart` event handler. + if ($location.absUrl() !== newUrl) { + return; + } + + if (defaultPrevented) { $location.$$parse(oldUrl); $location.$$state = oldState; } else { if (urlOrStateChanged) { - setBrowserUrlWithFallback($location.absUrl(), currentReplace, + setBrowserUrlWithFallback(newUrl, currentReplace, oldState === $location.$$state ? null : $location.$$state); } afterLocationChange(oldUrl, oldState); diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 5a338729cf00..09c5bde961a1 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -1690,6 +1690,95 @@ describe('$location', function() { expect($browser.url()).toEqual('http://server/'); })); + it('should allow redirect during $locationChangeStart', + inject(function($location, $browser, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + if (newUrl === 'http://server/#/somePath') { + $location.url('/redirectPath'); + } + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + $location.url('/somePath'); + $rootScope.$apply(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/redirectPath', 'http://server/', + 'http://server/#/redirectPath']); + + expect($location.url()).toEqual('/redirectPath'); + expect($browser.url()).toEqual('http://server/#/redirectPath'); + }) + ); + + it('should allow redirect during $locationChangeStart even if default prevented', + inject(function($location, $browser, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + if (newUrl === 'http://server/#/somePath') { + event.preventDefault(); + $location.url('/redirectPath'); + } + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + $location.url('/somePath'); + $rootScope.$apply(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/redirectPath', 'http://server/', + 'http://server/#/redirectPath']); + + expect($location.url()).toEqual('/redirectPath'); + expect($browser.url()).toEqual('http://server/#/redirectPath'); + }) + ); + + it('should allow multiple redirect during $locationChangeStart', + inject(function($location, $browser, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + if (newUrl === 'http://server/#/somePath') { + $location.url('/redirectPath'); + } else if (newUrl === 'http://server/#/redirectPath') { + $location.url('/redirectPath2'); + } + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + $location.url('/somePath'); + $rootScope.$apply(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath2', 'http://server/', 'http://server/']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/redirectPath2', 'http://server/', + 'http://server/#/redirectPath2']); + + expect($location.url()).toEqual('/redirectPath2'); + expect($browser.url()).toEqual('http://server/#/redirectPath2'); + }) + ); + it ('should fire $locationChangeSuccess event when change from browser location bar', inject(function($log, $location, $browser, $rootScope) { $rootScope.$apply(); // clear initial $locationChangeStart @@ -1715,6 +1804,66 @@ describe('$location', function() { }) ); + it('should allow redirect during browser url change', + inject(function($location, $browser, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + if (newUrl === 'http://server/#/somePath') { + $location.url('/redirectPath'); + } + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + $browser.url('http://server/#/somePath'); + $browser.poll(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', + 'http://server/#/somePath']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath', 'http://server/#/somePath', + 'http://server/#/somePath']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/redirectPath', 'http://server/#/somePath', + 'http://server/#/redirectPath']); + + expect($location.url()).toEqual('/redirectPath'); + expect($browser.url()).toEqual('http://server/#/redirectPath'); + }) + ); + + it('should allow redirect during browser url change even if default prevented', + inject(function($location, $browser, $rootScope, $log) { + $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) { + $log.info('before', newUrl, oldUrl, $browser.url()); + if (newUrl === 'http://server/#/somePath') { + event.preventDefault(); + $location.url('/redirectPath'); + } + }); + $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) { + $log.info('after', newUrl, oldUrl, $browser.url()); + }); + + $browser.url('http://server/#/somePath'); + $browser.poll(); + + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/somePath', 'http://server/', + 'http://server/#/somePath']); + expect($log.info.logs.shift()). + toEqual(['before', 'http://server/#/redirectPath', 'http://server/#/somePath', + 'http://server/#/somePath']); + expect($log.info.logs.shift()). + toEqual(['after', 'http://server/#/redirectPath', 'http://server/#/somePath', + 'http://server/#/redirectPath']); + + expect($location.url()).toEqual('/redirectPath'); + expect($browser.url()).toEqual('http://server/#/redirectPath'); + }) + ); it('should listen on click events on href and prevent browser default in hashbang mode', function() { module(function() { diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 009c6d2dd43a..7864cc7364d3 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -70,6 +70,36 @@ describe('$route', function() { }); }); + it('should allow redirects while handling $routeChangeStart', function() { + module(function($routeProvider) { + $routeProvider.when('/some', { + id: 'some', template: 'Some functionality' + }); + $routeProvider.when('/redirect', { + id: 'redirect' + }); + }); + module(provideLog); + inject(function($route, $location, $rootScope, $compile, log) { + $rootScope.$on('$routeChangeStart', function(event, next, current) { + if (next.id === 'some') { + $location.path('/redirect'); + } + }); + $compile('
')($rootScope); + $rootScope.$on('$routeChangeStart', log.fn('routeChangeStart')); + $rootScope.$on('$routeChangeError', log.fn('routeChangeError')); + $rootScope.$on('$routeChangeSuccess', log.fn('routeChangeSuccess')); + $rootScope.$apply(function() { + $location.path('/some'); + }); + + expect($route.current.id).toBe('redirect'); + expect($location.path()).toBe('/redirect'); + expect(log).toEqual(['routeChangeStart', 'routeChangeStart', 'routeChangeSuccess']); + }); + }); + it('should route and fire change event', function() { var log = '', lastRoute,