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

fix($location): avoid unnecessary $locationChange* events due to empty hash #16636

Closed
wants to merge 9 commits into from

Conversation

gkalpak
Copy link
Member

@gkalpak gkalpak commented Jul 19, 2018

What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Fix (among others).

What is the current behavior? (You can also link to an open issue here)
#16632

What is the new behavior (if this is a feature change)?
No unnecessary $locationChange* events due to empty hash

Does this PR introduce a breaking change?
Hopefully not 😁

Please check if the PR fulfills these requirements

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

@googlebot
Copy link

☹️ Sorry, but only Googlers may change the label cla: yes.

1 similar comment
@googlebot
Copy link

☹️ Sorry, but only Googlers may change the label cla: yes.

@@ -143,7 +143,7 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
// - pendingLocation is needed as browsers don't allow to read out
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
// https://openradar.appspot.com/22186109).
return pendingLocation || location.href;
return pendingLocation || trimEmptyHash(location.href);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about pendingLocation?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pendingLocation always has the empty hash fragment stripped. In a nutshell, pendingLocation is always the value $browser.url(url) is called with, which is always $location.absUrl() (directly or indirectly), which always has the empty hash stripped off.

This potential difference between pendingLocation and location.href is what causes the issue. Because while pendingLocation is defined, $browser.url() return the url without the empty hash, but once pendingLocation gets undefined (i.e. on hashchange, popstate, which may be triggered asynchronously), then $browser.url() starts returning the url with the empty fragment.

So, the returned url changes, even if there is no actual change in the url.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This potential difference between pendingLocation and location.href is what causes the issue

Aha, that helps me understand this a bit more. Why does location.href have the hash in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what browsers do (sometimes 😁). I found that this can happen when clicking a link with a trailing # (but not when directly setting location.hash).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd vote to still do trimEmptyHash(pendingLocation || location.href) just for consistency...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@jbedard
Copy link
Collaborator

jbedard commented Jul 19, 2018

Where is $browser.url() being used to cause this bug? I question if we should fix it there instead similar to aee7d53

@gkalpak
Copy link
Member Author

gkalpak commented Jul 19, 2018

Where is $browser.url() being used to cause this bug? I question if we should fix it there instead similar to aee7d53

This is one place. There may be others.
Note that is many places we trim the empty hash before using the returned url. (If we decide this fix is desired, I will remove those unnecessary calls.)

Also, this looks a little problematic:

$rootScope.$evalAsync(function() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
newState, oldState).defaultPrevented;

Although we don't trim the empty hash for determining there's been a url change, we trim it before broadcasting the event. So, you will get a $locationChangeStart event from /foo/ to /foo/ 😱

In general, I prefer to address the root of the problem. Patching stuff "just-in-time" to avoid a specific bug makes the code too complicated and hard to follow.

(But I understand $location/$browser might a special case.)

@jbedard
Copy link
Collaborator

jbedard commented Jul 19, 2018

While patching stuff "just-in-time" can be iffy, modifying something generic like $browser.url() for a specific case that might not apply everywhere is also iffy. I think that was one reason we chose not to modify $browser.url() in aee7d53?

For the $locationChangeStart newUrl thing... can we just drop the newUrl = trimEmptyHash(newUrl); line?

@gkalpak
Copy link
Member Author

gkalpak commented Jul 19, 2018

modifying something generic like $browser.url() for a specific case that might not apply everywhere is also iffy.

My point is that the proposed change does apply everywhere and does not change the behavior of $browser other than not incorrectly reporting URL changes.

But I will understand if you feel uncomfortable with changing .url() 😃

@jbedard
Copy link
Collaborator

jbedard commented Jul 19, 2018

But is adding/removing # a URL change? The browser thinks it is... 🤔

@gkalpak
Copy link
Member Author

gkalpak commented Jul 19, 2018

I think you already found #16636 (comment), which (kind of) explains that it's not the browser that thinks there's a change, it's $browser.url() that temporarily thinks so (while pendingLocation is set).

@jbedard
Copy link
Collaborator

jbedard commented Jul 19, 2018

I mean if you append # to the end of a URL and press enter, the browser will push a new entry into the history. So normally browsers do consider / and /# to be different URLs.

I guess I just want to acknowledge that the browser does consider them different, it's only us/$location that treats them the same. IDK if that actually means anything though >_>

I'm curious what other code could be simplified with the current fix... and how the $locationChangeStart event can be fixed. Want to go ahead with this as-is for now? I'm still tempted to try the other method and compare though...

@@ -93,7 +94,7 @@ function stripHash(url) {
}

function trimEmptyHash(url) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove some calls to this within this file now that $browser does it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Absolutely!

Note that is many places we trim the empty hash before using the returned url. (If we decide this fix is desired, I will remove those unnecessary calls.)

@gkalpak gkalpak force-pushed the fix-location-on-empty-hash branch 2 times, most recently from 000489f to efb1706 Compare July 25, 2018 19:49
@@ -1,5 +1,14 @@
'use strict';
/* global stripHash: true */
/* global getHash: true, stripHash: false, trimEmptyHash: true */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is getHash used globally?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is "used" in tests, in a mock window. The feature that relies on it is not actually used atm, so there were no breakages.

See 34fe24f (yes, there is a typo in that commit message 😁).

@@ -1,4 +1,5 @@
'use strict';
/* global stripHash: true */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could move stripHash global into browser.js as well for consistency. Otherwise you have two way importing going on.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stripHash is used more in $location than $browser and other similar helpers (e.g. stripBaseUrl, stripFile) are also in $location. I only moved trimEmptyHash, because it is only used in $browser now.

Even if we had imports/exports (which we don't because everything lives inside the great AngularJS IIFE 😁), I don't see two way importing going on. $location would export stripHash and $browser would import stripHash and export getHash and trimEmptyHash.

(We definitely have enough URL-specific helpers that we could move them into a separate file and "import" from there 😁)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a little weird having $browser depend on $location. Or was that always the case?

Copy link
Member Author

@gkalpak gkalpak Jul 30, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know about "always", but it definitely was the case before this PR 😛
(And still is. This PR does not change anything in that regard.)

Copy link
Contributor

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of minor comments.

@gkalpak gkalpak force-pushed the fix-location-on-empty-hash branch from af2da25 to cdd1f42 Compare July 26, 2018 07:58
expect($browser.url()).toBe('http://new.com/#');
mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'});
inject(function($browser, $location, $window) {
expect($browser.url()).toBe('http://new.com/');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The (non-public) docs for browser.url() say "Without any argument, this method just returns current value of location.href." Isn't this now wrong as the test shows a difference between url() and location.href?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@gkalpak gkalpak closed this in 864c7f0 Jul 30, 2018
gkalpak added a commit that referenced this pull request Jul 30, 2018
@gkalpak gkalpak deleted the fix-location-on-empty-hash branch July 30, 2018 20:41
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants