history.js
history.js copied to clipboard
The artificial triggering of 'onpopstate' in pushState() causes losing of steps
Hi,
Initially I posted my problem first at http://getsatisfaction.com/balupton/topics/history_js_back_button_goes_two_states_steps_back. After some research I found the the problem is caused by the manually triggered 'onpopstate' event in the end of the body of History.pushState(). Triggering the event consumes the state too early and later when the user clicks browser's back button there is no notification for the last registered event. I.e. there is a single link clicking on which pushes a new state. Clicking three times on this link and then clicking browser's back button will call twice my 'statechange' handler - once for the second click and once for the first one, but none for the third.
https://gist.github.com/909482 A simple page to reproduce the problem.
Steps:
- Open console (Firebug or Developer tools)
- Click on any link (the list items) In console you will see the timestamp for each click
- repeat 2) few more times
- click browser back button in console you'll see 'statechange' event for all pushed states but the last one
Commenting History.Adapter.trigger(window,'popstate'); at line 1737 in history.js fixes this problem. The new problem now is that the statechange events pop in wrong order, 1 -> 2 -> 3, instead of 3 -> 2 -> 1.
Hi Martin,
I cannot replicate the issue. The demo works perfectly, as well as tests in all browsers for the v1.7 release. http://balupton.com/sandbox/history.js/demo/
The reason for the manual trigger of popstate is to fix that "new problem" you've reported.
I'd suggest not using the isInternal
flag, there is no use for it. The ajax request should be performed in the statechange handler. Refer to https://gist.github.com/854622 for a best practice solution.
Hope that clears some things up.
It is very strange to me that you trigger 'statechange' as soon as I call pushState(). I expect 'statechange' events only when the browser back/fwd buttons are clicked. Those I'm interested in and I want to react to. With the current behavior my callback is called immediately.
See http://diveintohtml5.org/examples/history/brandy.html, this is the main example for http://diveintohtml5.org/history.html. There is gallery.js with a handler for 'popstate' at the bottom. It is called only when I use browser's back/fwd buttons or window.history.back/forward/go() are used. It doesn't fire when I click the prev/next links in the page. And this is the idea in HTML History API.
Your example (gist 854622) is nice and it seems History.js is designed to support it. Whenever you click a link you cancel the event and fire statechange. In the handler you actually do the work to load the new content.
In my case the link is not always an anchor (tag "a"), or it may be anchor but has onclick handler and doesn't use the href attribute. So my onclick handler does the work and finally registers an event in the history (via pushState()). Without 'isInternal' my statechange handler will revert the work that I just did in onclick.
That's why I (and Johan in the forums) need the "isInternal hack". But even with it it still doesn't work because the history behaves as queue, not as stack.
Ah, I just noticed you closed the ticket as "cannot reproduce" :-) That's because you didn't use my code, but yours ... My code shows different use case.
Hi Martin,
Not really sure what you are asking of me?
The current code works in all browsers, as intended. Your recommendation for commenting out that particular line, breaks other functionality as you have mentioned therefore introducing bugs.
I feel the bug is with your implementation, rather than with History.js as I've already pointed out by the best-practice gist.
If you can provide a practical use case which the way History.js hinders you, which can't be coded in the best-practice way, then I will be willing to look into it. However as the issue currently stands, there is no onus on myself to look into this further than I already have.
Feel free to fork the code, and make the modifications you need to. Be sure to test it (using the tests folder) in all supported browsers to make sure you don't break anything.
Hi Benjamin,
I am not sure whether you saw the link in my second comment here: https://gist.github.com/909482. This is the mini application that exposes the problem. And yes - I'm going to fork the project and see whether I can make my use case working. Thanks for your support!
Martin,
Try this: http://pastie.org/1780113
History.js doesn't work the way you think. I had the same difficulty. In normal HTML5 History, you'd do your XHR or whatever, and then do pushState on completion. History.js assumes that you want to do the pushState first, and then use the statechange event that it fires to trigger your event.
Benjamin, it'd be nice if we could distinguish between pushState()/replaceState()-initiated statechange events and user-initiated history traversals. The "pushState first, AJAX later" paradigm is fundamentally flawed. If an XHR fails or is delayed, you have a URL/content disagreement. It doesn't make sense to do the fast part first, followed by the slow and unreliable part.
Users of History.js just need a way to distinguish between user-initiated events and code-initiated events (which they don't care about).
Hi Mark,
Thanks! Your approach looks like what I want. I'll try it in my real application. I think if I combine it with my "isInternal" flag it will behave as I want it. For now I use http://tkyk.github.com/jquery-history-plugin. It doesn't look that cool as HTML5 enabled history management but at least fits perfectly for my needs.
Thanks again!
Benjamin, first off, great work on the project.
The issue here is that according to the best practice for History.js is to fire the ajax request inside the statechange event. But there are times where this can't happen because there's already another function handling the ajax request.
For example, Rails 3 introduced unobtrusive Javascripts where the user needs to specify in their html element "data-remote = true" and Rails will handle the Ajax request automatically. Ideally in html5, I would add an oncomplete handler to the ajax request and call history.pushState to update the url. I would also create a listener for the onpopstate and inside that I just call $.getScript(location.href);
Here's a Railcast on this approach http://asciicasts.com/episodes/246-ajax-history-state
Unfortunately it's challenging to use History.js since it will cause an issue with this approach. The statechange/popstate event is being triggered on History.pushState. In the statechange/popstate function, we need a utility to determine if the event is caused because of a pushState or from an actual popstate. Depending on the type of event, one can then determine whether or not to ignore it (ie when it's from a pushState) or to fire an ajax request (ie from a popstate)
Thanks for the detailed response, I will look at the screencast and see what I can do.
My big concern with this, is then instead of having the detection of internal vs external in statechange, it would then be in your success handler of the Ajax request.
There are some other use cases I'm looking into such as caching the result of Ajax requests in the state data, I'll see what I can come up with. If I can create a History.getState().internal flag, would that work?
If I can create a History.getState().internal flag, would that work?
That'd be a start. Might also be useful to tell which kind of event triggered it. pushState(), replaceState(), other.
The internal flag is now there in dev for HTML5 browsers, still need to add it for HTML4 browsers. It was flipping hard to add. https://github.com/balupton/history.js/commit/c79c17f9801373f5218f144088c387f1935f3462
Perhaps as an alternative a mask_event flag could be added to the pushState/replaceState calls. If the flag is set then the onpopstate event is not triggered. Then it wouldn't be necessary to check where the event came from when handling it.
What the status of this issue? Is the flag going in?
Thanks!
The 1.8 version still has a while left. I'll be releasing a v1.7.1 release sooner, to address the most urgent issues (session state storage, native adapter, etc). If you'd like to speed up release of the v1.8 release, check it out and get the tests passing in all the different browsers - the codes in there, just need to ensure it works in all the different browsers (the hardest bit).
Just wondering, how is the state of the art so far?
I've tried the development plugin and the functionality commented above is just working good. The thing is I feel like that branch is a little bit discontinued, isn't it?
The problems I have with the dev plugin are related to the state store system. When I reload the page, if I click on the back button, it triggers the statechange event, but the state received is not the expected. This behavior doesn't happen in the master branch plugin, so would it be possible to see the dev branch with the new changes on master? (I think they'll fix all my probs).
thanks and great work! (seriously)
Hi balupton
First of all thanks for providing History.js, i just recently started using it and I quickly found the need for the internal flag that was added in your dev build.
It works perfectly on the websites I am developing when browsers are HTML 5, but of course it would be nice if the same flag could be added for HTML 4 to make it backward compatible.
Do you by any chance have an idea of when this will happen, I know your are busy and only working on this project in your spare time?
Best regards
Benny
Hi all, I added three lines to the History.js, v.1.7.1, to see where the event came from: back/forward, popstate, or the initial state of the page: In the function 'History.onPopState = function(event,extra){..' you have this switch: // Fetch State if ( stateId ) { // Vanilla: Back/forward button was used newState = History.getStateById(stateId); newState.origin = 'bfw'; } else if ( History.expectedStateId ) { // Vanilla: A new state was pushed, and popstate was called manually newState = History.getStateById(History.expectedStateId); newState.origin = 'add'; } else { // Initial State newState = History.extractState(document.location.href); newState.origin = 'ini'; }
The 'newState.origin=...' are the additions. The full method is at http://fabalint.info/history_js/origin.mod.and.tile.html , along with a partial update helper (so on back/forward the page parts can be updated.) Enjoy(: Balint
Balint, that's a great little hack - the problem is that it doesn't work with the HTML4 support in IE - any idea if it's possible to tell whether the event came as a result of a Back/Forward or an explicit pushState call?
My big concern with this, is then instead of having the detection of internal vs external in statechange, it would then be in your success handler of the Ajax request.
Benjamin, why don't you want it in the success handler? If this is a rehashed point, I'm happy to go read another thread/post.
As far as the user is concerned, the state change happens only once the data has been loaded. In our use case, we're using it for a set of filters as checkboxes. Clicking on each filter fires off an ajax request. Earlier requests get aborted if they haven't returned by the time the user clicks on another checkbox. We don't want to have any of the intermediate states show up in the history (referring to the ajax requests that were aborted). Clicking 'Back' should take the user directly to their previous set of loaded data.
@balupton — First, thanks for building History.js.
I don't really have much to add to this discussion, but I wanted to add a +1 for providing some way to tell if a statechange
event was triggered by pushState
/replaceState
or by user action (browser back/forward buttons).
I recently started using History.js in a Backbone/PhoneGap project and then abandoned History.js because the 'statechange
on pushState
' behavior of History.js basically forces a stateless style of app development when integrating with the Backbone router (designed around the specified HTML5 pushState
/replaceState
/popstate
behavior), which is problematic when doing client-side MVC. See, eg.: http://lostechies.com/derickbailey/2011/08/03/stop-using-backbone-as-if-it-were-a-stateless-web-server/ for some remarks about why this is problematic.
+1 and following updates...
Just a little trick I did to know if statechange was triggered by pushState/replaceState. In my state object I use for pushState I add a timestamp :
var timestamp = new Date().getTime(); var stateObj = { id: unique_id_action , createtime: timestamp }; History.pushState(stateObj, "page 2", "/")
And in my listener I verify the time difference, if it's very small I consider I come from pushState and do nothing:
History.Adapter.bind(window,'statechange',function(){ var State = History.getState();
var createtime = State.data.createtime; var timestamp = new Date().getTime(); var diff = timestamp - createtime;
if(diff>500){ //manage back and forward button here ... }
});
It works fine for me, I hope it can help ;-)
@seb78, thanks for the hack :-)
@semaperepelitsa - While it's a nice hack, anyone using @seb78's hack should be aware that it's not really dependable. Anything could happen to cause the timestamps to be different and the messed up behavior would be really hard to reproduce and/or debug. I would hesitate very strongly before sticking it into our production code.
+1 for this useful feature for Rails developers for form_for or link_to with :remote flag sorry for bad English
This pull request fixed the issue for me - 'statechange' is only fired when using the back/forward button, and not when using pushState.
Hi, below is how the issue can be resolved:
var timestamps = []; // Array of unique timestamps.
// Push state
function pushState(title, url, anydata){
// Creating a unique timestamp that will be associated with the state.
var t = new Date().getTime();
timestamps[t] = t;
//Adding to history
History.pushState({timestamp:t}, title, url);
}
// Listening
History.Adapter.bind(window,'statechange',function(){
var State = History.getState();
if(State.data.timestamp in timestamps) {
// Deleting the unique timestamp associated with the state
delete timestamps[State.data.timestamp];
}
else{
// Manage Back/Forward button here
}
});
Haven't worked out the details, but this timestamp idea is helping me distinguish a manual pushState from statechange triggered by the back button. Thanks mdakota.
@mdakota I found your timestamps very useful. It works whether you make timestamps an array or an object, but it made more sense to me as an object (associative array) just because I could examine the object more clearly in Firefox's console. In the console, when I use an array, it just looks like this [ ]
whether it contains a timestamp or not. If I use an object, the empty object (indicating I used the back button) looks like this { }
and after using pushState I get a nice clear indication Object { 1361395647783=1361395647783}
Maybe it doesn't matter, but it's just easier during debugging and developing.