Comments (64)
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 ;-)
from history.js.
Another very easy workaround is using History.getCurrentIndex()
When you push your state
History.pushState({_index: History.getCurrentIndex(), someData: ...}, someTitle, someUrl);
and then in the event binding
History.Adapter.bind(window,'statechange',function(){
var currentIndex = History.getCurrentIndex();
var internal = (History.getState().data._index == (currentIndex - 1));
if (!internal) {
// your action
}
});
from history.js.
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.
from history.js.
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.
from history.js.
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.
from history.js.
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.
from history.js.
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.
from history.js.
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.
from history.js.
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!
from history.js.
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).
from history.js.
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!
from history.js.
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)
from history.js.
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?
from history.js.
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.
from history.js.
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.
c79c17f
from history.js.
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.
from history.js.
What the status of this issue? Is the flag going in?
Thanks!
from history.js.
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).
from history.js.
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)
from history.js.
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
from history.js.
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
from history.js.
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.
from history.js.
@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.
from history.js.
+1 and following updates...
from history.js.
@seb78, thanks for the hack :-)
from history.js.
@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.
from history.js.
+1 for this useful feature for Rails developers for
form_for or link_to with :remote flag
sorry for bad English
from history.js.
This pull request fixed the issue for me - 'statechange' is only fired when using the back/forward button, and not when using pushState.
from history.js.
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
}
});
from history.js.
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.
from history.js.
@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.
from history.js.
I have implemented this to handle the problem:
window.History.Adapter.bind (window, 'statechange', function() {
if (typeof History.Adapter.artificial != "undefined") {delete (History.Adapter.artificial); return}
.... your code here .....
})
window.History.pushStateOriginal = window.History.pushState
window.History.pushState = function (state, string, url) {
window.History.Adapter.artificial = true
window.History.pushStateOriginal (state, string, url)
}
from history.js.
Do we have an update on this issue. I am working with the HTML4 version as I use IE9, and window.History.Adapter.bind (window, 'statechange', function() {}) does not even get invoked when I click browser back and then forward. Please provide on an update please. Thank you
from history.js.
I'm having this same issue:
I am working with the HTML4 version as I use IE9, and window.History.Adapter.bind (window, 'statechange', function() {}) does not even get invoked when I click browser back and then forward. Please provide on an update please.
It would be very nice if history.js worked as described for back/forward. These cause state changes, but doesn't trigger onstatechange.
from history.js.
So I took a look at the examples as @balupton said the library worked in the examples. I knew for a fact it did NOT but I took a look anyway, and saw he was using jquery.history.js. I was using native.history.js, so figuring this might be the root of my issue, I replaced the script in my app to use jquery.history.js instead of native.history.js. Now back() DOES properly fire statechange event. This is good because I would have had to switch to another library.
@phamtn8 not sure if your issue was ever solved, but if your project already uses jQuery try using the jquery.history.js. Also, @balupton if you could look into and solve the issue I've described it would be greatly appreciated. :)
from history.js.
@balupton since this is an active project whose goal is to polyfill/fix history API. It would be really nice if we could have a definitive fix or at least an official best practices addressing this issue (which persists for so long).
Do you have any suggestion of what could be done / who could look at it?
from history.js.
I have enhanced my function which I posted a few months earlier and made it into a simple wrapper for history.js, (though I haven't tested this in IE9 yet.. should work, maybe?):
from history.js.
echoing @rxaviers would definitely like to see a recommended fix for this. 3 years and no update. I'd rather use a faithful polyfill but if not I guess Backbone.History could be ripped out.
from history.js.
thanks so much @amertum! Your solution works like a charme!
from history.js.
@amertum, great workaround, thanks!
from history.js.
I had problems with the "current index" workaround, because if I pressed back button and then forward, statechange was not fired because I reactivated the peek state.
My simple workaround for this problem (using some jQuery as well):
// handling push and replace at once and setting a flag that indicates an internal state change. Also does some data normalization and random value injection for forcing state change (that was my special need)
History.pushOrReplaceState = function (data, title, url, isPush) {
var func = isPush ? "pushState" : "replaceState";
if (data === undefined || data === null)
data = {};
if (typeof data !== 'object')
data = { value: data };
History.internalChanging = true;
History[func]($.extend(data, { _ran: Math.random() }), title, url);
};
// checks if the flag is set, and resets it immediately
History.isInternalChange = function () {
var b = History.internalChanging;
History.internalChanging = false;
return b;
};
// shorthand for subscribing statechange event, using a callback which accepts the current state as parameter
History.onStateChange = function(handler)
{
if (!$.isFunction(handler)) return;
$(window).on("statechange", function () {
if (History.isInternalChange()) return;
handler(History.getState());
});
};
For me this works as expected, and shorter in usage. If anyone finds something wrong with it please feel free to suggest any addition or fix.
from history.js.
I have submitted a pull request with an internal flag for the latest HistoryJS version. When the back/forward button is used, a new flag is returned in the getState() function.
var state = History.getState();
if (state.internal) {
// back/forward event, do something
} else {
// manual pushState event, do something else
}
https://github.com/browserstate/history.js/pull/401/files
from history.js.
@dherran This looks like exactly what I need. Super easy. No idea why something similar hasn't been added in order to close this longstanding issue.
from history.js.
Thank you dherran!!! That's exactly what I was looking for, and it works perfectly!! This is how I'm using it in my code:
$(document).ready(function() {
History.Adapter.bind(window, 'statechange', function () {
var state = History.getState();
if (state.internal) { //this catches only browser back or forward button
if (History.getStateByIndex (History.getCurrentIndex() - 1).data.state == 'book'){
returnBackToTopics(); //display this specific page
} else{
window.location.replace(document.URL);
}
}
});
from history.js.
@cervengoc thanks for your example, i've implemented a similar method.
@dherran I'm not sure if i'm reading your PR correctly, on back/forward you are setting isInternal to true, and when triggered via pushState/replaceState it is false? I figure that should be the other way around? In my interpretation Internal would mean "from my internal code".
from history.js.
Hi. I'm new to GitHub, but I signed up for an account just to comment with my use case. I'm using https://github.com/browserstate/history.js/pull/401/files with success. But one thing that bugged me was that the very first State which is created on the landing page does not contain the internal flag.
So I added a line in the storeState
function:
// Set State.internal
newState.internal = ( typeof newState.internal === 'undefined' ? true : newState.internal );
Now it means that if a visitor uses the back button to go all the way back to the first landing page, internal flag is set and your code knows the statechange
was because of a browser back button.
from history.js.
Thanks @dherran and also @mdingena for your fixes. This is working how I expected to now and is a great library. How do we get this merged as @balupton isn't maintaining this anymore?
from history.js.
@nevace I'm happy to add new owners and maintainers to the project.
from history.js.
I agree with @TomK, I also think that as far as terminology goes, the word "internal" should refer to programmatically changing state, and using Back/Forward button should be the "non-internal". At least I would expect it this way.
from history.js.
It's probably internal
because the state change is triggered from the browser itself, rather than - say - clicking a link on a page.
from history.js.
Yes, that can be a reason. However, in most programming environment "internal" means rather something like I wrote. Maybe would be better to call it something like clientChange
, which is more self explanatory IMHO.
from history.js.
@balupton I'm happy to help maintain this along with others. I know the amount of time you were spending got too much so if any others would also help then that would be great.
from history.js.
@cervengoc has a point, remove the problem altogether by calling it something different. Perhaps isNavigation
?
but then clicking a state change link is also navigation :/
from history.js.
That's why I thought of isClientChange
, because that covers everything that the "client" does, not the "developer".
from history.js.
@nevace sweet will add you, then you can add others as it goes - the main issue is the tests, the tests require manual runs - for two reasons:
- older versions of safari on iOS (not relevant anymore, as no one uses them) - would have different behaviour of a programatic state change versus an actual back button click by a human
- browsers sometimes either forget state data or persist state data when: you do a few state changes, then navigate to a different website, then hit the back button
So, for each version, I was spending a day or more testing a huge array of browsers manually - if that can be automated using better tooling, then history.js would be easily maintained, however such better tooling is not something I have any experience with.
We have a slack community at https://slack.bevry.me that I can add a history.js room for if maintenance is going to resume.
from history.js.
@nevace invite sent
from history.js.
Anyone else want to help maintain too?
from history.js.
Moving maintenance discussion over to #450
from history.js.
Where in the file would I insert
// Set State.internal
newState.internal = ( typeof newState.internal === 'undefined' ? true : newState.internal );
as per @mdingena's suggestion? The forward button motion seems to work, but any click on the back button results in a false flag for internal.
from history.js.
In the storeState
function, as I suggested.
from history.js.
I tried adding it there, line 1104 in @dherran's edit, still didn't get a true flag on internal when using the back button. http://dev.podcamptoronto.com/sessions/
from history.js.
I'm using this which I've modified with the above and works as expected regarding back/forward.
I'm not sure what the best way is to get it to work on first load, I just ended up manually calling History.pushState() on first load. Not very nice to have duplicate entries but works for back/forward (I need to re-init animations etc. on different pages so need to know when back/forward is pressed).
from history.js.
Why is this issue still not merged into master?? In the main-while you can get the file here:
https://github.com/danger89/history.js/blob/master/scripts/bundled/html5/jquery.history.js
from history.js.
@seb78 Thank you, you very helped me.
from history.js.
Related Issues (20)
- Few test cases are failing on X86. Saying "operation insecure"
- How to use History API State with hash urls?
- IE/Edge pop last saved object irrespective of current URL
- change url cross domain
- native.history.js detected as virus HOT 1
- No default jsDelivr CDN file set
- jquery.history.js breaks the script adn return "History.js Adapter has already been loaded..." error.
- replaceState removes first character of hash (#) when it contains dot "."
- indicating locale code to url
- Problems with webpack encore at page loading
- The Solutions section is missing the hashtag solution
- Change domain "mysite.com/page1" to "mysite.com"
- npm WARN deprecated: Update deprecated dependencies HOT 1
- history.goback() is not working for mozilla firefox
- page is not jump used push HOT 1
- OnPopState calls in case of section change
- Back Button Error while using with VueJs Data
- Revise tests
- How to unbind function from History.Adapter.bind? HOT 1
- security scan result HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from history.js.