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

feat($location): add support for History API state handling #9027

Closed
wants to merge 1 commit into from

Conversation

mgol
Copy link
Member

@mgol mgol commented Sep 11, 2014

This is a continuation of #3325, re-created per my e-mail thread with @IgorMinar.

EDIT: new commit message:

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.

mgol added a commit to mgol/angular.js that referenced this pull request Sep 11, 2014
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 angular#9027
@mgol
Copy link
Member Author

mgol commented Sep 11, 2014

The location.js file has more changes but they're mostly trivial.

@mgol
Copy link
Member Author

mgol commented Sep 11, 2014

@IgorMinar I'm still not sure what to think about the pushState title handling. I created $location.pushState(state, title, url) signature so that it matches native history.pushState but AFAIK every browser simply ignores the title argument so I'm not sure how useful it's to pass it around.

You'd think the title param changes the title of the page or something but nope. Web standards.

@mgol mgol self-assigned this Sep 11, 2014
@mgol mgol added this to the 1.3.0-rc.2 milestone Sep 11, 2014
mgol added a commit to mgol/angular.js that referenced this pull request Sep 11, 2014
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 angular#9027
mgol added a commit to mgol/angular.js that referenced this pull request Sep 11, 2014
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 angular#9027
// Android Browser BFCache causes location, history reference to become stale.
if (location !== window.location) location = window.location;
if (history !== window.history) history = window.history;

// setter
if (url) {
if (lastBrowserUrl == url) return;
if (lastBrowserUrl == url && (!$sniffer.history || equals(lastBrowserState, state))) {
Copy link
Member Author

Choose a reason for hiding this comment

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

@IgorMinar IIRC this if was originally meant to prevent infinite digest in IE9 in hash mode. I added comparing states, I could also check title (though browsers don't expose the title param used in pushState in any way) but I'm not sure if it's needed in HTML5 mode?

Copy link
Contributor

Choose a reason for hiding this comment

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

no need to use equals here. we can just compare references since browsers always create new instance of history.state when state change occurs.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you are right. this stuff should not be needed at all. let's remove it until we see a need for 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.

OK, I'm leaving it as it was then as it's for the hash mode only anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, actually, I need to be able to do $location.pushState(state, '', 'the-same-url') so I need to have a relaxed condition (I've already written a test for that). Ideally I'd just skip the URL check here in HTML5 mode but $browser doesn't know about such a concept. I'll think about 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.

For reference, this logic was added in ffb2701; am I right that it affects only the hash-fallback-in-html5-mode case?

Anyway, while it was added because of oldIE, people may depend on the fact they can call $location.url('the-same-url') and expect nothing to happen in such a case. I might skip this logic if $sniffer.history but then the behavior would depend on whether the browser supports the History API or not which is not ideal.

Ideally I'd skip this logic only if the change originated via $location.pushState/replaceState, but I can't detect that from within $browser, mainly because history.pushState(undefined, '', 'new-url') is still valid so the basic state === undefined check won't be enough.

This hash-in-html5-mode fallback is a real PITA. :)

@tbosch tbosch modified the milestones: 1.3.0-rc.2, 1.3.0-rc.3 Sep 15, 2014
mgol added a commit to mgol/angular.js that referenced this pull request Sep 17, 2014
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 angular#9027
mgol added a commit to mgol/angular.js that referenced this pull request Sep 20, 2014
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 angular#9027
@mgol mgol added this to the 1.3.0 milestone Sep 23, 2014
@mgol
Copy link
Member Author

mgol commented Sep 23, 2014

@IgorMinar any remarks?

mgol added a commit to mgol/angular.js that referenced this pull request Sep 24, 2014
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 angular#9027
else {
history.pushState(null, '', url);
// Crazy Opera Bug: http://my.opera.com/community/forums/topic.dml?id=1185462
baseElement.attr('href', baseElement.attr('href'));
Copy link
Contributor

Choose a reason for hiding this comment

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

I just merged #8589 so this hunk is now obsolete

@mgol
Copy link
Member Author

mgol commented Oct 4, 2014

Note: current design assumes that $location.url(newUrl) will do a pushState/replaceState with the current state, not a null/undefined one. Same applies to $location.state(newState) - the same URL is used.

If both are needed to be changed, one has to invoke $location.url(newUrl).state(newState).

mgol added a commit to mgol/angular.js that referenced this pull request Oct 4, 2014
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 angular#9027
@mgol mgol changed the title feat($location): add support for state and title in pushState feat($location): add support for History API state handling Oct 4, 2014
@mgol
Copy link
Member Author

mgol commented Oct 6, 2014

One note: currently one invocation of $location.state({a: 2}) will cause all subsequent $location.url('newUrl') invocations to trigger history.pushState({a: 2}, '', 'newUrl'), even far in the site future. I could reset the state during digest if that's desired.


$location.$$parse(newUrl);
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
oldUrl).defaultPrevented) {
$location.$$state = copy(newState);
Copy link
Contributor

Choose a reason for hiding this comment

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

why copy?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because (at least in Chrome) history.state cannot be swapped but can be augumented:

history.pushState({a: 2}, 'a', 'b');
history.state; // {a: 2}
history.state.b = 3;
history.state; // {a: 2, b: 3};

Copy link
Member Author

Choose a reason for hiding this comment

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

The more important copy is in Location#state, though as then we save the state object passed by the user and nothing prevents them from modifying it afterwards (i'd guess it may be common).

Copy link
Contributor

Choose a reason for hiding this comment

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

we should add this reasoning into the code as a comment so that we don't forget.

Copy link
Member Author

Choose a reason for hiding this comment

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

ok, will do

mgol added a commit to mgol/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
var currentReplace = $location.$$replace;
var currentStateSent = $location.$$stateSent && changeCounter;
Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what's the deal with changeCounter but without this check here I was getting into the if below with equal URLs & states and $$stateSent === true which was causing the state to be set to null (& breaking a test at that).

@mgol
Copy link
Member Author

mgol commented Oct 7, 2014

@IgorMinar ready for the final review.

@mgol
Copy link
Member Author

mgol commented Oct 7, 2014

One weird result is that if we pushed {a: 2} and we want to later push the same object again, checking $location.state() will give us {a: 2} but we still need to invoke $location.state({a: 2}) to have the state set to this object and not null.

if (lastBrowserUrl == url) return;
// Prevent IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
// See https://github.com/angular/angular.js/commit/ffb2701
if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) {
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

updated a comment

IgorMinar pushed a commit to IgorMinar/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
mgol added a commit to mgol/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
mgol added a commit to mgol/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
mgol added a commit to mgol/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
@mgol
Copy link
Member Author

mgol commented Oct 7, 2014

@IgorMinar @tbosch I merged Igor's changes and rebased over master and now I have one test failure on a test introduced today:

expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', false);

Could you have a look?

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 angular#9027
IgorMinar pushed a commit to IgorMinar/angular.js that referenced this pull request Oct 7, 2014
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 angular#9027
@mgol mgol closed this in 6fd36de Oct 7, 2014
@mgol mgol deleted the pushState branch October 8, 2014 10:56
bullgare pushed a commit to bullgare/angular.js that referenced this pull request Oct 9, 2014
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 angular#9027
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants