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

Commit 2963115

Browse files
committed
feat($location): add support for state and title in pushState
Adds $location pushState & replaceState methods acting as proxies to history pushState & replaceState methods. This allows using pushState to change state and title as well as URL. Note that these methods are not compatible with browsers not supporting the HTML5 History API, e.g. IE 9. Closes #9027
1 parent 3686f45 commit 2963115

File tree

7 files changed

+233
-36
lines changed

7 files changed

+233
-36
lines changed

src/ng/browser.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ function Browser(window, document, $log, $sniffer) {
123123
//////////////////////////////////////////////////////////////
124124

125125
var lastBrowserUrl = location.href,
126+
lastBrowserState = history.state,
126127
baseElement = document.find('base'),
127128
newLocation = null;
128129

@@ -143,23 +144,31 @@ function Browser(window, document, $log, $sniffer) {
143144
* {@link ng.$location $location service} to change url.
144145
*
145146
* @param {string} url New url (when used as setter)
146-
* @param {boolean=} replace Should new url replace current history record ?
147+
* @param {boolean=} replace Should new url replace current history record?
148+
* @param {object=} state object to use with pushState/replaceState
149+
* @param {string=} title to use with pushState/replaceState
147150
*/
148-
self.url = function(url, replace) {
151+
self.url = function(url, replace, state, title) {
149152
// Android Browser BFCache causes location, history reference to become stale.
150153
if (location !== window.location) location = window.location;
151154
if (history !== window.history) history = window.history;
152155

153156
// setter
154157
if (url) {
155-
if (lastBrowserUrl == url) return;
158+
if (lastBrowserUrl == url && (!$sniffer.history || equals(lastBrowserState, state))) {
159+
return;
160+
}
156161
lastBrowserUrl = url;
162+
lastBrowserState = copy(state);
157163
if ($sniffer.history) {
158-
if (replace) history.replaceState(null, '', url);
159-
else {
160-
history.pushState(null, '', url);
161-
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
162-
baseElement.attr('href', baseElement.attr('href'));
164+
title = title || '';
165+
state = state || null;
166+
try {
167+
history[replace ? 'replaceState' : 'pushState'](state, title, url);
168+
} catch (e) {
169+
throw minErr('$browser')('pstterr', 'pushState or replaceState failed; ' +
170+
'most likely the passed state object is too complex',
171+
e.stack || e.message || e);
163172
}
164173
} else {
165174
newLocation = url;
@@ -188,7 +197,7 @@ function Browser(window, document, $log, $sniffer) {
188197

189198
lastBrowserUrl = self.url();
190199
forEach(urlChangeListeners, function(listener) {
191-
listener(self.url());
200+
listener(self.url(), history.state, rawDocument.title);
192201
});
193202
}
194203

src/ng/location.js

+108-12
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,100 @@ LocationHashbangInHtml5Url.prototype =
530528
}
531529
};
532530

531+
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) {
532+
extend(Location.prototype = {}, locationPrototype);
533+
});
534+
535+
/**
536+
* @name $location#$$state
537+
*
538+
* Current History API state.
539+
* @private
540+
*/
541+
LocationHtml5Url.prototype.$$state = null;
542+
543+
/**
544+
* @name $location#$$title
545+
*
546+
* Current History API title.
547+
* @private
548+
*/
549+
LocationHtml5Url.prototype.$$title = '';
550+
551+
/**
552+
* @ngdoc method
553+
* @name $location#state.
554+
*
555+
* @description
556+
* Return current history state.
557+
*
558+
* NOTE: this method is available only in HTML5 mode and only in browsers supporting
559+
* HTML5 History API. If you need to support older browsers (like IE9), don't use this
560+
* method!
561+
*
562+
* @return {object} state
563+
*/
564+
LocationHtml5Url.prototype.state = function() {
565+
return this.$$state;
566+
};
567+
568+
/**
569+
* @ngdoc method
570+
* @name $location#pushState
571+
*
572+
* @description
573+
* Invokes history.pushState. Changes url, state and title.
574+
*
575+
* NOTE: this method is available only in HTML5 mode and only in browsers supporting
576+
* HTML5 History API. If you need to support older browsers (like IE9), don't use this
577+
* method!
578+
*
579+
* @param {object=} state object for pushState
580+
* @param {string=} title for pushState (ignored by most browsers)
581+
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
582+
* @return {object} $location
583+
*/
584+
LocationHtml5Url.prototype.pushState = function(state, title, url, replace) {
585+
this.$$state = copy(state);
586+
this.$$title = title;
587+
this.$$replace = replace;
588+
this.url(url, replace);
589+
return this;
590+
};
591+
592+
/**
593+
* @ngdoc method
594+
* @name $location#replaceState.
595+
*
596+
* @description
597+
* Invokes history.replaceState. Changes url, state and title.
598+
*
599+
* NOTE: this method is available only in HTML5 mode and only in browsers supporting
600+
* HTML5 History API. If you need to support older browsers (like IE9), don't use this
601+
* method!
602+
*
603+
* @param {object=} state object for replaceState
604+
* @param {string=} title for replaceState (ignored by most browsers)
605+
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
606+
* @return {object} $location
607+
*/
608+
LocationHtml5Url.prototype.replaceState = function(state, title, url) {
609+
this.pushState(state, title, url, true);
610+
return this;
611+
};
612+
613+
614+
LocationHashbangInHtml5Url.prototype.state =
615+
LocationHashbangInHtml5Url.prototype.pushState =
616+
LocationHashbangInHtml5Url.prototype.replaceState =
617+
LocationHashbangUrl.prototype.state =
618+
LocationHashbangUrl.prototype.pushState =
619+
LocationHashbangUrl.prototype.replaceState = function() {
620+
throw $locationMinErr('psthtml4', 'History API state-related methods are available only ' +
621+
'in HTML5 mode and only in browsers supporting HTML5 History API');
622+
};
623+
624+
533625
function locationGetter(property) {
534626
return function() {
535627
return this[property];
@@ -717,7 +809,7 @@ function $LocationProvider(){
717809
}
718810

719811
// update $location when $browser url changes
720-
$browser.onUrlChange(function(newUrl) {
812+
$browser.onUrlChange(function(newUrl, state, title) {
721813
if ($location.absUrl() != newUrl) {
722814
$rootScope.$evalAsync(function() {
723815
var oldUrl = $location.absUrl();
@@ -726,9 +818,9 @@ function $LocationProvider(){
726818
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
727819
oldUrl).defaultPrevented) {
728820
$location.$$parse(oldUrl);
729-
$browser.url(oldUrl);
821+
$browser.url($location.absUrl(), false, state, title);
730822
} else {
731-
afterLocationChange(oldUrl);
823+
afterLocationChange(oldUrl, state, title);
732824
}
733825
});
734826
if (!$rootScope.$$phase) $rootScope.$digest();
@@ -740,28 +832,32 @@ function $LocationProvider(){
740832
$rootScope.$watch(function $locationWatch() {
741833
var oldUrl = $browser.url();
742834
var currentReplace = $location.$$replace;
835+
var currentState = $location.$$state;
836+
var currentTitle = $location.$$title;
743837

744838
if (!changeCounter || oldUrl != $location.absUrl()) {
745839
changeCounter++;
746840
$rootScope.$evalAsync(function() {
747-
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
841+
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, currentState, currentTitle).
748842
defaultPrevented) {
749843
$location.$$parse(oldUrl);
750844
} else {
751-
$browser.url($location.absUrl(), currentReplace);
752-
afterLocationChange(oldUrl);
845+
$browser.url($location.absUrl(), currentReplace, currentState, currentTitle);
846+
afterLocationChange(oldUrl, currentState, currentTitle);
753847
}
754848
});
755849
}
756850
$location.$$replace = false;
851+
delete $location.$$state;
852+
delete $location.$$title;
757853

758854
return changeCounter;
759855
});
760856

761857
return $location;
762858

763-
function afterLocationChange(oldUrl) {
764-
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
859+
function afterLocationChange(oldUrl, state, title) {
860+
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, state, title);
765861
}
766862
}];
767863
}

src/ngMock/angular-mocks.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,11 @@ angular.mock.$Browser.prototype = {
144144
return pollFn;
145145
},
146146

147-
url: function(url, replace) {
147+
url: function(url, replace, state, title) {
148148
if (url) {
149149
this.$$url = url;
150+
this.$$state = state;
151+
this.$$title = title;
150152
return this;
151153
}
152154

0 commit comments

Comments
 (0)