This repository was archived by the owner on Apr 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($location): add support for History API state handling #9027
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
@ngdoc error | ||
@name $location:nostate | ||
@fullName History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API | ||
@description | ||
|
||
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). | ||
|
||
To avoid this error, either drop support for those older browsers or avoid using this method. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -303,9 +303,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) { | |
} | ||
|
||
|
||
LocationHashbangInHtml5Url.prototype = | ||
LocationHashbangUrl.prototype = | ||
LocationHtml5Url.prototype = { | ||
var locationPrototype = { | ||
|
||
/** | ||
* Are we in html5 mode? | ||
|
@@ -314,7 +312,7 @@ LocationHashbangInHtml5Url.prototype = | |
$$html5: false, | ||
|
||
/** | ||
* Has any change been replacing ? | ||
* Has any change been replacing? | ||
* @private | ||
*/ | ||
$$replace: false, | ||
|
@@ -530,6 +528,46 @@ LocationHashbangInHtml5Url.prototype = | |
} | ||
}; | ||
|
||
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) { | ||
Location.prototype = Object.create(locationPrototype); | ||
|
||
/** | ||
* @ngdoc method | ||
* @name $location#state | ||
* | ||
* @description | ||
* This method is getter / setter. | ||
* | ||
* Return the history state object when called without any parameter. | ||
* | ||
* Change the history state object when called with one parameter and return `$location`. | ||
* The state object is later passed to `pushState` or `replaceState`. | ||
* | ||
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting | ||
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support | ||
* older browsers (like IE9 or Android < 4.0), don't use this method. | ||
* | ||
* @param {object=} state State object for pushState or replaceState | ||
* @return {object} state | ||
*/ | ||
Location.prototype.state = function(state) { | ||
if (!arguments.length) | ||
return this.$$state; | ||
|
||
if (Location !== LocationHtml5Url || !this.$$html5) { | ||
throw $locationMinErr('nostate', 'History API state support is available only ' + | ||
'in HTML5 mode and only in browsers supporting HTML5 History API'); | ||
} | ||
// The user might modify `stateObject` after invoking `$location.state(stateObject)` | ||
// but we're changing the $$state reference to $browser.state() during the $digest | ||
// so the modification window is narrow. | ||
this.$$state = isUndefined(state) ? null : state; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so you decided not to copy after all? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought that's what you wanted? :) I even added a test confirming state can be modified before digest but not after that. I don't care a lot either way, whatever you prefer. |
||
|
||
return this; | ||
}; | ||
}); | ||
|
||
|
||
function locationGetter(property) { | ||
return function() { | ||
return this[property]; | ||
|
@@ -649,9 +687,14 @@ function $LocationProvider(){ | |
* details about event object. Upon successful change | ||
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired. | ||
* | ||
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when | ||
* the browser supports the HTML5 History API. | ||
* | ||
* @param {Object} angularEvent Synthetic event object. | ||
* @param {string} newUrl New URL | ||
* @param {string=} oldUrl URL that was before it was changed. | ||
* @param {string=} newState New history state object | ||
* @param {string=} oldState History state object that was before it was changed. | ||
*/ | ||
|
||
/** | ||
|
@@ -661,9 +704,14 @@ function $LocationProvider(){ | |
* @description | ||
* Broadcasted after a URL was changed. | ||
* | ||
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when | ||
* the browser supports the HTML5 History API. | ||
* | ||
* @param {Object} angularEvent Synthetic event object. | ||
* @param {string} newUrl New URL | ||
* @param {string=} oldUrl URL that was before it was changed. | ||
* @param {string=} newState New history state object | ||
* @param {string=} oldState History state object that was before it was changed. | ||
*/ | ||
|
||
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement', | ||
|
@@ -688,8 +736,29 @@ function $LocationProvider(){ | |
$location = new LocationMode(appBase, '#' + hashPrefix); | ||
$location.$$parseLinkUrl(initialUrl, initialUrl); | ||
|
||
$location.$$state = $browser.state(); | ||
|
||
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i; | ||
|
||
function setBrowserUrlWithFallback(url, replace, state) { | ||
var oldUrl = $location.url(); | ||
var oldState = $location.$$state; | ||
try { | ||
$browser.url(url, replace, state); | ||
|
||
// Make sure $location.state() returns referentially identical (not just deeply equal) | ||
// state object; this makes possible quick checking if the state changed in the digest | ||
// loop. Checking deep equality would be too expensive. | ||
$location.$$state = $browser.state(); | ||
} catch (e) { | ||
// Restore old values if pushState fails | ||
$location.url(oldUrl); | ||
$location.$$state = oldState; | ||
|
||
throw e; | ||
} | ||
} | ||
|
||
$rootElement.on('click', function(event) { | ||
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) | ||
// currently we open nice url link and redirect then | ||
|
@@ -740,52 +809,63 @@ function $LocationProvider(){ | |
$browser.url($location.absUrl(), true); | ||
} | ||
|
||
// update $location when $browser url changes | ||
$browser.onUrlChange(function(newUrl) { | ||
if ($location.absUrl() != newUrl) { | ||
$rootScope.$evalAsync(function() { | ||
var oldUrl = $location.absUrl(); | ||
var initializing = true; | ||
|
||
$location.$$parse(newUrl); | ||
if ($rootScope.$broadcast('$locationChangeStart', newUrl, | ||
oldUrl).defaultPrevented) { | ||
$location.$$parse(oldUrl); | ||
$browser.url(oldUrl); | ||
} else { | ||
afterLocationChange(oldUrl); | ||
} | ||
}); | ||
if (!$rootScope.$$phase) $rootScope.$digest(); | ||
} | ||
// update $location when $browser url changes | ||
$browser.onUrlChange(function(newUrl, newState) { | ||
$rootScope.$evalAsync(function() { | ||
var oldUrl = $location.absUrl(); | ||
var oldState = $location.$$state; | ||
|
||
$location.$$parse(newUrl); | ||
$location.$$state = newState; | ||
if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl, | ||
newState, oldState).defaultPrevented) { | ||
$location.$$parse(oldUrl); | ||
$location.$$state = oldState; | ||
setBrowserUrlWithFallback(oldUrl, false, oldState); | ||
} else { | ||
initializing = false; | ||
afterLocationChange(oldUrl, oldState); | ||
} | ||
}); | ||
if (!$rootScope.$$phase) $rootScope.$digest(); | ||
}); | ||
|
||
// update browser | ||
var changeCounter = 0; | ||
$rootScope.$watch(function $locationWatch() { | ||
var oldUrl = $browser.url(); | ||
var oldState = $browser.state(); | ||
var currentReplace = $location.$$replace; | ||
|
||
if (!changeCounter || oldUrl != $location.absUrl()) { | ||
changeCounter++; | ||
if (initializing || oldUrl !== $location.absUrl() || | ||
($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) { | ||
initializing = false; | ||
|
||
$rootScope.$evalAsync(function() { | ||
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl). | ||
defaultPrevented) { | ||
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl, | ||
$location.$$state, oldState).defaultPrevented) { | ||
$location.$$parse(oldUrl); | ||
$location.$$state = oldState; | ||
} else { | ||
$browser.url($location.absUrl(), currentReplace); | ||
afterLocationChange(oldUrl); | ||
setBrowserUrlWithFallback($location.absUrl(), currentReplace, | ||
oldState === $location.$$state ? null : $location.$$state); | ||
afterLocationChange(oldUrl, oldState); | ||
} | ||
}); | ||
} | ||
|
||
$location.$$replace = false; | ||
|
||
return changeCounter; | ||
// we don't need to return anything because $evalAsync will make the digest loop dirty when | ||
// there is a change | ||
}); | ||
|
||
return $location; | ||
|
||
function afterLocationChange(oldUrl) { | ||
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl); | ||
function afterLocationChange(oldUrl, oldState) { | ||
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl, | ||
$location.$$state, oldState); | ||
} | ||
}]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
if this fix is just for ie<10 as you documented then we'll never need to check
history.state
, no?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.
updated a comment