Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 8d39bd8

Browse files
fix($browser): handle async updates to location
Both browser reloads and iOS 9 bugs cause the window.location to report a different href that which we have just set. The change does not become available until the next tick. This change generalises previous work to deal with reloads to deal with the iOS 9 bug in the UIWebView component. Closes #12241 Closes #12819
1 parent 472d076 commit 8d39bd8

File tree

2 files changed

+59
-12
lines changed

2 files changed

+59
-12
lines changed

src/ng/browser.js

Lines changed: 11 additions & 6 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -87,7 +87,7 @@ function Browser(window, document, $log, $sniffer) {
87
var cachedState, lastHistoryState,
87
var cachedState, lastHistoryState,
88
lastBrowserUrl = location.href,
88
lastBrowserUrl = location.href,
89
baseElement = document.find('base'),
89
baseElement = document.find('base'),
90-
reloadLocation = null;
90+
pendingLocation = null;
91

91

92
cacheState();
92
cacheState();
93
lastHistoryState = cachedState;
93
lastHistoryState = cachedState;
@@ -147,8 +147,8 @@ function Browser(window, document, $log, $sniffer) {
147
// Do the assignment again so that those two variables are referentially identical.
147
// Do the assignment again so that those two variables are referentially identical.
148
lastHistoryState = cachedState;
148
lastHistoryState = cachedState;
149
} else {
149
} else {
150-
if (!sameBase || reloadLocation) {
150+
if (!sameBase || pendingLocation) {
151-
reloadLocation = url;
151+
pendingLocation = url;
152
}
152
}
153
if (replace) {
153
if (replace) {
154
location.replace(url);
154
location.replace(url);
@@ -157,14 +157,18 @@ function Browser(window, document, $log, $sniffer) {
157
} else {
157
} else {
158
location.hash = getHash(url);
158
location.hash = getHash(url);
159
}
159
}
160+
if (location.href !== url) {
161+
pendingLocation = url;
162+
}
160
}
163
}
161
return self;
164
return self;
162
// getter
165
// getter
163
} else {
166
} else {
164-
// - reloadLocation is needed as browsers don't allow to read out
167+
// - pendingLocation is needed as browsers don't allow to read out
165-
// the new location.href if a reload happened.
168+
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
169+
// https://openradar.appspot.com/22186109).
166
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
170
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
167-
return reloadLocation || location.href.replace(/%27/g,"'");
171+
return pendingLocation || location.href.replace(/%27/g,"'");
168
}
172
}
169
};
173
};
170

174

@@ -186,6 +190,7 @@ function Browser(window, document, $log, $sniffer) {
186
urlChangeInit = false;
190
urlChangeInit = false;
187

191

188
function cacheStateAndFireUrlChange() {
192
function cacheStateAndFireUrlChange() {
193+
pendingLocation = null;
189
cacheState();
194
cacheState();
190
fireUrlChange();
195
fireUrlChange();
191
}
196
}

test/ng/browserSpecs.js

Lines changed: 48 additions & 6 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -12,12 +12,20 @@ function MockWindow(options) {
12
var events = {};
12
var events = {};
13
var timeouts = this.timeouts = [];
13
var timeouts = this.timeouts = [];
14
var locationHref = 'http://server/';
14
var locationHref = 'http://server/';
15+
var committedHref = 'http://server/';
15
var mockWindow = this;
16
var mockWindow = this;
16
var msie = options.msie;
17
var msie = options.msie;
17
var ieState;
18
var ieState;
18

19

19
historyEntriesLength = 1;
20
historyEntriesLength = 1;
20

21

22+
function replaceHash(href, hash) {
23+
// replace the hash with the new one (stripping off a leading hash if there is one)
24+
// See hash setter spec: https://url.spec.whatwg.org/#urlutils-and-urlutilsreadonly-members
25+
return stripHash(href) + '#' + hash.replace(/^#/,'');
26+
}
27+
28+
21
this.setTimeout = function(fn) {
29
this.setTimeout = function(fn) {
22
return timeouts.push(fn) - 1;
30
return timeouts.push(fn) - 1;
23
};
31
};
@@ -46,24 +54,28 @@ function MockWindow(options) {
46

54

47
this.location = {
55
this.location = {
48
get href() {
56
get href() {
49-
return locationHref;
57+
return committedHref;
50
},
58
},
51
set href(value) {
59
set href(value) {
52
locationHref = value;
60
locationHref = value;
53
mockWindow.history.state = null;
61
mockWindow.history.state = null;
54
historyEntriesLength++;
62
historyEntriesLength++;
63+
if (!options.updateAsync) this.flushHref();
55
},
64
},
56
get hash() {
65
get hash() {
57-
return getHash(locationHref);
66+
return getHash(committedHref);
58
},
67
},
59
set hash(value) {
68
set hash(value) {
60-
// replace the hash with the new one (stripping off a leading hash if there is one)
69+
locationHref = replaceHash(locationHref, value);
61-
// See hash setter spec: https://url.spec.whatwg.org/#urlutils-and-urlutilsreadonly-members
70+
if (!options.updateAsync) this.flushHref();
62-
locationHref = stripHash(locationHref) + '#' + value.replace(/^#/,'');
63
},
71
},
64
replace: function(url) {
72
replace: function(url) {
65
locationHref = url;
73
locationHref = url;
66
mockWindow.history.state = null;
74
mockWindow.history.state = null;
75+
if (!options.updateAsync) this.flushHref();
76+
},
77+
flushHref: function() {
78+
committedHref = locationHref;
67
}
79
}
68
};
80
};
69

81

@@ -132,7 +144,7 @@ describe('browser', function() {
132

144

133
logs = {log:[], warn:[], info:[], error:[]};
145
logs = {log:[], warn:[], info:[], error:[]};
134

146

135-
var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
147+
fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
136
warn: function() { logs.warn.push(slice.call(arguments)); },
148
warn: function() { logs.warn.push(slice.call(arguments)); },
137
info: function() { logs.info.push(slice.call(arguments)); },
149
info: function() { logs.info.push(slice.call(arguments)); },
138
error: function() { logs.error.push(slice.call(arguments)); }};
150
error: function() { logs.error.push(slice.call(arguments)); }};
@@ -703,7 +715,11 @@ describe('browser', function() {
703
describe('integration tests with $location', function() {
715
describe('integration tests with $location', function() {
704

716

705
function setup(options) {
717
function setup(options) {
718+
fakeWindow = new MockWindow(options);
719+
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
720+
706
module(function($provide, $locationProvider) {
721
module(function($provide, $locationProvider) {
722+
707
spyOn(fakeWindow.history, 'pushState').andCallFake(function(stateObj, title, newUrl) {
723
spyOn(fakeWindow.history, 'pushState').andCallFake(function(stateObj, title, newUrl) {
708
fakeWindow.location.href = newUrl;
724
fakeWindow.location.href = newUrl;
709
});
725
});
@@ -827,6 +843,32 @@ describe('browser', function() {
827
});
843
});
828

844

829
});
845
});
846+
847+
// issue #12241
848+
it('should not infinite digest if the browser does not synchronously update the location properties', function() {
849+
setup({
850+
history: true,
851+
html5Mode: true,
852+
updateAsync: true // Simulate a browser that doesn't update the href synchronously
853+
});
854+
855+
inject(function($location, $rootScope) {
856+
857+
// Change the hash within Angular and check that we don't infinitely digest
858+
$location.hash('newHash');
859+
expect(function() { $rootScope.$digest(); }).not.toThrow();
860+
expect($location.absUrl()).toEqual('http://server/#newHash');
861+
862+
// Now change the hash from outside Angular and check that $location updates correctly
863+
fakeWindow.location.hash = '#otherHash';
864+
865+
// simulate next tick - since this browser doesn't update synchronously
866+
fakeWindow.location.flushHref();
867+
fakeWindow.fire('hashchange');
868+
869+
expect($location.absUrl()).toEqual('http://server/#otherHash');
870+
});
871+
});
830
});
872
});
831

873

832
describe('integration test with $rootScope', function() {
874
describe('integration test with $rootScope', function() {

0 commit comments

Comments
 (0)