Skip to content

@balupton /README.md
Last active

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Ajaxify a Website with the HTML5 History API using History.js, jQuery and ScrollTo
// History.js It!
// v1.0.1 - 30 September, 2012
// https://gist.github.com/854622
(function(window,undefined){
// Prepare our Variables
var
History = window.History,
$ = window.jQuery,
document = window.document;
// Check to see if History.js is enabled for our Browser
if ( !History.enabled ) {
return false;
}
// Wait for Document
$(function(){
// Prepare Variables
var
/* Application Specific Variables */
contentSelector = '#content,article:first,.article:first,.post:first',
$content = $(contentSelector).filter(':first'),
contentNode = $content.get(0),
$menu = $('#menu,#nav,nav:first,.nav:first').filter(':first'),
activeClass = 'active selected current youarehere',
activeSelector = '.active,.selected,.current,.youarehere',
menuChildrenSelector = '> li,> ul > li',
completedEventName = 'statechangecomplete',
/* Application Generic Variables */
$window = $(window),
$body = $(document.body),
rootUrl = History.getRootUrl(),
scrollOptions = {
duration: 800,
easing:'swing'
};
// Ensure Content
if ( $content.length === 0 ) {
$content = $body;
}
// Internal Helper
$.expr[':'].internal = function(obj, index, meta, stack){
// Prepare
var
$this = $(obj),
url = $this.attr('href')||'',
isInternalLink;
// Check link
isInternalLink = url.substring(0,rootUrl.length) === rootUrl || url.indexOf(':') === -1;
// Ignore or Keep
return isInternalLink;
};
// HTML Helper
var documentHtml = function(html){
// Prepare
var result = String(html)
.replace(/<\!DOCTYPE[^>]*>/i, '')
.replace(/<(html|head|body|title|meta|script)([\s\>])/gi,'<div class="document-$1"$2')
.replace(/<\/(html|head|body|title|meta|script)\>/gi,'</div>')
;
// Return
return result;
};
// Ajaxify Helper
$.fn.ajaxify = function(){
// Prepare
var $this = $(this);
// Ajaxify
$this.find('a:internal:not(.no-ajaxy)').click(function(event){
// Prepare
var
$this = $(this),
url = $this.attr('href'),
title = $this.attr('title')||null;
// Continue as normal for cmd clicks etc
if ( event.which == 2 || event.metaKey ) { return true; }
// Ajaxify this link
History.pushState(null,title,url);
event.preventDefault();
return false;
});
// Chain
return $this;
};
// Ajaxify our Internal Links
$body.ajaxify();
// Hook into State Changes
$window.bind('statechange',function(){
// Prepare Variables
var
State = History.getState(),
url = State.url,
relativeUrl = url.replace(rootUrl,'');
// Set Loading
$body.addClass('loading');
// Start Fade Out
// Animating to opacity to 0 still keeps the element's height intact
// Which prevents that annoying pop bang issue when loading in new content
$content.animate({opacity:0},800);
// Ajax Request the Traditional Page
$.ajax({
url: url,
success: function(data, textStatus, jqXHR){
// Prepare
var
$data = $(documentHtml(data)),
$dataBody = $data.find('.document-body:first'),
$dataContent = $dataBody.find(contentSelector).filter(':first'),
$menuChildren, contentHtml, $scripts;
// Fetch the scripts
$scripts = $dataContent.find('.document-script');
if ( $scripts.length ) {
$scripts.detach();
}
// Fetch the content
contentHtml = $dataContent.html()||$data.html();
if ( !contentHtml ) {
document.location.href = url;
return false;
}
// Update the menu
$menuChildren = $menu.find(menuChildrenSelector);
$menuChildren.filter(activeSelector).removeClass(activeClass);
$menuChildren = $menuChildren.has('a[href^="'+relativeUrl+'"],a[href^="/'+relativeUrl+'"],a[href^="'+url+'"]');
if ( $menuChildren.length === 1 ) { $menuChildren.addClass(activeClass); }
// Update the content
$content.stop(true,true);
$content.html(contentHtml).ajaxify().css('opacity',100).show(); /* you could fade in here if you'd like */
// Update the title
document.title = $data.find('.document-title:first').text();
try {
document.getElementsByTagName('title')[0].innerHTML = document.title.replace('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
}
catch ( Exception ) { }
// Add the scripts
$scripts.each(function(){
var $script = $(this), scriptText = $script.text(), scriptNode = document.createElement('script');
scriptNode.appendChild(document.createTextNode(scriptText));
contentNode.appendChild(scriptNode);
});
// Complete the change
if ( $body.ScrollTo||false ) { $body.ScrollTo(scrollOptions); } /* http://balupton.com/projects/jquery-scrollto */
$body.removeClass('loading');
$window.trigger(completedEventName);
// Inform Google Analytics of the change
if ( typeof window._gaq !== 'undefined' ) {
window._gaq.push(['_trackPageview', relativeUrl]);
}
// Inform ReInvigorate of a state change
if ( typeof window.reinvigorate !== 'undefined' && typeof window.reinvigorate.ajax_track !== 'undefined' ) {
reinvigorate.ajax_track(url);
// ^ we use the full url here as that is what reinvigorate supports
}
},
error: function(jqXHR, textStatus, errorThrown){
document.location.href = url;
return false;
}
}); // end ajax
}); // end onStateChange
}); // end onDomLoad
})(window); // end closure
@balupton
Owner

You can now trial this script without actually implementing it on your server by using this bookmark script: https://gist.github.com/919358

@rawfalafel

I ran into a problem with certain symbols in inline javascript (i.e. ' < ' symbol would become <t&; or whatever). The easiest fix was to change scriptText = $script.html() to scriptText = $script.text() on line 156

FYI

@pokonski

Magnificent work, Benjamin. You simplified the simple! Now it just works with zero work on the server side :D

@crucialfelix

This says that it will "ajaxify your website", but its specific to some specific website.

The Explanation doesn't actually explain what it does :)

There's a bunch of DOM dependent stuff that seems to suggest that its going to insert into the #content area of a webpage.

So what is going on ? You fetch a URL and then extract this content area out of that and insert that into the current page ?
I assume the content area is some smaller interior part of the page. It really would help if you could add a paragraph to explanation rather than ask us to look analyze the implementation.

other than that it looks like what I need to solve my case.

thanks!

@pokonski

You just have to adjust the selector with your tag containing actual content. Also you need to wrap your ajax responses in the same tag, because ajaxify also executes that selector on Ajax response.

@crucialfelix

As far as I can see it isn't necessarily intended to return ajax responses, but rather to return the entire page and then extract that DOM id item.

I had to remove the menu stuff as it wasn't applicable to my site.

My comment is just that it would be very helpful to introduce with a paragraph that explains what this is since "ajaxify your website" is quite vague.

@balupton
Owner

Hey @pokonski and @crucialfelix I've updated this gist's readme with an explanation of what it does. Typically I try to educate developers rather than spell it out for them, though I can accept when sometimes I've made that step a bit too hard. So thanks for the feedback I really appreciate :-)

I'm hoping to get some time (or rather, get someone to pay me so I can make time) to update my jQuery Ajaxy project for History.js. Ajaxy still uses the old jQuery History project which is still hashes. Benefit of Ajaxy is that it works with subpages, forms and JSON responses.

@crucialfelix

great explanation, thanks for that ! its not so much about educating us but just telling us if what we are staring at is at all relevant to whatever problem we are currently working on. is it even going to be worth the 20 minutes to figure out what this thing is ? thanks again

@thomasgg

First of all, I really appreciate the work you've done!
But is there an easy way to disable the ajax request for a specific link?

@pokonski
@balupton
Owner

@pokonski is right on, @thomasgg here is the relevant code you'll want:

        // Ajaxify Helper
        $.fn.ajaxify = function(){
            // Prepare
            var $this = $(this);

            // Ajaxify
            $this.find('a:internal:not(.no-ajaxy)').click(function(event){

In fact, I'll update the actual gist to include that by default. Done.

@pokonski
@elidupuis

Just wondering about the :internal helper... should it be counting anchor tags that have an href of # (or start with #) linke <a href="#"> or <a href="#something">? I know lots of people still use # on links that have javascript only functionality or even simply named anchors.

Thoughts?

@balupton
Owner

@pokonski how do :not([data-remote]) links work in rails??? intrigued

@elidupuis can you provide some sample links, and whether or not those links should or shouldn't be ajaxified. This will let me be 100% sure on what you're asking :-)

@elidupuis

I'm thinking that the following links should not be ajaxified:

<a href="#">tied to some misc javascript functionality</a>
<a href="#heading">tied to an element on this page</a>

While the following should be ajaxified as normal:

<a href="page.html#heading">tied to an element on a different page</a>
@pokonski

@balupton, Rails uses his own custom adapter for binding links with data-remote attribute: https://github.com/rails/jquery-ujs . That's why I added :not clause to skip those links.

@chrisjacob

Interesting script!

I recommend using GitHub Pages for linked resources ... not /raw/...

Read
http://code.lancepollard.com/posts/github-as-a-cdn/

@balupton
Owner

@chrisjacob thanks for the feedback. Updated the links.

@zietbukuel

The menu update does not work for me, I think I'm doing it wrong, I'd appreciate a bit of help here, this is my html code:

<div id="menu">
    <ul class="sf-menu">
        <li><a href="/"><img src="/images/home.png" /></a></li>
        <li><a href="/link1">Link 1</a>
            <ul>
                <li><a href="/link2">Link 2</a></li>
                <li><a href="/link3">Link 3</a></li>
                <li><a href="/link4">Link 4</a></li>
            </ul>
        </li>
    </ul>
</div>

I need to select the 'a' not the li, I've tried with:

menuChildrenSelector = '> li > a,> ul > li > a',

But it doesn't work, it does not add any class to the 'a' tag... But when I use this:

menuChildrenSelector = '> li,> ul > li',

It adds the class to the li, which in my case is useless as I need the 'a' to have the class not the li.

BTW, awesome plugin, I love it.

Thanks.

@craigmdennis

@zietbukuel It may be a CSS cascading issue. Try adding the specific class you're looking to target menuChildrenSelector = '.my-li-selector'. The idea is that you customise this gist to your site structure.

@zietbukuel

Thank you, I managed to make it work with the main links, however, the sub-menus do not show work. I'll try your suggestion.

@roc

Hey @balupton this is brilliant.

Thanks for making something that should be very simple, er, very simple.

I have a quick question about re-applying $scripts to new content.

The .document-script class is being applied to all script references in the document just fine, this Gist seems to create a fragment and then populate the body of that fragment with the innards of the script and inject it into the new page. Though this doesn't seem to allow new scripts with common jQuery $(function(){ style instantiations to run (I'm probably dead wrong!).

Do you suggest reattaching document.ready style handlers or self-invoking functions around each script loaded via the .document-script method, or something similar (function(window,undefined){});

Or perhaps something completely different?

Sorry to be slow, I've spent about half an hour with this and honestly think it's the only thing that's not instantly obvious and instantly great. Thanks again :)

EDIT:
My super basic solution was just to include a custom event trigger at e.g. line 145 of this gist. and then monitor that from other scripts.
e.g.

$content.html(contentHtml).ajaxify().css('opacity',100).show().trigger("contentLoaded");
@chmig29fulcrum

Hi everybody. I am trying to make ajax site. With back forward history using ajax. And maximum what I did is making it but using # in address. There is a small plugin that detects changes after # sign when u press back or forward.

What I want is to have clean address bar like mysite.com/test1/test2 . Without # sign.
And as I understand this script does it. But I'm sorry I'm a littlebit novice in scripting and programing , so I couldn't understand the usage of it. Also I see it's a bit powerfull with lots of functions in it. But I need only basics.

Is there any light version of this script which can only detect if back or forward button was pressed , and fire alert if so. That's all I want. I've done the rest using my own scripting. What I need is just this.
If user is on mysite.com pressing some link for example inside link is "test1" then something must be triggered address bar must be changed to mysite.com/test1 and the rest will does my script.

Also if user is already on mysite.com/test1 pressing "back" button then address must change to mysite.com and then my script must be fired.

Is it easy to make and can you help me with such code , or I must study the whole project ?

@knowntobe

Hi all
First, like others, I find this script awesome. Was really struggling with other history scripts but this works great. That said, Im having trouble with forms, my form resubmits the page. Is this correct?

would also be useful to maybe add something into the script to put &ajax=yes or similar onto links so the called page could be stripped out of unneeded things, server side, as long as the url didnt change, this is the part Im struggling to do.

Additionally is it possible to make the script reparse links? I have a shopping basket on my page and it doesnt "see" new things added to it unless the page is refreshed, which ruins the ajax smoothness.

@peterwilsoncc

To avoid ajaxing in page (href="#*) links, change
$this.find('a:internal:not(.no-ajaxy)') to $this.find('a:internal:not(.no-ajaxy,[href^="#"])')

This prevented problems I was having with a skip to content link, <a href="#content"...

@knowntobe

can you get it so take over form submits without page refreshes?

the problem I have with it not parsing links is links added to the page by additional JavaScript i.e. on the fly. Anything that this plugin adds is hooked correctly.

@chmig29fulcrum

guys please tell me how to set up simple script to grab href source when pressed, put it into addressbar, and if user presses back fire some action with old source code. I don't need anything else

@pokonski
@knowntobe

sorry for all the question spam >< but I have another (aswell as the forms one, and parsing java added links)
I have been testing this in FF, Chrome, and IE 9. FF and Chrome work great, every part works inc bookmarking. However, IE 9 is causing issues. When using the page it works fine, as it should but when you save a bookmark it saved "http://doman.com/index.asp#details.asp?id=99715" if I try to then view the details page from a saved bookmark, it just loads the index page, ignoring the hash stuff. Obviously FF and Chrome dont have this problem because they dont use the #. To reproduce this :
browse site as you would, save one to favs. Close browser (if you leave it open the links load as they should). Load bookmark. It loads the main page. If you then try the bookmark again, it loads the correct page.

@chmig29fulcrum

trying to add this script to my page...

missing variable name
[Break On This Error] = $(contentSelector).filter(':first'),

This comes when initializing variables...

@thomasgg
@knowntobe

will chuck another question into the mix, although none of my other ones got answered :s
anyway, aside from my issues above, namely forms, I got this working well, until I decided to do some URL Rewriting. This seems to have broken the history stuff. i.e. /folder/index.asp?value=1 works great, add some url rewriting so it looks like /folder/cool_file_name1.asp and the history stops working, and instead reloads the page, correctly loading the url but not fading in/out the content as it should
Any ideas? the correct content is being sent out based on the URL but for some reason the history has trouble with it and causes a normal page load instead

@balupton
Owner

@knowntobe can you please provide a sample url to reproduce the issue, the version of History.js you are using, the OS and version you are using, and the browser and version you are using. This will allow us to help you out.

@knowntobe

I can yes but do you have an address I can email details to? the site Im working on us pass protected currently as its under development.
Browser tried are IE9, FF latest, Chrome Latest. History Im using is ajaxify-html5.js on my site, latest download, and linked to your copy of /jquery.history.js

Further info on the above issue. It appears the history starts, and tries to load cool_file_name1.asp (content area fades out), but then redirects to its actual name of index.asp?value=1 instead.

@knowntobe

the cool_name issue seems to be resolved, I think it was because I edited the gist ajax call to include a random number (had caching issues in IE), once I took this out it started to work fine. I havent tested things like details.asp?value=1 being changed to details.asp/value/1 yet. But the main part is working. if I could just get it to work with forms, and links added via ajax (after history.js has loaded a page), I would be all sorted.
Its truely awesome though, love this thing :)

@knowntobe

been doing some testing in various versions of IE, and IE 7 seems to have issues with :
scriptNode.appendChild(document.createTextNode(scriptText));
which is line 158 of ajaxify-html5.js
from some googling, I found a stackoverflow post here http://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml
which suggests a workaround. Not got it to work myself yet but thought I would post.


this is my current code, working in all versions of IE now
// Add the scripts
$scripts.each(function(){
var $script = $(this), scriptText = $script.html(), scriptNode = document.createElement('script');
// scriptNode.appendChild(document.createTextNode(scriptText));
// contentNode.appendChild(scriptNode);
try {
// doesn't work on ie...
scriptNode.appendChild(document.createTextNode(scriptText));
contentNode.appendChild(scriptNode);

} catch(e) {
// IE has funky script nodes
scriptNode.text = scriptText;
contentNode.appendChild(scriptNode);
}
});

obviously remove the commented lines, left in for reference

@jetlej

1) I second the previous question about using this to AJAX-ify forms. Any easy way to do that?

2) I'm having issues having other jQuery scripts work for content that is loaded by this script. Can you please give some instructions on how to use the built-in script loading functions?

@knowntobe

for my forms I did use Jquery to load the content but the AJAX-ify then doesnt see the new content and so doesnt hook into the links, which means a full page refresh. I assume this is the same as the above issue.

@craigmdennis

@knowntobe @jetlej The reason for this the that the event hooks are set on the elements that are present on page load and not onto new elements loaded into the DOM. You can get around this by using .live() to bind event handlers that listen for newly added content too. So instead on $('#myElement').click( function(){ // do stuff }); you would use $('#myElement').live('click', function(){ // do stuff });

@jetlej
@jetlej

Just noticed that if I click an ajaxified link, and then click another link before the former has a chance to load AND the latter link loads faster than the former, it will display the latter and then load the former over it when it's finished loading. Where can I place a stop() function to prevent this?

@knowntobe

Thanks Craig, just having a look through the ajaxift-html5.js file and found $this.find('a:internal:not(.no-ajaxy,[href^="#"])').click(function(event){ so will edit this and see how it goes

edit

works a treat \o/ so pleased
Just the form issue to try and solve now

@craigmdennis

@jetlej Yeh 1.7 uses .on() in place of bind, live and delegate so it's a unified event handler. It also adds innerShiv which solved the ajax issue of loading HTML5 content in non-HTML5 browsers. At the time of posting it wasn't hosted on Google's CDN so I didn't want to muddy the waters.

@knowntobe

fixed my form problem by writing a Jquery ajax function to send the form search to the content div, then using .live in the Ajaxify file making it parse the newly loaded links. Awesome stuff. Now another issue
I have paged results, for example this is one link+query /index.asp/page/14/offset/10. Some how, the ajax is sending repeated url changes, so it ends up like this :
http://www.domain.com/property/index.asp/page/14/offset/10#index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/14/offset/index.asp/page/16/offset/10
basically each part from index.asp to the end of the querystring is repeated constantly until the browser calls this error :
Line: 1
Error: Access is denied.
jquery.history.js, line 1 character 15681
The code the IE debugger highlights is this :
return b !== !1 && m.busy() ? (m.pushQueue({
scope: m,
callback: m.setHash,
args: arguments,
queue: b
}), !1) : (c = m.escapeHash(a), m.busy(!0), e = m.extractState(a, !0), e && !m.emulated.pushState ? m.pushState(e.data, e.title, e.url, !1) : d.location.hash !== c && (m.bugs.setHash ? (f = m.getPageUrl(), m.pushState(null, null, f + "#" + c, !1)) : d.location.hash = c), m)

This is in IE8 and 9. Chrome and FF seem unaffected. Turning off url rewriting fixes it, or appears to at least. The rewrite rule is :
RewriteRule ^(.?.asp)/([^/])/([^/]*)(/.+)? $1$4?$2=$3 [NC,LP,QSA]
turns index.asp?value=1&field=2 etc into index.asp/value/1/field/1

I know this isnt a fix my site section, just posting in case this is a real overlooked issue. Meanwhile I'll go back to trying to find a fix in case its just me thats broken it.

@jetlej

I've added the class 'document-script' to my scripts being loaded in the ajax content and they're being parsed and re-added but not executed! Anyone know what I'm doing wrong??

@AndrejVasso

I have got a serious issues and unfortunately I dont have the knowledge to solve it myself and would be happy if some1 can take a look at this:

When using a HTML4 browser (in my case its IE8) and I try to navigate directly to an "ajax sub-page", for instance by going to "hxxp://example.com/index.html#sub-page.html" - the AJAX does not fire off and I get to see the content of "index.html" instead of "sub-page.html". The same happens when I reload the page: instead of seeing the content of "sub-page.html", I see the content of "index.html"

I put it online to an testing host: http://test-this.allalla.com

Try to visit http://test-this.allalla.com/index.html#ajax1.html with a HTML4 Browser and you will see the content of "index.html" instead of "ajax1.html".

I didnt change anything in the script, besides the selectors for the content and navigation. Some more infos:

OS: Windows 7
Browser: IE8
History script: html4+html5 jquery.history.js [newest version downloaded from here]

@loickreitmann

Regarding the conversion of script tags to div.document-script tags, I've come up with a fix. The issue I had was related to tweet and facebook like buttons no longer working when the content was loading with AJAX. I modified the $scripts detachment block as follows:

// Fetch the scripts
$scripts = $dataContent.find('.document-script');
$scripts.each(function () {
    if ($(this).text().match(/(twitter|facebook)/i)) {
        $(this).replaceWith('<script type="text/javascript">' + $(this).text() + '</script>');
    }
});
if ( $scripts.length ) {
    $scripts.detach();
}
@asawilliams

@blocosted if you have not solved it already, you need to disable caching on all you ajax partial requests. The back and forward buttons load content that is in the history. That is why you are only seeing the partial. This answer in Stackoverflow shows how to disable the caching: http://stackoverflow.com/questions/985530/best-way-to-disable-client-caching

@knowntobe

@loickreitmann
would your edit be workable for googlemaps? Im having issues getting mine to display.

@loickreitmann

@knowntobe
It might work. Try the following:

// Fetch the scripts
$scripts = $dataContent.find('.document-script');
$scripts.each(function () {
    if ($(this).text().match(/(google)/i)) {
        $(this).replaceWith('<script type="text/javascript">' + $(this).text() + '</script>');
    }
});
if ( $scripts.length ) {
    $scripts.detach();
}
@matteodepalo

I'm trying to make this gist work for my embeddable ajax widget. Is there any way to modify it in order to make it work with an external root url? In other words I need a way to implement a browsing history of my widget on another website. Pull every url from my website, strip it of the root url, and push the state on the embedding website. Right now I'm not using this gist but I'm using History.js with this code in my widget. This almost works but has a lot of problems like not having the possibility to bookmark the url.

$("a").live("click", function() {
  $.getScript(this.href + ".js");
  var relativeUrl = this.href.replace('mydomain', '');
  History.pushState(null, null, relativeUrl);
  return false;
});

Please tell me if I'm totally off the road, I'm kinda new at this stuff and I apologize in advance. I'm using Rails 3.

@christianslater

Really cool! I tried to implement this in a wordpress theme. When I embed the scripts from the official servers like in the installation described, everything works fine. When I download a copy of the ajaxify-html5.js and link this in my head, the script does not work. How come? I mean it is the same. Sorrs, but I have not so much experience in coding and using scripts.

@DarrenFung

First of all, awesome gist! I just have a couple questions as I don't have much experience in the client-side scripting portion of web apps. First, is it bad practice to, in this script, load scripts that are in the "head" portion of the site as well? I ask because I have a framework that loads scripts into this portion of the site. Secondly, to have each "page load" call a certain function, I have everything in a "function pageLoad()", and this gets called at the end of the script. But, this doesn't work if I just put "pageLoad()" at the end of the gist [as it relies on scripts loaded through this gist?]. To work around this, I have a setTimeout to a certain time (1ms in my case, which doesn't work particularly well). Is there a better way to do this?

@knowntobe

to use an example.
Put ALL your includes into the head of index.php
Assume you have a div with id "content" inside your index page, all your sub pages will be loaded, via ajax, into this div. So all the sub page functions will work as if they were written directly into index.php (since all your includes are in this head).
For your function, you have 2 choices.
1. at the end of each sub page, call your page_load, this will fire once ajax has loaded your subpage into your content div, do this for each sub page
2. in ajaxify-html5.js, roughly line 186, look for "if ( $body.ScrollTo||false )" after this line put your page_load call. This will fire after each ajax call has completed, this is what I do.

you also probably want to find :
$this.find('a:internal:not(.no-ajaxy)').click(function(event){
and change to :
$this.find('a:internal:not(.no-ajaxy,[href^="#"])').live('click', function(event){
so it ignores page anchors, and also works on some ajax related page changes.

note : Im by no means an expect, see my questions above. This is just what I've done, and found works for me, might not be best practice.

@DarrenFung

Thanks for the quick reply, I'll try it out!

@DarrenFung

I just tried it out, and say I load the first page, "Home", and then I go to a second page "Users". Home will have one version of pageLoad, and Users will have the other. If I do #2, it still fires the first version of pageLoad (the Home version) instead of the Users version. I tried to do something like window['pageLoad'] = function() {} instead, but haven't had any luck getting it to work

@knowntobe
@christianslater

Hi,
I managed to use this in a wordpress theme. Two things I can't get to work. First of all I wanted to fade in the the new content and replaced the line: $content.html(contentHtml).ajaxify().css('opacity',100).show(); with $content.html(contentHtml).ajaxify().css('opacity',100).fadeIn(800); But this does not work. Another Problem is removing the active class from the current menu item. Wordpress gives the current menu item the class "current-menu-item". So when I load another page the new page is highlighted as it should, but the old page stays highlighted as well. So the class is not removed correctly.

Can anyone help?

@craigmdennis

@christianslater Try adding the wordpress menu classes to

activeClass = 'active selected current youarehere',
activeSelector = '.active,.selected,.current,.youarehere' 
@christianslater

@craigmdennis Thats what I did

activeClass = 'current-menu-item',
activeSelector = '.current-menu-item' 

The problem is, that the script is changing the class name to "current-menu-item" when I click on it, but does nit remove the class from the recent item. Take a look at this page: www.schauundhorch.de/wordpress/?page_id=10

any idea how to get the changed content fade in?

regards christianslater

@craigmdennis

@christianslater Well Wordpress uses many different class names to denote the active state and the hierarchy, in your case the class is current_page_item and not current-menu-item so maybe that's the issue. Bare in mind that this class is just for pages and differs for posts and sub pages.

@craftedpixelz

Those having issues with forms - It is possible to have AJAX-based forms, you just need to re-execute the code after the AJAX for the page is complete.

Create a function that houses the code for your AJAX-based form, then call that function in the success callback of the AJAX load used in this Gist. The same goes for if you need to reinitialize plugins upon entering a page.

@christianslater

Hey, I managed to get the current stat change of the menu working. But I can't get the content fade in. Can someone help me?! here's my test page: http://schauundhorch.de/wordpress/

@christianslater

Ah, I got it. I don't know if it's correctly used, but it is fading in the content now!

I changed this line: $content.html(contentHtml).ajaxify().css('opacity',100).show(); to this $content.html(contentHtml).ajaxify().css('opacity',100).hide();

and added this line afterwards: $content.fadeIn(800);

now it's working!

@christianslater

unfortunately the scrollTo function is not working anylonger. I think because I hide the content before I fade in the function can not detect the size of the content an does not scroll to top! Any other idea how to solve this?

regards chris

@craftedpixelz

Instead of .show() and .hide(), perhaps try using .css('visiblility', visible') and .css('visiblity', 'hidden') ?

@christianslater

Thanks! "visibility" did the trick. here's my line of code:

$content.html(contentHtml).ajaxify().css({opacity: 0.0, visibility: "visible"}).animate({opacity: 1.0},800);

@scruffian

this is amazing.

one small thing, shouldn't it be: css('opacity',1)

@broucz

nice job ! really :)

@alfonsosiloniz

Hi

Thank you for this great gist. It is amazing

But I have find that doesn't work properly with Safari Versión 5.0.2 (6533.18.5) It works perfect on Chrome, Safari and even Internet Explorer 7

The thing I have observed with Safari is that event statechanged is triggered twice. The first one when the user clicks on an internal link (url variable is set to the new url), and a second time with the url variable still set to the older url. Making this to work erratic.

You can take a look at http://testwp1.alfonsosiloniz.es/wp2/home/

@broucz

Work fine here on Safari 5.1.3... I can't test on 5.0.2 sorry

@alfonsosiloniz
@omarvelous

Got this working great, only issue is it's not updating Meta tags, any assistance on how to go about integrating that?

@scruffian

Omar, look at lines 124 and 154 where the scripts are detached and then added. You should be able to follow a similar pattern for tags. Is it really worth bothering though? Isn't that data only for machines, which won't use this method of loading pages anyway?

@omarvelous

@scruffian Hmmm, let me try some more tests.... I was originally trying to accomodate the Facebook Like Button.... But I wonder if it's using the current pages meta actually or retrieving it on their end.... Good Point.

@scruffian

@omarvelous I also need to include meta tags for facebook, but facebook won't crawl your site like a user, it picks up the meta tags directly from the URL - you can see what it gets from the URL linter:

http://developers.facebook.com/tools/debug

So I don't think you need to do this to support facebook...

@jetlej

In case anyone would like to be able to also append external scripts using the .document-script class, you can modify the appending of the scripts code to look like this:

//Add the scripts
$scripts.each(function(){
var $script = $(this), scriptText = $script.text(), scriptSrc = $script.attr('src'), scriptNode = document.createElement('script');
if(scriptSrc) {
scriptNode.src = scriptSrc;
contentNode.appendChild(scriptNode);
}
else{
scriptNode.appendChild(document.createTextNode(scriptText));
contentNode.appendChild(scriptNode);
}
});

@davidultra

This whole concept is unusable in IE < 9 because script insertion from reading text on a page is bugged in those browsers. Every method of inserting text into a script node mentioned here has its problems.

If you use this, don't depend on script insertion at all or provide a fallback for older versions of IE if you need code to work everywhere.

@shmelman

I wanted to ask if its possible to add a loading indicator that shows up in between page loads..

Thanks!

@pokonski

@shmelman, you expect too much. This won't magically add indicators. This is your part. At line 106 it adds .loading class for <body>. Just style it appropriately.

@scruffian

Is it possible to prevent this scrolling to the top of the content when the user has clicked the back button? In this case this behaviour is not desirable. Is it possible to tell if the event is a "back" event inside the statechange event listener?

@scruffian

This is how I achieved it. I tried modifying the State object to do it this, but it ended up getting really messy, so instead I just defined a new variable and used the id of the state to store the current scrolling position for that state. It's simple but it should work. Just add this around line 83:

            var Scroll = [];

            //Add scroll position for this state
            Scroll[currentState.id] = $body.scrollTop();

And add this in the statechange event around line 162...

            //get the scroll position from the scroll object, if it exists.
            if (typeof (Scroll[State.id]) !== 'undefined') {
                top = Scroll[State.id];
            }
            $('html, body').animate({scrollTop: top}, 300);
@scruffian

Also, can anyone shed any light on the purpose of the closure around this gist? Thanks...

@pokonski

@scruffian, I believe this is just a precaution to separate the code from the rest of your Javascript.

@knowntobe

@ scruffian
Can you explain where exactly you put the code for the scrolling "fix"? I've edited mine a lot so "around line.." is messing things up as I cant find the correct location.

@scruffian

@knowntobe
i have changed the code a bit since that post, so it's probably better to explain what I have done and how it works, so that you understand the concept and can incorporate it into your code. The idea is to extend the History object with a new property - i called it Scroll. After you define the variables I would put:

History.Scroll = {}; // it should be an object not an array as I put above.

Then in the click handler which is assigned to each link you need to get the id of the state of the page that you are currently on (each state has an id associated with it), like this: currentState = History.getState();

You can use this id to save the current scroll position of this state, ready for using later on, like this: History.Scroll[currentState.id] = $body.scrollTop();

Then in the "statechange" event listener, where the scrollTo function is used, you can retrieve the scroll position that you saved earlier in the History.Scroll object, like this: top = History.Scroll[State.id];

Of course this may not exist so you need to wrap it with the if statement to check it exists, otherwise you should fall back to whatever mechanism you were using before to work out where to scroll to.

This all works fine, but it creates one strange behaviour, which is that if you click a link you have been to before, you will be scrolled to the position you were at last time you visited the page, which is unexpected and undesirable (probably). It's easy to resolve this though, you just need to remove the scroll position for any states that are visited via a click, rather than via back or forwards buttons. You can just add this to the click event handler:

            //get the full url for the next page to go to
            nextFullUrl = History.getFullUrl(url),
            //get the id for the state of the next page 
            nextId = History.getIdByUrl(nextFullUrl);
            //if the id exists (it always should, but just in case)
            if (typeof (nextId) !== 'undefined') {
                History.Scroll[nextId] = 0; // i just reset to zero, but actually it's probably better to remove this from the object altogether
            }

Hope this helps. If you have any more questions let me know...

@knowntobe

Thanks for the detailed reply, I shall give it a go when I get some time

Meanwhile, is anything having issues with pressing Back before the page fully loads? On a site Im testing this on, image heavy, it doesnt do anything, the url changes but the content doesnt. This doesnt happen 100% of the time, I think it depends how much of the new page has downloaded (unless something else is causing it to "stick"). A fully downloaded page doesnt have issues, where as one thats fresh, and only been opened for a few seconds does.

@clafleche

I've been working with this and have found it to be great, but one problem I'm having is triggering a separate .js again once the content has loaded or when I go back or forward in the history. Essentially, I have a .js that controls the visibility of some objects on the page as well as some of the menu styling, and I need it to fire every time I change pages. Sorry if this is a crazy dumb question.

@knowntobe
@clafleche

@knowntobe Saw that just now... whoops. Sorry. Figured it out, and everything is running well in Safari, Chrome, and Mac FF.

I've encountered another strange problem. In FF 3.6.3 (Win) and IE 9 (and probably older versions too), the ajax link paths are being appended to the root as #content/page1, #content/page2, etc. and all of the text content loads, embedded scripts execute, everything is fine... except that image paths are broken (I'm loading slideshows into some pages). I could go in and give all the images full paths, but I'm hoping there's an easier solution.

Possibly related: in my menu I have a li that is linked to "#" and when clicked opens a hidden ul (and the ajaxify ignores it using the solution provided above). But in FF (Win) and IE it points back to _root/# regardless of what page I'm on, throwing everything into a mess. What's going on?

@aspiziri

For those that are using Google Analytics and are having trouble with the above code actually tracking their ajax calls there is a quick fix for this. The following line in the current code is designed for the old analytics tracking code:

if ( typeof window.pageTracker !== 'undefined' ) {
     window.pageTracker._trackPageview(relativeUrl);
}

In order for the tracking to work with the most current tracking code you need to update to:

_gaq.push(['_trackPageview', relativeUrl]);
@DjCash

Thank you for your very useful work, I have a question from a js noob:

How can I modify the body id of each loaded content?

I tried using the following in the body of each page:

document.getElementsByTagName('body')[0].setAttribute('id','green');

('green' is the id I need to apply)
it kinda works but I don't think it's the correct way, also it seems to load the body id AFTER the css file so some styles won't update according to the page body ID.

Thank you

@zukilover

Hi, i got this problem. When i click on an ajaxify link, i need to add the link object into pushState, but the i got a message like: Uncaught TypeError: Converting circular structure to JSON . How can i add the link object and save it as a State object?

@envision

Could some please write a real-world practical example - or at least tell me and those after me - with what kind of site structure this demo is supposed to be working?

I used the whole yesterday trying to applying this to my site only to discover at night that I had it all wrong...

I assumed this was using links like /sub/sub/ and when JS visitors enabled would convert them / fetch content dynamically from source at corresponding location folder with index-file.

My site under dev (the violet section) can be found at http://envision.fi/lab/ for some time, and hopefully soon will be published at root :)

@vineonardo

Hello,
Thanks for this great plugin.
I'm facing some problems while using it with Twitter's bootstrap.
This plugin seems to add "#" url for internal links which previously were not showing #example-id in the url.
You can see what I'm trying to say in following fiddle:
http://jsfiddle.net/vineonardo/PL2Yv/2/
Just comment out the history pluging, the carousel will start working again.

Can you help me with this? I'm a complete newbie at jquery. Thanks for your time.

@knowntobe
@vineonardo

Hello,
Thanks again for the great plugin.
While working on a single page website, I found a problem that whenever after using internal links like http://example.com/#work, if I open a link which is of an external page like http://example.com/work/article-1, it loads the page nicely, but then the back button doesnt work.
It's supposed to take user back to http://example.com/#work, but instead, only url changes in the address box but the content loaded doesnt change back to original.
I'm sure I'm missing out something essential, if possible, someone kindly enlighten me.
Thanks.

Regards,
Vineonardo

EDIT: Ohkay, I just found out about the class="no-ajaxy" but, in single page websites, if user is on the bottom of the page when I click the external url, the back button should bring user back to that portion of the page, as it happens in normal webpages.
Any comments on this guys?

EDIT #2: @knowntobe: Yeah, I read a few comments. I'm really being very lazy lately :p. I just learned about contentSelector which seems to work the way I want it to work. When opened the new link, and pressed back button, the element specified in contentSelector is shown. In my case it'd be

.

Thanks buddy for your help.

@knowntobe
@slikk66

I see the comment above from @roc about $(document).ready() not firing properly, I seem to have the same issues. Is this a known issue with this ajaxify + history setup? Other than that, it's been pretty much flawless.. I'm really happy with it.

So, on my pages that have special commands in $(document).ready() just for the one page.. if I ajaxify browse to the page, I don't get the commands running. But, If I do a control+R on the page to refresh, then they run. In fact I can't seem to get any javascript that I embed into this one particular page to fire if I ajax browse to it. But I'm not getting any type of JS error in the console either, so I'm sort of confused what the problem is.

Is document.ready supposed to work? Or how about if I link to an external JS file on that view, is that supposed to work also?

Thanks!

@knowntobe
@slikk66

@knowntobe ok, but then I end up with ton of javascript in the main js file, essentially all js is sitewide instead of on a particular view.. I suppose it's not the end of the world, but that's the solution? I can live with that, wanted to make sure I wasn't wasting time if it's a known issue.. which guess it looks like it is. @roc also had a pretty good solution with the "finished loading" callback idea. I just found some good minifying JS scripts.. so i suppose compressing all JS is just as good of an answer: http://yearofmoo.com/2011/04/minify-css-and-js-with-git-hooks/

@knowntobe
@knowntobe

Is anyone having issues with google analytics not tracking ajaxy pages? my google code is :
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-xxxxxxxx-x']);
_gaq.push(['_setDomainName', 'Domain.com']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();

and in my ajaxify-html5.js I have :
// Inform Google Analytics of the change
if ( typeof window.pageTracker !== 'undefined' ) {
window.pageTracker._gaq.push(['_trackPageview', relativeUrl]);
}

using googles live view, I can watch myself moving around the site as long as I dont use ajaxy pages. If I do, the urls dont update on the tracking and it looks like Im staying on 1 page.
I installed the debug extension for chrome and get this error
Uncaught ReferenceError: blockreferrer is not defined
(anonymous function)
not sure what it means yet though but its shown on every Ajaxy link clicked.

@aspiziri

@knowntobe Take a look at my comment posted ~ 2 months back above. The code in this gist is using the old version of the Google Analytics code. You need to use the new version. My comment shows you what code to replace.

@knowntobe
@knowntobe

Further playing, if I change
if ( typeof window.pageTracker !== 'undefined' ) {
_gaq.push(['_trackPageview', relativeUrl]);
}
to just :
_gaq.push(['_trackPageview', relativeUrl]);

google tracking is updating (using analytics live view) the url the visitor is on, which is good but I dont know the negative effects of this or if something else is causing the block. I dont know enough about it and Im not getting any errors.

edit
@aspiziri, on reflection, your earlier post, did you mean replace all 3 lines with the new one? I Thought you meant just replace the 2nd line (of the 3) which is what I did when you made your post. Now I've replaced all 3 with the new 1, its working. I guess thats what you meant? now I see it like that its obvious, just the "The following line " comment threw me off /facepalm

@mamzellejuu

Thank you, really useful especially your documentHtml function!

@arfaRed

Hi,

Thanks for this very efficient script.
I need to get some meta tags from the head section of the page being called using ajax and replace the existing page's meta tags.
These are meta tags for social sharing like og:title, og:url, etc.
Can you please explain how I can get this done.
Let me know if you need any clarification on the issue.

@scruffian

@arfaRed no you don't. refer to previous comments on the matter...

@stuartz

I am submitting a form through a function rather than form submit and appending a table using the returned JSON into #information div. I then use jquery to hide(#myform) and show(#information). I am attempting to use history.js to provide back button capability. When using the back button, I can see that the title returns from /?state1 back to /. However, the content remains the same with #myform hidden and #information showing.

Is there a way to store the view and restore it when using the back button.

@stuartz

FYI solution for others who might have a similar issue...

I disabled the hide()/ show(), deleteded my #information div, and used the following to change the #content div in my javascript that is called to run the form submit:

                var
                History = window.History;
                History.Adapter.bind(window, 'statechange', function() {
                    var State = History.getState(); 
                    $('#content').empty().append(State.data.content);
                });

                /* to store starting state, otherwise it returns to an empty #content*/
                var content = $('#content').html(), State = History.getState();
                History.pushState({content: content}, State.title, State.url);

                                    /*parse returned json and put into table*/
                                                                             table += '</table>';
                                    /*insert table into #content with a new stored state*/
                                    History.pushState({content: table}, "Selection", "?state=2");

However, it now does not work with my jquery mobile which is activated when screen>799. When submitting the form, it fails and console shows "TypeError: c.originalEvent is undefined"

@arfaRed

Hi,

Came across the following 2 errors in IE7 when I implemented your script.
1. SCRIPT5007: Unable to get value of the property 'enabled': object is null or undefined
ajaxify-html5.js, line 11 character 2
This error comes when the index page & script is loaded.
2. SCRIPT65535: Unexpected call to method or property access.
ajaxify-html5.js, line 158 character 7
This error comes when I click on a link to load the content.

Please guide me through this.
You have created an awesome script.

Thanks

@arfaRed

Sorry the two errors mentioned above are for the following code of lines

  1. if ( !History.enabled ) {
  2. scriptNode.appendChild(document.createTextNode(scriptText));
@slikk66

just like to share something I found while working with this.. I use cakePHP which uses named parameters in a fashion like this - http://site.com/controller/action/param1:val1/param2:val2

i found that with this script, it was failing this check and would not ajaxify any links that had the : in the URL. I confirmed this by checking 2 identical links next to eachother in same place, same classes, and only diff was one had the ":" and one didn't. (i.e. /controller/action/param/2 vs /controller/action/param:2)

$this.find('a:internal:not(.no-ajaxy)').click(function(event){

My only guess is that the "internal" selector must see the ":" in the parameters values and think it's an external link..(i.e. http://) that's the only thing I can figure.. So I changed it to:

$this.find('a:not(.no-ajaxy)').click(function(event){

And now I will just add class no-ajaxy to external links also.. hope this helps someone.

@owlyowl

Hi just wondering how i'd load in scripts from the other pages I'm loading in?
I saw the lines: $dataContent.find('.document-script')

just wondering how I'd apply that to script blocks.. i've tried adding a class of document-script to scripts inside my content containers that im loading in but they're still not carried through

@sieppl

Great stuff. Please check (and merge) my gist: https://gist.github.com/4041997
It adds that also all links where any parent has the class "no-ajaxy" are exluded. Makes excluding e.g. navbars entirely more comfortable.

@fahimshani

Hi just wondering how to make it work if the page struture is like (index.html, about.html, and bla bla) in same folder, do i have to make some configuration in the gist, sorry for the silly question but i am trying for last 2 days to make it work for static website pure html

Thanks

@juji

i use ajaxify-html5.js, and have inline scripts in my html.

<script>
    for( var i = 0; i < 10; i++ ){ }
</script>

resulted in an error...

so i change the code.. (it's a mess though..)

i wonder if anyone have a better solution, i really need the help...

the code (edited):
the followings are inside the $window.bind('statechange',function(){ ...
starting from line 117

    // Ajax Request the Traditional Page
$.ajax({
    url: url,
    success: function(data, textStatus, jqXHR){
        // get inline script
        var body = data.match(/<body[^>]*?>[^]*?<\/body>/img);
        var inlinescript = body[0].match(/<script[^>]*?>[^]*?<\/script>/img);

        // replace inline script with '', 
        data = data.replace(/<script[^>]*?>[^]*?<\/script>/img,'');

        // Prepare
        var
            $data = $(documentHtml(data)),
            $dataBody = $data.find('.document-body:first'),
            $dataContent = $dataBody.find(contentSelector).filter(':first'),
            $menuChildren, contentHtml, $scripts;

        ...

        // Add the scripts
        $scripts.each(function(){
                var $script = $(this), scriptText = $script.text(), 
                scriptNode = document.createElement('script');
                scriptNode.appendChild(document.createTextNode(scriptText));
                contentNode.appendChild(scriptNode);
        });

        // add the inline script
        $(inlinescript).each(function(){
            var scriptText = this.replace(/<script[^]*?>/,'').replace(/<\/script>/,''), 
            scriptNode = document.createElement('script');
            scriptNode.appendChild(document.createTextNode(scriptText));
            contentNode.appendChild(scriptNode);
        });

        ...
    } //end of ajax success function

:)

@greencmg

This a great script! Love it!

I did run into an issue when implemented with the new Google Tag Manager (GTM) with Google Analytics. If using GTM the _gaq.push event still fires but there is no link to an account id. You will see the event fire but it's posting to UA-XXXXX-XX. To quickly work around this, in GTM, I created a rule based on {event} that equals a value of 'pageView'. Then in the ajaxify-html5.js script I changed the following starting at 172:

                // Inform Google Analytics of the change
                if ( typeof window._gaq !== 'undefined' ) {
                    window._gaq.push(['_trackPageview', relativeUrl]);
                }

Change to:

                if ( typeof window._gaq !== 'undefined' ) {
                    dataLayer.push({'event':'pageView'});
                } 

Pageviews are executing normally now. For more info on Google Tag Manager -- check out http://support.google.com/tagmanager/

@drivebass

Great script! I have a problem with my home page though that is loading in the container if i hit the back button. What i want is every page except my home page to load inside the container. Is this possible to exclude a page from loading in the container div?

@drivebass

Is it possible to exclude specific strings like wp-admin.php, wp-login.php, index.php etc? How can i add a function that when i visit the home page (including with browser's back button) will just hide the $content div?

@drivebass

Help anyone i desperately need a solution on this. Please!

@jerryjack

should note that this script throws an error if using jquery v1.9.0(latest version)

@genereddick

Error under jquery 1.9.0 is on $data = $(documentHtml(data)), and, after failing to figure out the selector, ends up here:
Sizzle.error = function( msg ) {
throw new Error( "Syntax error, unrecognized expression: " + msg );
};

@genereddick

In the jQuery 1.9 change log there is this: http://jquery.com/upgrade-guide/1.9/#jquery-htmlstring-versus-jquery-selectorstring

In my case, the response returned from documentHtml(data) doesn't start with a < (in my case it is a conditional for modernizr) is rejected by the new jquery selector parser.

changing the var documentHtml = function(html) to return $.parseHtml seems to work for me, though I haven't explored any follow on effects or issues.

// Return
return $.parseHTML(result);

@daslicht

You can also use Davis.js to Ajaxify/hijax you webpage:
see: http://78.47.126.11:3000

@supermensa

As far as I can see; the script makes an ajax call for the browser back-button and previously clicked links. Can you update this gist to show how to ensure that saved states/pages are retrieved from history instead of making a new ajax request?

@alexlangberg

The fix from genereddick works but ScrollTo is still broken with new versions of jQuery, since $.browser has been deprecated. I've tried including the old code for $.browser but it only seems to be working in Firefox.

@PepiSCZ

Why already loaded javascript is not applied to new content in content div?

@Zeokat

Zeokat you saved my day! thanks. Nice piece of code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.