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
18 changes: 11 additions & 7 deletions src/ng/browser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
'use strict';
/* global stripHash: true */
/* global getHash: true, stripHash: false */

function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}

function trimEmptyHash(url) {
return url.replace(/#$/, '');
}

/**
* ! This is a private undocumented service !
Expand Down Expand Up @@ -62,11 +71,6 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {

cacheState();

function getHash(url) {
var index = url.indexOf('#');
return index === -1 ? '' : url.substr(index);
}

/**
* @name $browser#url
*
Expand Down Expand Up @@ -143,7 +147,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 trimEmptyHash(pendingLocation || location.href);
}
};

Expand Down
14 changes: 4 additions & 10 deletions src/ng/location.js
Original file line number Diff line number Diff line change
@@ -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.)


var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
Expand Down Expand Up @@ -94,17 +95,11 @@ function stripBaseUrl(base, url) {
}
}


function stripHash(url) {
var index = url.indexOf('#');
return index === -1 ? url : url.substr(0, index);
}

function trimEmptyHash(url) {
return url.replace(/(#.+)|#$/, '$1');
}


function stripFile(url) {
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
}
Expand Down Expand Up @@ -943,7 +938,7 @@ function $LocationProvider() {


// rewrite hashbang url <> html5 url
if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) {
if ($location.absUrl() !== initialUrl) {
$browser.url($location.absUrl(), true);
}

Expand All @@ -962,7 +957,6 @@ function $LocationProvider() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;

Expand Down Expand Up @@ -990,8 +984,8 @@ function $LocationProvider() {
if (initializing || $location.$$urlUpdatedByLocation) {
$location.$$urlUpdatedByLocation = false;

var oldUrl = trimEmptyHash($browser.url());
var newUrl = trimEmptyHash($location.absUrl());
var oldUrl = $browser.url();
var newUrl = $location.absUrl();
var oldState = $browser.state();
var currentReplace = $location.$$replace;
var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||
Expand Down
3 changes: 2 additions & 1 deletion src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ angular.mock.$Browser.prototype = {
state = null;
}
if (url) {
this.$$url = url;
// The `$browser` service trims empty hashes; simulate it.
this.$$url = url.replace(/#$/, '');
// Native pushState serializes & copies the object; simulate it.
this.$$state = angular.copy(state);
return this;
Expand Down
34 changes: 34 additions & 0 deletions test/ng/browserSpecs.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,14 @@ describe('browser', function() {
expect(browser.url()).toEqual('https://another.com');
});

it('should strip an empty hash fragment', function() {
fakeWindow.location.href = 'http://test.com#';
expect(browser.url()).toEqual('http://test.com');

fakeWindow.location.href = 'https://another.com#foo';
expect(browser.url()).toEqual('https://another.com#foo');
});

it('should use history.pushState when available', function() {
sniffer.history = true;
browser.url('http://new.org');
Expand Down Expand Up @@ -1047,6 +1055,32 @@ describe('browser', function() {
expect($location.absUrl()).toEqual('http://server/#otherHash');
});
});

// issue #16632
it('should not trigger `$locationChangeStart` more than once due to trailing `#`', function() {
setup({
history: true,
html5Mode: true
});

inject(function($flushPendingTasks, $location, $rootScope) {
$rootScope.$digest();

var spy = jasmine.createSpy('$locationChangeStart');
$rootScope.$on('$locationChangeStart', spy);

$rootScope.$evalAsync(function() {
fakeWindow.location.href += '#';
});
$rootScope.$digest();

expect(fakeWindow.location.href).toBe('http://server/#');
expect($location.absUrl()).toBe('http://server/');

expect(spy.calls.count()).toBe(0);
expect(spy).not.toHaveBeenCalled();
});
});
});

describe('integration test with $rootScope', function() {
Expand Down
13 changes: 7 additions & 6 deletions test/ng/locationSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -693,10 +693,10 @@ describe('$location', function() {

describe('location watch', function() {

it('should not update browser if only the empty hash fragment is cleared by updating the search', function() {
it('should not update browser if only the empty hash fragment is cleared', function() {
initService({supportHistory: true});
mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'});
inject(function($rootScope, $browser, $location) {
mockUpBrowser({initialUrl: 'http://new.com/a/b#', baseHref: '/base/'});
inject(function($browser, $rootScope) {
$browser.url('http://new.com/a/b');
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
$rootScope.$digest();
Expand All @@ -707,10 +707,11 @@ describe('$location', function() {

it('should not replace browser url if only the empty hash fragment is cleared', function() {
initService({html5Mode: true, supportHistory: true});
mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'});
inject(function($browser, $location) {
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.

expect($location.absUrl()).toBe('http://new.com/');
expect($window.location.href).toBe('http://new.com/#');
});
});

Expand Down