Skip to content

Instantly share code, notes, and snippets.

@balupton
Last active April 20, 2022 13:21
Show Gist options
  • Save balupton/854622 to your computer and use it in GitHub Desktop.
Save balupton/854622 to your computer and use it in GitHub Desktop.
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
@knowntobe
Copy link

knowntobe commented Jul 15, 2012 via email

@knowntobe
Copy link

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
Copy link

Thank you, really useful especially your documentHtml function!

@arfaRed
Copy link

arfaRed commented Jul 19, 2012

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
Copy link

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

@stuartz
Copy link

stuartz commented Aug 29, 2012

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
Copy link

stuartz commented Aug 30, 2012

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
Copy link

arfaRed commented Sep 10, 2012

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
Copy link

arfaRed commented Sep 10, 2012

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

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

@slikk66
Copy link

slikk66 commented Sep 21, 2012

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
Copy link

owlyowl commented Oct 22, 2012

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
Copy link

sieppl commented Nov 8, 2012

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
Copy link

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
Copy link

juji commented Dec 19, 2012

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
Copy link

greencmg commented Jan 3, 2013

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
Copy link

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
Copy link

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
Copy link

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

@jerryjack
Copy link

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

@genereddick
Copy link

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
Copy link

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
Copy link

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

@supermensa
Copy link

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
Copy link

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
Copy link

PepiSCZ commented Mar 9, 2013

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

@Zeokat
Copy link

Zeokat commented Mar 1, 2014

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

@Shobby
Copy link

Shobby commented Nov 9, 2016

I am facing an issue. I want to run jaquery on the loaded page but it not working because the page is not loaded and jquery not hit i want to show an alert when the page is loaded but not work:
jQuery(document).ready(function() { alert("hello world"); )};

i put this code in footer file but not working

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment