Skip to content

Instantly share code, notes, and snippets.

@jouni-kantola
Forked from balupton/ajaxify-html5-native.js
Last active July 14, 2022 19:28
Show Gist options
  • Save jouni-kantola/8630279 to your computer and use it in GitHub Desktop.
Save jouni-kantola/8630279 to your computer and use it in GitHub Desktop.

This gist will ajaxify your website with the HTML5 History API and ScrollTo.

WARNING!!!

This gist is for demonstration purposes only as it uses the native implementation of the HTML5 History API which means that if you were to implement this, then you would experience cross-browser compatibility issues even when using all the modern browsers. This is because all browsers, even the modern ones implement the HTML5 History API differently. If you are considering using the HTML5 History API in your project, then use the gist here instead which implements History.js a project which provides a cross-browser compatible interface for the HTML5 History API.

Installation

<!-- jQuery --> 
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script> 

<!-- jQuery ScrollTo Plugin -->
<script defer src="http://github.com/balupton/jquery-scrollto/raw/master/scripts/jquery.scrollto.min.js"></script>

<!-- Gist --> 
<script defer src="http://gist.github.com/raw/1145804/ajaxify-html5-native.js"></script> 

Explanation

What do the installation instructions do?

  1. Load in jQuery
  2. Load in the jQuery ScrollTo Plugin allowing our ajaxify gist to scroll nicely and smoothly to the new loaded in content
  3. Load in this gist :-)

What does this gist do?

  1. Check if the HTML5 History API is enabled for our current browser, if it isn't then skip this gist.

  2. Create a way to detect our page's root url, so we can compare our links against it.

  3. Create a way to convert the ajax repsonse into a format jQuery will understand - as jQuery is only made to handle elements which go inside the body element, not elements made for the head element.

  4. Define our content and menu selectors, these are using when we load in new pages. We use our content selector to find our new content within the response, and replace the existing content on our current page. We use our menu selector to update the active navigation link in our menu when the page changes.

  5. Discover our internal links on our website, and upgrade them so when they are clicked it instead of changing the page to the new page, it will change our page's state to the new page.

  6. When a page state change occurs, we will:

    1. Determine the absolute and relative urls from the new url
    2. Use our content selector to find our current page's content and fade it out
    3. Send off an ajax request to the absolute url
    4. Convert the response into one we can understand
    5. Extract the response's title and set document.title and the title element to it
    6. Use our menu selector to find our page's menu, then scan for new page's url in the menu, and make that the active menu item and mark other menu items inactive
    7. Finish the current content's fadeout animation
    8. Use our menu selector to find the new page's content, and replace the current content with the new page's content
    9. Fade the new content in
    10. Scroll to the new current content so the user is directed to the right place - rather than them ending up looking at the footer or something instead of your page's content due to the height shift with the content change
    11. Inform Google Analytics and other tracking software about the page change
    12. Try to load view script and run it ({path}/{viewName}.js => render())

Further Reading

define(function(require) {
'use strict';
(function(window, undefined) {
// Prepare our Variables
var history = window.history,
document = window.document,
$ = require('jquery');
// Check to see if the HTML5 History API is enabled for our Browser
if (!history.pushState) {
return false;
}
/**
* history.getRootUrl()
* Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
* @return {string} rootUrl
*/
history.getRootUrl = function() {
// Create
var rootUrl = document.location.protocol + '//' + (document.location.hostname || document.location.host);
if (document.location.port || false) {
rootUrl += ':' + document.location.port;
}
rootUrl += '/';
// Return
return rootUrl;
};
// Wait for Document
$(function() {
/* Application Specific Variables */
var 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',
/* Application Generic Variables */
$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;
};
var updateView = function(url) {
var deferred = $.Deferred();
// 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.fadeOut('slow');
$.ajax({
url: url,
headers: {
'X-Push-State-Request': true
},
success: function(data, textStatus, jqXHR) {
var view = renderHtml(data, url);
deferred.resolve(view);
},
error: function(jqXHR, textStatus, errorThrown) {
deferred.fail(errorThrown);
document.location.href = url;
return false;
}
});
return deferred.promise();
};
// 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;
};
var renderHtml = function(html, url) {
// Prepare
var $data = $(documentHtml(html)),
$dataBody = $data.find('.document-body:first'),
$dataContent = $dataBody.find(contentSelector).filter(':first'),
view = $dataContent.data('view'),
relativeUrl = url.replace(rootUrl, ''),
$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().fadeIn('slow'); /* 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.html(),
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');
// Inform Google Analytics of the change
if (typeof window.pageTracker !== 'undefined') {
window.pageTracker._trackPageview(relativeUrl);
}
// Inform ReInvigorate of a state change
if (typeof window.reinvigorate !== 'undefined' && typeof window.reinvigorate.ajax_track !== 'undefined') {
window.reinvigorate.ajax_track(url);
// ^ we use the full url here as that is what reinvigorate supports
}
return view;
};
// Ajaxify Helper
$.fn.ajaxify = function() {
// Prepare
var $this = $(this);
// Ajaxify
$this.find('a:internal').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
document.title = title;
history.pushState(null, title, url);
$(window).trigger('popstate');
event.preventDefault();
return false;
});
// Chain
return $this;
};
// Ajaxify our Internal Links
$body.ajaxify();
// Hook into State Changes
$(window).bind('popstate', function() {
// Prepare Variables
var url = document.location.href;
// Update view to requested resource
updateView(url)
.then(function(viewPath) {
if (typeof(viewPath) === 'undefined') return;
require([viewPath], function(view) {
if (typeof(view) === 'undefined' || !view.hasOwnProperty('render')) return;
view.render();
});
});
}); // end onStateChange
}); // end onDomLoad
})(window); // end closure
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment