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

Commit b8eded2

Browse files
committed
feat($location): add support for History API state handling
Adds $location state method allowing to get/set a History API state via pushState & replaceState methods. Note that: - Angular treats states undefined and null as the same; trying to change one to the other without touching the URL won't do anything. This is necessary to prevent infinite digest loops when setting the URL to itself in IE<10 in the HTML5 hash fallback mode. - The state() method is not compatible with browsers not supporting the HTML5 History API, e.g. IE 9 or Android < 4.0. Closes #9027
1 parent 7cb01a8 commit b8eded2

File tree

8 files changed

+331
-47
lines changed

8 files changed

+331
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@ngdoc error
2+
@name $location:nostate
3+
@fullName History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API
4+
@description
5+
6+
This error occurs when the {@link ng.$location#state $location.state} method is used when {@link ng.$locationProvider#html5Mode $locationProvider.html5Mode} is not turned on or the browser used doesn't support the HTML5 History API (for example, IE9 or Android 2.3).
7+
8+
To avoid this error, either drop support for those older browsers or avoid using this method.

src/ng/browser.js

+31-10
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ function Browser(window, document, $log, $sniffer) {
124124
//////////////////////////////////////////////////////////////
125125

126126
var lastBrowserUrl = location.href,
127+
lastHistoryState = history.state,
127128
baseElement = document.find('base'),
128129
newLocation = null;
129130

@@ -144,27 +145,31 @@ function Browser(window, document, $log, $sniffer) {
144145
* {@link ng.$location $location service} to change url.
145146
*
146147
* @param {string} url New url (when used as setter)
147-
* @param {boolean=} replace Should new url replace current history record ?
148+
* @param {boolean=} replace Should new url replace current history record?
149+
* @param {object=} state object to use with pushState/replaceState
148150
*/
149-
self.url = function(url, replace) {
151+
self.url = function(url, replace, state) {
150152
// Android Browser BFCache causes location, history reference to become stale.
151153
if (location !== window.location) location = window.location;
152154
if (history !== window.history) history = window.history;
153155

154156
// setter
155157
if (url) {
156-
if (lastBrowserUrl == url) return;
158+
// Prevent IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
159+
// See https://github.com/angular/angular.js/commit/ffb2701
160+
if (lastBrowserUrl === url && history.state == null && state == null) return;
161+
if (isUndefined(state)) {
162+
state = null;
163+
}
157164
var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
158165
lastBrowserUrl = url;
159166
// Don't use history API if only the hash changed
160167
// due to a bug in IE10/IE11 which leads
161168
// to not firing a `hashchange` nor `popstate` event
162169
// in some cases (see #9143).
163-
if (!sameBase && $sniffer.history) {
164-
if (replace) history.replaceState(null, '', url);
165-
else {
166-
history.pushState(null, '', url);
167-
}
170+
if ($sniffer.history && (!sameBase || state !== null)) {
171+
history[replace ? 'replaceState' : 'pushState'](state, '', url);
172+
lastHistoryState = history.state;
168173
} else {
169174
newLocation = url;
170175
if (replace) {
@@ -183,6 +188,20 @@ function Browser(window, document, $log, $sniffer) {
183188
}
184189
};
185190

191+
/**
192+
* @name $browser#state
193+
*
194+
* @description
195+
* This method is a getter.
196+
*
197+
* Return history.state.
198+
*
199+
* @returns {object} state
200+
*/
201+
self.state = function() {
202+
return history.state;
203+
};
204+
186205
var urlChangeListeners = [],
187206
urlChangeInit = false;
188207

@@ -192,11 +211,13 @@ function Browser(window, document, $log, $sniffer) {
192211
}
193212

194213
function checkUrlChange() {
195-
if (lastBrowserUrl == self.url()) return;
214+
if (lastBrowserUrl === self.url() && lastHistoryState === history.state) {
215+
return;
216+
}
196217

197218
lastBrowserUrl = self.url();
198219
forEach(urlChangeListeners, function(listener) {
199-
listener(self.url());
220+
listener(self.url(), history.state);
200221
});
201222
}
202223

src/ng/location.js

+66-17
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
303303
}
304304

305305

306-
LocationHashbangInHtml5Url.prototype =
307-
LocationHashbangUrl.prototype =
308-
LocationHtml5Url.prototype = {
306+
var locationPrototype = {
309307

310308
/**
311309
* Are we in html5 mode?
@@ -314,7 +312,7 @@ LocationHashbangInHtml5Url.prototype =
314312
$$html5: false,
315313

316314
/**
317-
* Has any change been replacing ?
315+
* Has any change been replacing?
318316
* @private
319317
*/
320318
$$replace: false,
@@ -530,6 +528,43 @@ LocationHashbangInHtml5Url.prototype =
530528
}
531529
};
532530

531+
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) {
532+
Location.prototype = Object.create(locationPrototype);
533+
534+
/**
535+
* @ngdoc method
536+
* @name $location#state
537+
*
538+
* @description
539+
* This method is getter / setter.
540+
*
541+
* Return the history state object when called without any parameter.
542+
*
543+
* Change the history state object when called with one parameter and return `$location`.
544+
* The state object is later passed to `pushState` or `replaceState`.
545+
*
546+
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting
547+
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
548+
* older browsers (like IE9 or Android < 4.0), don't use this method.
549+
*
550+
* @param {object=} state State object for pushState or replaceState
551+
* @return {object} state
552+
*/
553+
Location.prototype.state = function(state) {
554+
if (!arguments.length)
555+
return this.$$state;
556+
557+
if (Location !== LocationHtml5Url || !this.$$html5) {
558+
throw $locationMinErr('nostate', 'History API state support is available only ' +
559+
'in HTML5 mode and only in browsers supporting HTML5 History API');
560+
}
561+
this.$$state = copy(state);
562+
563+
return this;
564+
};
565+
});
566+
567+
533568
function locationGetter(property) {
534569
return function() {
535570
return this[property];
@@ -649,9 +684,14 @@ function $LocationProvider(){
649684
* details about event object. Upon successful change
650685
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
651686
*
687+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
688+
* the browser supports the HTML5 History API.
689+
*
652690
* @param {Object} angularEvent Synthetic event object.
653691
* @param {string} newUrl New URL
654692
* @param {string=} oldUrl URL that was before it was changed.
693+
* @param {string=} newState New history state object
694+
* @param {string=} oldState History state object that was before it was changed.
655695
*/
656696

657697
/**
@@ -661,9 +701,14 @@ function $LocationProvider(){
661701
* @description
662702
* Broadcasted after a URL was changed.
663703
*
704+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
705+
* the browser supports the HTML5 History API.
706+
*
664707
* @param {Object} angularEvent Synthetic event object.
665708
* @param {string} newUrl New URL
666709
* @param {string=} oldUrl URL that was before it was changed.
710+
* @param {string=} newState New history state object
711+
* @param {string=} oldState History state object that was before it was changed.
667712
*/
668713

669714
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
@@ -741,18 +786,20 @@ function $LocationProvider(){
741786
}
742787

743788
// update $location when $browser url changes
744-
$browser.onUrlChange(function(newUrl) {
745-
if ($location.absUrl() != newUrl) {
789+
$browser.onUrlChange(function(newUrl, newState) {
790+
if ($location.absUrl() !== newUrl ||
791+
($location.$$html5 && $location.state() != newState && !equals($location.state(), newState))) {
746792
$rootScope.$evalAsync(function() {
747793
var oldUrl = $location.absUrl();
794+
var oldState = $location.state();
748795

749796
$location.$$parse(newUrl);
750-
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
751-
oldUrl).defaultPrevented) {
797+
if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
798+
newState, oldState).defaultPrevented) {
752799
$location.$$parse(oldUrl);
753-
$browser.url(oldUrl);
800+
$browser.url($location.absUrl(), false, newState);
754801
} else {
755-
afterLocationChange(oldUrl);
802+
afterLocationChange(oldUrl, oldState);
756803
}
757804
});
758805
if (!$rootScope.$$phase) $rootScope.$digest();
@@ -763,17 +810,19 @@ function $LocationProvider(){
763810
var changeCounter = 0;
764811
$rootScope.$watch(function $locationWatch() {
765812
var oldUrl = $browser.url();
813+
var oldState = $browser.state();
766814
var currentReplace = $location.$$replace;
767815

768-
if (!changeCounter || oldUrl != $location.absUrl()) {
816+
if (!changeCounter || oldUrl !== $location.absUrl() ||
817+
($location.$$html5 && oldState != $location.state() && !equals(oldState, $location.state()))) {
769818
changeCounter++;
770819
$rootScope.$evalAsync(function() {
771-
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
772-
defaultPrevented) {
820+
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl,
821+
$location.state(), oldState).defaultPrevented) {
773822
$location.$$parse(oldUrl);
774823
} else {
775-
$browser.url($location.absUrl(), currentReplace);
776-
afterLocationChange(oldUrl);
824+
$browser.url($location.absUrl(), currentReplace, $location.state());
825+
afterLocationChange(oldUrl, oldState);
777826
}
778827
});
779828
}
@@ -784,8 +833,8 @@ function $LocationProvider(){
784833

785834
return $location;
786835

787-
function afterLocationChange(oldUrl) {
788-
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
836+
function afterLocationChange(oldUrl, oldState) {
837+
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, $location.state(), oldState);
789838
}
790839
}];
791840
}

src/ngMock/angular-mocks.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ angular.mock.$Browser = function() {
4646
self.onUrlChange = function(listener) {
4747
self.pollFns.push(
4848
function() {
49-
if (self.$$lastUrl != self.$$url) {
49+
if (self.$$lastUrl != self.$$url || self.$$state != self.$$lastState) {
5050
self.$$lastUrl = self.$$url;
51-
listener(self.$$url);
51+
self.$$lastState = self.$$state;
52+
listener(self.$$url, self.$$state);
5253
}
5354
}
5455
);
@@ -144,15 +145,20 @@ angular.mock.$Browser.prototype = {
144145
return pollFn;
145146
},
146147

147-
url: function(url, replace) {
148+
url: function(url, replace, state) {
148149
if (url) {
149150
this.$$url = url;
151+
this.$$state = state;
150152
return this;
151153
}
152154

153155
return this.$$url;
154156
},
155157

158+
state: function() {
159+
return this.$$state;
160+
},
161+
156162
cookies: function(name, value) {
157163
if (name) {
158164
if (angular.isUndefined(value)) {

0 commit comments

Comments
 (0)