posted on 13 February 2013

26 comments

Reload web pages without refreshing: the html5 history api

TAGS: AJAX,HTML5,Javascript,JQuery

As most of us know, HTML5 has introduced heaps of new features for front-end developers. In this article we are going to take a look in particular at the history object. The main problem we are trying to solve from this article would be the disadvantage that most developers face when trying to develop apps which do not refresh the page when loading new content. One of the techniques most frequently used to achieve this is by using the "hashbang" (#!) pattern. The main problem with this is that if JavaScript is not enabled this won't work as the used identifier can only be accessed on the client side. This will result in broken links.

Introduction

The HTML5 history API allows us to manipulate the browser history through JavaScript, some of these features have been available in older HTML versions. However, the new bits came along with HTML5. These include a way to add entries to the browser history and change the URL in the browser bar without reloading pages, which I think is very interesting and useful in order to improve the UX as traditional websites are becoming more app like oriented.

Syntax

The History API gives us the ability to convert URLs, like for example http://mydomain.com  to http://mydomain.com/hola without the necessity of refreshing the whole page. The following is a list of the members that make up the history object:

window.history.length: //returns the number of entries in the joint session history.

window.history.state: // returns the current state object.

window.history.go ( [ delta ] ) //goes back or forward by the specified number of steps in the joint session history A zero delta will reload the current page. If the delta is out of range, does nothing.

window.history.back() //goes back one step in the joint session history. If there is no previous page, does nothing.

window.history.forward() //goes forward one step in the joint session history. If there is no next page, does nothing.

window.history.pushState(data, title [, url]) //pushes the given data onto the session history, with the given title and if provided, the given URL.

window.history.replaceState (data, title [, url]) //updates the current entry in the session history to have the given data, title and if provided URL.


//for example to add history entries with history.pushState we would do this:
history.pushState({data: 'monkey'}, 'Title', '/hello.html')

//to replace a history entry using history.replaceState, we would do the following:
history.replaceState({data: 'elephant'}, 'New Title')

Compatibility

Unfortunately, all that glitters is not gold... so, pushState() and replaceState() are not supported by old browsers. Therefore we need a plan B to ensure that user experiences are not affected in old browsers. We can achieve this by applying the concept of progressive enhancement and you can see how we apply this concept in the next example.

Example

What we have seen so far are the basics, now you will understand these concepts better with a real example. This example is a PHP and AJAX based app which allows users to navigate through the site without refreshing the page each time they request a new page. The user will be able to click the back and forward  button on the browser and they will still work even if we are using AJAX to load the content on the page. If the user has JavaScript enabled or the browser does not support the history API it will still work perfectly as it will fallback to its natural behaviour (this is where we utililze progressive enhancement). The following is a screenshot of our app, so that you can have an idea of what we are going to build today.

 

I will not be going into much detail on header.php and footer.php as these files contain the main markup which I think is pretty self-explanatory.

header.php

<!doctype html>
<html>
<body>
<head>
<link rel="stylesheet" href="style.css" />
</head>
<div id="wrapper">
<header>
  <h1>HEADER</h1>
</header>
<div id='menu'>
<a href='page1.php'>Item 1</a>
<a href='page2.php'>Item 2</a>
<a href='page3.php'>Item 3</a>
</div>

footer.php

<footer>
  <h1>FOOTER</h1>
</footer>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script src="app.js"></script>
</body>
</html>

The following are the very basic styles for our app:

style.css

body
{
    font-family: Arial;
}
*
{
    margin:0;
    padding: 0;
}    
#wrapper
{
        width: 60%;
        margin: 0 auto;
}
#main-content
{
    min-height: 500px;
    margin-top: 40px;
    background-color: #2b2b2b;
    padding: 20px;
    color: #fff;
    position: relative;
}
header
{
    margin-bottom: 50px;
    padding-top: 20px;
}
a
{
    text-decoration: none;
    text-transform: uppercase;
    margin-right: 20px;
    color: #2b2b2b;
}
footer
{
    margin-top: 20px;
}
img.ajax-loader
{
    position: absolute;
    top: 45%;
    left: 45%;
    display: none;
}

page1.php, page2.php and page3.php are very similar, the only difference is the content they display. These pages have a bit of PHP in them in order to check if type=ajax was set via a GET request. Obviously first time the page loads no matter which page it is, the type GET request won’t be set as it gets set only when a menu item is clicked.

page1.php

<?php
if($_GET['type']!='ajax'){
    include 'header.php';
    echo "<div id='main-content'>";
}
?>
Hola this is content 1
<img class="ajax-loader" src="ajax-loader.gif" alt="loading..." />
<?php
if($_GET['type']!='ajax'){
    echo "</div>";
    include 'footer.php';
}?>

page2.php

<?php
if($_GET['type']!='ajax'){
    include 'header.php';
    echo "<div id='main-content'>";
}
?>
Hola this is content 2
<img class="ajax-loader" src="ajax-loader.gif" alt="loading..." />
<?php
if($_GET['type']!='ajax'){
    echo "</div>";
    include 'footer.php';
}?>

page3.php

<?php
if($_GET['type']!='ajax'){
    include 'header.php';
    echo "<div id='main-content'>";
}
?>
Hola this is content 3
<img class="ajax-loader" src="ajax-loader.gif" alt="loading..." />
<?php
if($_GET['type']!='ajax'){
    echo "</div>";
    include 'footer.php';
}?>

And here is where the magic comes in... app.js

First off all, we need get the value of the href attribute of the menu item the user has clicked.

$("a").on('click', function (e) {
    pageUrl = $(this).attr('href');
    e.preventDefault();
});

Then we perform an asynchronous HTTP (Ajax) request using JQuery to get the content we need to display and add it into our #main-content div.

    $.ajax({
        url: pageUrl + '?type=ajax', success: function (data) {
            $('#main-content').html(data);
            // hide ajax loader
            $('.ajax-loader').hide();
        }
    });

Next but not last, we will be manipulating the browser URL as we need to change it to the one the user has clicked.

if (pageUrl != window.location) {
        window.history.pushState({ path: pageUrl }, '', pageUrl);
}

And in the last bit we will be overriding the browser back and forward buttons so that we can navigate back and forward without refreshing the page.

$(window).on('popstate', function () {
    $.ajax({
        url: location.pathname + '?type=ajax', success: function (data) {
            $('#main-content').html(data);
        }
    });
});

The completed app.js

$.cergis = $.cergis || {};
$.cergis.loadContent = function () {
    $('.ajax-loader').show();
    $.ajax({
        url: pageUrl + '?type=ajax',
        success: function (data) {
            $('#main-content').html(data);
            // hide ajax loader
            $('.ajax-loader').hide();
        }
    });
    if (pageUrl != window.location) {
        window.history.pushState({ path: pageUrl }, '', pageUrl);
    }
}
$.cergis.backForwardButtons = function () {
    $(window).on('popstate', function () {
        $.ajax({
            url: location.pathname + '?type=ajax',
            success: function (data) {
                $('#main-content').html(data);
            }
        });
    });
}
$("a").on('click', function (e) {
    pageUrl = $(this).attr('href');
    $.cergis.loadContent();
    e.preventDefault();
});
$.cergis.backForwardButtons();

Well, this is all for now, I really hope that you have found this article useful and enjoyed it!

Please feel free to leave comments or ask any question you may have.

See the demo in action.

Jose

José holds a first class degree in “Business Information Systems” from the University of East London where he specialized in Web Development. His expertise and interests lie in front-end technologies, including HTML/5, CSS3, JavaScript (as well as frameworks like jQuery). His server side skills roll around ASP.NET

Find out more about Jose here.

26 comments

Gordon said:

Awesome post man! Been looking for this for ages now.

Ralph said:

Great example of using the history api. Good that you mention progressive enhancement over creating a fallback solution with a hashbang for browsers that don't support the history-api. Hashbanging is no good! One little thing... in header.php you have the opening body tag before the head tag.

Ralph said:

I had a play with your code just to experiment a bit with it and I ran in a couple of issues. 1. If I want to open a page that's in a subdirectory It doesn't work unless I use absolute paths to that page (http://localhost:8888/testsuite/history-api/sub/subsub/page.php). This works until some extend, cause if I do a page refresh on this page, it only loads the #main-content div and it's content... thus without the header and footer includes. I thought it was a path issue, but I've changed everything to absolute paths (even to the CSS and JS files) and still this weird behavior. 2. The second issue is that if I have a hyperlink in #main-content that needs to open a page in #main-content it will do a complete page refresh... thus no AJAX magic. Is this all just a natural behavior or am I missing something? I know there is a polyfill (history.js) that says it solves a lot of browser inconsistencies with the history api and it says also that you can AJAX with it to subdirectories and subdomains, but to be honest that gist is a bit over my head to get that to work.

Chris Diplock said:

I have been trying to find a solution like this for ages and I was really pleased that everything looked like it was working until I tried Internet Explorer. I loaded a youtube embed into the header for testing purposes play the video then press the buttons. In firefox, chrome and safari it changes without disrupting the video from playing but in internet explorer 7,8 and 9 the video reloads. So close to the perfect solution I have spent years looking for but not quite there, if there is anything that could be done to resolve this I would be delighted and only too happy to get the drinks in!

Jose / Author said:

Hi Ralph, first of all thanks for your comments and really sorry for my late reply. The reason why I wrote this tutorial is to show the great power of the HTML5 History API. This worked well to some extent. As you have noticed, when we start loading content from files which are not in the same root issues will rise. However, these issues can be solved by extending the History API using History.js as you mentioned.

Jose / Author said:

Hi Crhis, thanks for your interest on my blog post. Unfortunately, the IE10 ancestors do not support the HTML5 history API. I understand that you want it to have the same effect on older browsers but in this case you are going to have to let it degrade gracefully to its default behavior.

Trevin said:

Jose, this is fantastic. Thank you so much for sharing your skill!!! This worked beautifully. I only ran into one snag, which was the fatal error of $_GET['type'} == null on the first load. I solved this by setting it to an arbitrary string if it was null. if (!isset($_GET['type'])) { $_GET['type']='load'; } Again, thank you for this post!!!

catalin said:

this is awesome. i want to use it on my next project but my knowledge with PHP and CSS are limited and i need some help to make it work as i want. i will contact you via the contact form. regards

ABDUL MUHEET said:

Really very nice work..i have no words to explain that how i am glad to see such tutorial...Keep it up..my good wishes are with you.

Nathalie Bergeron said:

thank you :)

Revathi said:

Nice work with HTML5 history. I have an issue in my project with javascript. When page navigates javascript file changes are not applying and error is displayed. Is there any way to auto update javascript file without reloading a page. Please suggest.

Jose / Author said:

Thanks everyone for your interest on this post. Revathi, do you have a link to provide? so that I could have a look to the issue you are having?

Todd said:

This is great, but for some reason I can't get it to work as written. I even tried copy/pasting the code and substituting only my div's class instead of ('#main-content') I'm using ('.top') and it still won't work for me. Everything else in the code makes sense, there are just two things I'm unclear on. First, what does $.cergis = $.cergis || {}; do? I've seen something like this before, but I can't remember what it was for and it's a difficult search term. Second, is pageUrl a variable or is it a built-in method with jQuery?

Maureen Dunlap said:

Thanks so much for this article. I am using this technique to redesign our corporate site, and it's almost perfect. The only issue I am facing is how to include anchor links within a page? One failed attempt using js doesn't work because it loads a second footer to the page. What's the best way to accomplish anchors within a long page.

Kyaw Zin Htun said:

hello author, When I test with your example code I have some problem this problem is ( ! ) Notice: Undefined index: type in D:\xampp\htdocs\AjaxTest\pageload\page3.php on line 2 Call Stack # Time Memory Function Location 1 0.0007 125816 {main}( ) ..\page3.php:0 ( ! ) Notice: Undefined index: type in D:\xampp\htdocs\AjaxTest\pageload\page3.php on line 10 Call Stack # Time Memory Function Location 1 0.0007 125816 {main}( ) ..\page3.php:0 please help me and can you give any advice thank author and everyone

MOHSSINE said:

It does not work when I add another parameter in the url like : <a href='page1.php?p=1'>Item 1</a> <a href='page2.php?p=2'>Item 2</a> OR <a href='page3.php?p=3&a=2'>Item 3</a> someone can help me please !!

ujnimz said:

Hi, Thank you very much for this article. I have a question. I tried to pass php variables through the link. But then the pages refreshing and it doesn't work properly. Can you please advice how to use this in this case? Thanks again.

Nimantha said:

You can do it by changing app.js file little. <script> $(function(){ $("a[role='tab']").click(function(e){ e.preventDefault(); /* if uncomment the above line, html5 nonsupported browers won't change the url but will display the ajax content; if commented, html5 nonsupported browers will reload the page to the specified link. */ // show ajax loader if you have $('.ajax-loader').show(); //get the link location that was clicked pageurl = $(this).attr('href'); //get the page tittle and set document.title = $(this).attr('title'); //to get the ajax content and display in div with id 'content' $.ajax({url:pageurl+'&role=tab',success: function(data){ $('#content').html(data); $('.ajax-loader').hide(); }}); //to change the browser URL to 'pageurl' if(pageurl!=window.location){ window.history.pushState({path:pageurl},'',pageurl); } return false; }); }); /* the below code is to override back button to get the ajax content without reload*/ $(window).bind('popstate', function() { $.ajax({url:location.pathname+'&role=tab',success: function(data){ $('#content').html(data); }}); }); </script> This is not the same script as above, but they almost same and you can see the change I have done. Instead of ? place & before type in your code.

sabari N said:

Hai jose, Excellent post.I tried this example.but i got fatal error. Error: Notice: Undefined index: type in C:\wamp\www\practise\page1.php on line Please help me

Swata said:

Hello, how this work with SEO? Is robot allowed to index all pages which are behind partly links? history.pushState({data: 'monkey'}, 'Title', '/hello.html' Thanks

kooldandy said:

thanks this is the stuff for which i was looking for...

Joan Josep said:

Awsome, so thanks!!

KY said:

Very useful even for me who knows little about web stuff. I have same questions as Todd: 1)what does $.cergis = $.cergis || {}; do? , 2)is pageUrl a variable or is it a built-in method with jQuery? Thanks so much.

Krishnakumar said:

It is good example for the learners who are willing to learn HTML5 from scratch. Very good example for the persons like me who knows nothing in developments in IT field

Harbir said:

Hi, Just wondering what are the other options to PHP, can this be done using HTML and JS only?

Harbir said:

Hi guys, I am new to js and ui, I am wondering how to convert this example busing html and js? Thanks

add your comment

Email me when other users reply