diff --git a/src/ng/browser.js b/src/ng/browser.js index 3ca4a7c0a86c..528a1a9899e4 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -29,7 +29,8 @@ function Browser(window, document, $log, $sniffer) { history = window.history, setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, - pendingDeferIds = {}; + pendingDeferIds = {}, + hasChangedOutside = false; self.isMock = false; @@ -146,6 +147,7 @@ function Browser(window, document, $log, $sniffer) { * @param {boolean=} replace Should new url replace current history record ? */ self.url = function(url, replace) { + var currentHref; // Android Browser BFCache causes location, history reference to become stale. if (location !== window.location) location = window.location; if (history !== window.history) history = window.history; @@ -175,7 +177,38 @@ function Browser(window, document, $log, $sniffer) { // - newLocation is a workaround for an IE7-9 issue with location.replace and location.href // methods not updating location.href synchronously. // - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172 - return newLocation || location.href.replace(/%27/g,"'"); + if (newLocation) { + return newLocation; + } + if (lastBrowserUrl !== (currentHref = location.href.replace(/%27/g,"'"))) { + hasChangedOutside = true; + } + return currentHref; + } + }; + + /** + * @name $browser#urlChangedOutsideAngular + * + * @description + * GETTER: + * Without any argument, this method just returns current value of urlChangedOutsideAngular. + * + * SETTER: + * With at least one argument, this method sets urlChangedOutsideAngular to new value. + * + * NOTE: this api is intended for use only by the $location service inside of $locationWatch. + * + * @param {boolean} val New value to set as urlChangedOutsideAngular, + * typically used to reset value to false. + */ + self.urlChangedOutsideAngular = function(hasChanged) { + if (isDefined(hasChanged)) { + hasChangedOutside = hasChanged; + return self; + } + else { + return hasChangedOutside; } }; @@ -184,6 +217,7 @@ function Browser(window, document, $log, $sniffer) { function fireUrlChange() { newLocation = null; + hasChangedOutside = false; if (lastBrowserUrl == self.url()) return; lastBrowserUrl = self.url(); diff --git a/src/ng/location.js b/src/ng/location.js index 98dec092dd26..463d8f82fa63 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -738,18 +738,31 @@ function $LocationProvider(){ // update browser var changeCounter = 0; $rootScope.$watch(function $locationWatch() { - var oldUrl = $browser.url(); + var browserUrl = $browser.url(); var currentReplace = $location.$$replace; - if (!changeCounter || oldUrl != $location.absUrl()) { + if (!changeCounter || browserUrl != $location.absUrl()) { changeCounter++; $rootScope.$evalAsync(function() { - if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). + /** + * $browser should detect if url was changed outside of Angular, in which + * case $location should automatically update to the new url. + * + * NOTE: Detecting outside changes to location happens automatically in $browser via + * window events (or polling if events aren't supported), but those methods are + * async. For this reason, $browser.url() will perform a comparison with each call + * to the method as a getter. + */ + if ($browser.urlChangedOutsideAngular()) { + $location.$$parse(browserUrl); + $browser.urlChangedOutsideAngular(false); + } + else if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), browserUrl). defaultPrevented) { - $location.$$parse(oldUrl); + $location.$$parse(browserUrl); } else { $browser.url($location.absUrl(), currentReplace); - afterLocationChange(oldUrl); + afterLocationChange(browserUrl); } }); } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index ba9790539ff8..55feee371d07 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -35,6 +35,7 @@ angular.mock.$Browser = function() { self.$$url = "http://server/"; self.$$lastUrl = self.$$url; // used by url polling fn self.pollFns = []; + self.$$hasChangedOutside = false; // TODO(vojta): remove this temporary api self.$$completeOutstandingRequest = angular.noop; @@ -68,6 +69,16 @@ angular.mock.$Browser = function() { return self.deferredNextId++; }; + self.urlChangedOutsideAngular = function (hasChanged) { + if (isDefined(hasChanged)) { + self.$$hasChangedOutside = hasChanged; + return self; + } + else { + return self.$$hasChangedOutside; + } + }; + /** * @name $browser#defer.now @@ -148,6 +159,10 @@ angular.mock.$Browser.prototype = { return this; } + if (this.$$mockLocation && this.$$mockLocation.href && url !== (this.$$url = this.$$mockLocation.href.replace(/%27/g,"'"))) { + this.hasChangedOutside = true; + } + return this.$$url; }, diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index ff36ce42a888..16623c98773d 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -769,6 +769,29 @@ describe('$location', function() { }); + it('should update location when location changed outside of Angular', function() { + module(function($windowProvider, $locationProvider, $browserProvider) { + $locationProvider.html5Mode(true); + $browserProvider.$get = function($document, $window, $log, $sniffer) { + var b = new Browser($window, $document, $log, $sniffer); + b.pollFns = []; + return b; + }; + }); + + inject(function($rootScope, $browser, $location, $sniffer){ + if ($sniffer.history) { + window.history.replaceState(null, '', '/hello'); + // Verify that infinite digest reported in #6976 no longer occurs + expect(function() { + $rootScope.$digest(); + }).not.toThrow(); + expect($location.path()).toBe('/hello'); + } + }); + }); + + it('should rewrite when hashbang url given', function() { initService(true, '!', true); inject(