-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix($location): correctly handle external URL change during $digest
#15561
Conversation
Previously, `$browser.$$checkUrlChange()` (which was run before each `$digest`) would only detect an external change (i.e. not via `$location`) to the browser URL. External changes to `history.state` would not be detected and propagated to `$location`. This would not be a problem if changes were followed by a `popstate` or `hashchange` event (which would call `cacheStateAndFireUrlChange()`). But since `history.pushState()/replaceState()` do not fire any events, calling these methods manually would result in `$location` getting out-of-sync with the actual history state. This was not detected in tests, because the mocked `window.history` would incorrectly trigger `popstate` when calling `pushState()/replaceState()`, which "covered" the bug. This commit fixes it by always calling `cacheState()`, before looking for and propagating a URL/state change.
2e7a128
to
2bda69c
Compare
Previously, when the URL was changed directly (e.g. via `location.href`) during a `$digest` (e.g. via `scope.$evalAsync()` or `promise.then()`) the change was not handled correctly, unless a `popstate` or `hashchange` event was fired synchronously. This was an issue when calling `history.pushState()/replaceState()` in all browsers, since these methods do not emit any event. This was also an issue when setting `location.href` in IE11, where (unlike other browsers) no `popstate` event is fired at all for hash-only changes ([known bug][1]) and the `hashchange` event is fired asynchronously (which is too late). This commit fixes both usecases by: 1. Keeping track of `$location` setter methods being called and only processing a URL change if it originated from such a call. If there is a URL difference but no setter method has been called, this means that the browser URL/history has been updated directly and the change hasn't yet been propagated to `$location` (e.g. due to no event being fired synchronously or at all). 2. Checking for URL/state changes at the end of the `$digest`, in order to detect changes via `history` methods (that took place during the `$digest`). [1]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3740423/ Fixes angular#11075 Fixes angular#12571 Fixes angular#15556
2bda69c
to
c0a874e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This sounds like a reasonable change.
The fact that the tests were artificially hiding the real behaviour was worrying (probably my fault).
Are we concerned that we now need a digest for the location to be updated - or was it the case that this was just broken completely previously, so waiting for a digest is a step forward?
Wow, that was quick 😃
The latter. I am a (tiny) little concerned that people might somehow rely on |
I don't that is a breaking change. It was not documented to do that and it is very reasonable that the previous functionality is wrong. |
Previously, when the URL was changed directly (e.g. via `location.href`) during a `$digest` (e.g. via `scope.$evalAsync()` or `promise.then()`) the change was not handled correctly, unless a `popstate` or `hashchange` event was fired synchronously. This was an issue when calling `history.pushState()/replaceState()` in all browsers, since these methods do not emit any event. This was also an issue when setting `location.href` in IE11, where (unlike other browsers) no `popstate` event is fired at all for hash-only changes ([known bug][1]) and the `hashchange` event is fired asynchronously (which is too late). This commit fixes both usecases by: 1. Keeping track of `$location` setter methods being called and only processing a URL change if it originated from such a call. If there is a URL difference but no setter method has been called, this means that the browser URL/history has been updated directly and the change hasn't yet been propagated to `$location` (e.g. due to no event being fired synchronously or at all). 2. Checking for URL/state changes at the end of the `$digest`, in order to detect changes via `history` methods (that took place during the `$digest`). [1]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3740423/ Fixes #11075 Fixes #12571 Fixes #15556 Closes #15561
I've merged this into master and v1.6.x. I thought it might be too much for v1.5.x (considering it is a non-trivial change/fix), but could be convinced otherwise 😃 |
Previously, when the URL was changed directly (e.g. via `location.href`) during a `$digest` (e.g. via `scope.$evalAsync()` or `promise.then()`) the change was not handled correctly, unless a `popstate` or `hashchange` event was fired synchronously. This was an issue when calling `history.pushState()/replaceState()` in all browsers, since these methods do not emit any event. This was also an issue when setting `location.href` in IE11, where (unlike other browsers) no `popstate` event is fired at all for hash-only changes ([known bug][1]) and the `hashchange` event is fired asynchronously (which is too late). This commit fixes both usecases by: 1. Keeping track of `$location` setter methods being called and only processing a URL change if it originated from such a call. If there is a URL difference but no setter method has been called, this means that the browser URL/history has been updated directly and the change hasn't yet been propagated to `$location` (e.g. due to no event being fired synchronously or at all). 2. Checking for URL/state changes at the end of the `$digest`, in order to detect changes via `history` methods (that took place during the `$digest`). [1]: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/3740423/ Fixes angular#11075 Fixes angular#12571 Fixes angular#15556 Closes angular#15561
You were right! :) I was relying on this behavior to ensure that I could hit the With this patch now (in Angular 1.6) route changes are triggered after you push the custom history state -- and if you try and cancel them via |
@dlongley - sorry about this. If you don't find a workaround please post again and we can see what we can do to fix it. |
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Bug fix(es).
What is the current behavior? (You can also link to an open issue here)
$browser.$$checkUrlChange()
(which was run before each$digest
) will only detect an external change (i.e. not via$location
) to the browser URL. External changes tohistory.state
will not be detected and propagated to$location
.This would not be a problem if changes were followed by a
popstate
orhashchange
event (which would callcacheStateAndFireUrlChange()
). But sincehistory.pushState()/replaceState()
do not fire any events, calling these methods manually will result in$location
getting out-of-sync with the actualhistory state.
This is not detected in tests, because the mocked
window.history
will incorrectly triggerpopstate
when callingpushState()/replaceState()
, which "covers" the bug.When the URL is changed directly (e.g. via
location.href
) during a$digest
(e.g. viascope.$evalAsync()
orpromise.then()
) the change is not handled correctly, unless apopstate
orhashchange
event is fired synchronously.This is an issue when calling
history.pushState()/replaceState()
in all browsers, since these methods do not emit any event. This is also an issue when settinglocation.href
in IE11, where (unlike other browsers) nopopstate
event is fired at all for hash-only changes (known bug) and thehashchange
event is fired asynchronously (which is too late).What is the new behavior (if this is a feature change)?
The first bug is fixed by ensuring
cacheState()
is always called, before looking for and propagating a URL/state change.The second bug(s) is fixed by:
$location
setter methods being called and only processing a URL change if it originated from such a call. If there is a URL difference but no setter method has been called, this means that the browser URL/history has been updated directly and the change hasn't yet been propagated to$location
(e.g. due to no event being fired synchronously or at all).$digest
, in order to detect changes viahistory
methods (that took place during the$digest
).Does this PR introduce a breaking change?
Who kNOws?
(This PR fixes some uncommon usecases, but I am not 100% sure it was not possible to somehow "exploit" the bugs to support a usecase that is now broken.)
Please check if the PR fulfills these requirements
Docs have been added / updated (for bug fixes / features)Other information:
Fixes #11075, #12571 and #15556.