#246 AJAX History State
Dec 27, 2010 | 10 minutes |
Ajax
The new pushState function in JavaScript allows us to change the full URL with AJAX. This means the back button, reloading, and bookmark support can be easily added.
- Download:
- source codeProject Files in Zip (123 KB)
- mp4Full Size H.264 Video (21.3 MB)
- m4vSmaller H.264 Video (12.6 MB)
- webmFull Size VP8 Video (29 MB)
- ogvFull Size Theora Video (28.1 MB)
If you want to backport support for legacy browsers have a look at: https://github.com/fortes/history.js
I really like the approach, funny enough I have figured out how to use the latest jquery-bbq over the last 3 days.
I will convert the app to this approach, it will be nicer with slash-URLs and without a # anyway.
BUT: for those who need FF3/IE-support:
Full blown: https://gist.github.com/755139
Reduced FeatureS: https://gist.github.com/757510
Have fun :)
Do you have directions how to transform the URL address generated like this from:
http://domain.tld/products?direction=desc&search=dj&sort=title&utf8=%E2%9C%93
... to:
http://domain.tld/products/search:dj/direction:desc/sort:title
Do I need anything else besides rails3 router + changing the form input serialization to return slashs and colons instead of ampersands and equal-signs?
I posted about this as well here a while ago: http://www.railstoolkit.com/posts/html-5-ajax-history-and-adress-bar-integration
Does the back button in this example also work to get back all the way to the initial non-ajax load of the page? I had to add the state for the initial page load after the first ajax call using the replaceState function to get it working. See my blog post mentioned in my previous comment for details.
It seems to me that with Firefox history.pushState doesn't work very well.
With Chrome it's ok but with Firefox it does not.
Here is the code:
$(function () {
$('.pagination a').live('click', function () {
$.getScript(this.href);
history.pushState(null, "", this.href);
return false;
});
$(window).bind("popstate", function() {
$.getScript(location.href);
});
});
Chrome renders index.js.erb and it is ok, while firefox renders index.js.erb and after it renders index.html.erb too and it is not ok.
I've tried jquery.ba-bbq.js and it works well with Chrome and with Firefox.
history.js does not work.
I'm unsure how the set document title works. Because you're doing an ajax call and it's the same ajax response which sets the title, however you're getting the title in the next line for the pushState function... how do you ensure that your ajax call returns BEFORE you ask for the title? Does $.getScript isn't async?
Great screencast!
In Google Chrome 10, I'm getting a problem where an extra get request is made for every single action on my controller for a JS template. This doesn't result in any visible errors but the extra request can be seen in, for example, Firebug or Mongrel. The server returns a 500 error on every action apart from the index action. Only the JS request is made when an AJAX call is made.
The problem is that the code inside the function binded to "popstate" is being executed on page load. This can be seen more clearly if you replace the getScript with console.log('hello').
Is anybody else experiencing this behaviour? I'll post again if I find a fix.
According to this source, http://www.splefty.com/js/popstate.html, the popstate event is supposed to fire on every page load for various reasons. This means in order to get rid of the unnecessary requests, we need to hook onto the popstate event such that our code only runs when the popstate fires AND the user has clicked the back/forward buttons.
So far it looks like the events are identical (and therefore indiscernible) unless you pass an object to the first argument of pushState. This object gets put onto the state property of the event object but only when the back/forward button is pressed and not on page load.
Unfortunately, an object only gets associated with a page when pushState is called and when you first visit a page, the state will be null. Thus if a user clicks on two links which make calls to pushState (with an object passed to state), and then clicks back twice, the first popstate event will have a state object but the second will still be null. This is because you will be back on the original page and the original page was accessed through a normal page load, not push state.
This means we still can't categorically distinguish between a page that has been reached after pushState and one that has been reached normally.
@rupert: I saw the same issue and replaced the popstate handling code as follows to resolve it:
window.onpopstate = function(e) {
if (!(typeof e.state == 'undefined')) {
// actions here
}
}
I had to use window.onpopstate instead of jQuery's bind method because otherwise the event object wasn't being passed properly.
@Nick: Thanks but that doesn't work for me on Chrome 10, unfortunately.
Under what circumstances will e.state be undefined? As far as I can see, on an initial page load, e.state is defined with a value of null.
If I replace //actions here with console.log('hello'), I get hello on the console under the following circumstances:
* Initial page load
* Use of back/forward buttons, after some AJAX calls
* On any other action in the controller, such as show.
Also, so far as I can see, the event from onpopstate is identical to event.originalEvent when using bind.
Rupert, check my blog post i mentioned in an earlier comment for a way to fix the problem with the missing state for the initial page load: http://www.railstoolkit.com/posts/html-5-ajax-history-and-adress-bar-integration
Someone else has issues with Back button displaying the js/html returned with the ajax request when using the back button instead of the full pages? How to fix?
When I use back and forward buttons I get a lot of requests in the log. In between 50 and 150 to be exact. The returns are 200 codes for the url in question.
Any suggestion how to solve this?
Thanks in advance!
@rupert: I ended up just faking it by always tracking the currently loaded URL in an attribute on the body tag, and then returning false from window.onpopstate if the loaded URL is the same as the popped location.
Full file here: https://gist.github.com/782275
This works! Thank you!
Very good! I did a small gist with modifications to get all ajax links (data-remote) automatically, and a small fix to the initial page status. If any one is interested: https://gist.github.com/807074
Tiago, your gist is not available. Can you share it again?
Hi, can you share this again, link isn't available.
@Tiaglo Scolari: this is the best solution I could find! Simple but effective. I have renamed ajax_loaded to state_pushed, but this is the only modification I could come up with :) (I support AJAX queries even if history state is not available)
Can you share the code?
Could you share the code? Original isn't available anymore.
I found a weird thing that sometimes when using the back and forward buttons, the javascript is actually displayed in the browser.
See: http://www.youtube.com/watch?v=kCKa2OzSqKE
Has anyone else noticed this? Any suggestions for a solution?
Did you find any solutions? I've notice same thing in my site, and also even in RailsCasts site, in FF as well as in Chrome Linux versions...
Hey Adam, I just ran into this as well. Were you able to find the reason or a solution? Thanks.
This code is fantastic. Ryan, thanks for making a somewhat complicated concept appear so simple! I had spent half a day trying to get this to work another way, but railscasts came to the rescue!
If your users use internet explorer, the buttons and functionality doesn't even work! Is there a way to progressively enhance your usability by using pushState?
ionas,
I came across this problem in Chrome (doesn't happen in Firefox). Apparently, its because the page and ajax request urls are the same (since Ryan showed pagination with only the requested format differentiating between returning the html and the javascript responses).
See:
http://stackoverflow.com/questions/6309477/moving-back-to-a-pushstate-entry-that-used-ajax
I think a more elegant solution than given in the stackoverflow answer, is to remove any page extension, and append the .js extension to change the url.