Tuesday 17 September 2013

Take your Website Full Ajax with the history API

Its 2013, is your website using AJAX? If it is not take a good look in the mirror. Every time I see a HTML gallery where I have to reload the page every time to get to the next photo I die a little inside but why stop there? The web is moving to single load web applications. Single load applications make the user experience more fluid and enjoyable.

Concept  How can we take existing basic HTML web pages and convert them to a single load web application?

Solution  With the help of AJAX, javascript and the history api along with a lightweight javascript api full-ajax.js created by myself.

Download    Beta Version full-ajax.js its free to use and modify by the GNU version 3 aggreement.



AJAX and the history API


AJAX  Stands for Asynchronous Javascript and XML and it lets you make asynchronous call to the web without having to do a full page refresh; anything form images, scripts, JSON and HTML.
//Simple jQuery AJAX example
$.ajax({type: "GET",dataType: 'html', url: url})
  .done(function(data) {
       //done callback
   })
   .fail(function(error) { 
       //failure callback
   });
Hisorty API  is a new HTML5 feature that lets you mimic standard brower navigation with two simple things. One being the call of pushState which pushes a new state on the history stack and updates the browser URL bar and the other being the onpopstate callback function which is subsequently called when you later navigate using the browsers forwards and back functions. Before we start with should also ensure we have this functionality available to us by checking two objects for their existence.
history.pushState(stateObj,title,URL);
history.onpopstate = function(event){};
var hasHistory = window.history && history.pushState;
Standard Workflow:
  1. Modify page that would reflect the need to update the URL
  2. Call history.pushState with a StateObj, title and URL. The browser URL will change and state will be stored.
  3. Later press Back on the browser the URL will update and the callback with the StateObj being returned in the event.
  4. Re-modify page to what it was in the previous state.

Full AJAX Solution


Now knowing about these useful tools how can we achieve our single page load experience?
How can we simply port exiting web pages to utilize this powerful functionality?

Request entire webpages using AJAX and modify DOM via javascript!

In a nutshell we will be requesting entire HTML webpages and stripping out its content using special attributes, id's and classes. Instead of the default anchor behaviour we will be overriding clicks to make AJAX request to the pages they are linked to. We use our data tag to specify what container by id we want to replace instead of replacing the entire page like in a standard web request.
We will not have to modify too much of the markup!
We need to note that this only works when you have a standard web page where the layout and styles stay consistent and certain section of the content changes page to page.

Full-Ajax API Implementation


- First we id our main content container which contains the two columns - "mid"
- We id our content container that contains the right column only - "mainContent"
- Add a special class on all our anchor tags class="ajaxAnchor"
- Add a special data- property data-update-container-id="{container_id}" where it will be one of the id'd containers "mid" or "mainContent"
- Ensure all anchor paths are absolute, because the page we start from could be any of the pages on the entire site. Are relative URL is always based of our initial page we load.
Page 1
//container container left column and main content
//main content area
Now with our id's and data in place we can add our anchor listeners, we will override the default anchor click for an AJAX call to request the href link. We pass our makeAjaxCall our anchors link as well as the data-update-container-id.
FullAjaxJS.initAnchors = function(){
    $("a.ajaxAnchor").unbind("click");
    $("a.ajaxAnchor").click(function(event){ 
        var updateContainerId = $(this).attr("data-update-container-id");
        var aHref = $(this).attr("href");
        FullAjaxJS.makeAjaxCall(aHref,updateContainerId,true);
        event.preventDefault();
        return false;
    });
};
FullAjaxJS.makeAjaxCall = function(aHref,updateContainerId,isNew){
    $.ajax({type: "GET",dataType: 'html', url: aHref
        })
        .done(function(data) {
            FullAjaxJS.updateContent(data, updateContainerId, aHref, isNew);
        })
        .fail(function(error) { 
            throw error;
        });
};
The makeAjaxCall request's the ENTIRE HTML page of our anchors link using a jQuery AJAX call. We pass the entire HTML data to our updateContent function with the updateContainerId and anchor href.
FullAjaxJS.updateContent = function(data, updateContainerId, aHref, isNew){
    if(isNew){
        var stateObj = {"aHref":aHref , "updateContainerId":updateContainerId};
        history.pushState( stateObj, "title" , aHref);  
    }
    var containerToUpdate = $("#" + updateContainerId);
    var containerToAdd = $(data).find("#" + updateContainerId);   
    containerToUpdate.empty();    
    containerToUpdate.append(containerToAdd.contents());
    //init Anchors
    FullAjaxJS.initAnchors();  
    FullAjaxJS.executeOnReady();
};
To update the content we simply empty the desired container and pull out that same container from the requested HTML data using jQuery. Remember the data-update-container-id attribute? We use its value which is a container id on all our pages.
You will also notice we re initAnchors, we need to once again add the listeners to the HTML if new anchors are in the new content. ExecuteOnReady is also called, more on that later.
Notice we call the history.pushState and we pass it our stateObj. These objects are stored on our stack and passed back to us when our callback is initiated. We store both the updateContainerId and the aHref in our stateObject because we need them to once again utilize makeAjaxCall to update and revert to a previous page state. Since we use our updateContent function for new pages on the stack and previously stored states we need to distinguish between the two. The isNew is used and if true and its a new page we call the pushState from the history api.

What happens now? well on a user initiated back we will receive a call back, First we need to add a function to the supplied call back as well and save an initial state initState.

FullAjaxJS.initFullAjax = function(){
    if(window.onpopstate == null){
        window.onpopstate = FullAjaxJS.onPushPopState;
        FullAjaxJS.initState =  {"aHref": document.location.pathname , 
                             "updateContainerId":"mid", "initialLoad":true};
    }
};

FullAjaxJS.onPushPopState = function(event)
{   
   var stateObj;
    if(event.state){
        stateObj = event.state;
    }
    else if(event.state == null){
        stateObj = FullAjaxJS.initState;    
        if(FullAjaxJS.initState.initialLoad == true){
            FullAjaxJS.initState.initialLoad = false;
            return;
        }
    }
    FullAjaxJS.makeAjaxCall(stateObj.aHref, stateObj.updateContainerId, false);
};

While working with this functionality extensively I noticed history.onpopstate is called on several occasions
1) on page load 2) on back 3) on forward
The code above handles these cases, normal case we call our makeAjaxCall with the properties stored in our stateObj with isNew being false.
The event.state == null on two occasions, on initial page load and when we have return to the initial state where our history stack is empty.
This is why we have a initState stateObj, we sets its aHref to the current page absolute URL and its updateContainerId to our largest container.

Scripting Issues


What happens when we want to run some javascript on our page?
When we append our HTML from our AJAX request to the DOM with jQuery the scripts contained inside will execute but only the scripts contained in the container we add. We may have other scripts contained in different parts of the HTML and We want it to execute our scripts only after we have modified our DOM with the new content.
We use the jQuery parseHTML function that returns us a list of DOM elements including all the scripts.

var parseData = $.parseHTML(data,document,true);
  var containerToAdd = null;
  var scripts = [];
  $(parseData).each(function(i,o){        
      if(o.toString().indexOf("HTMLDivElement") > -1){     
          $.merge(scripts , $(o).find('script'));
          var foundDiv = $(o).find("#" + updateContainerId);           
          if(containerToAdd == null && foundDiv.length > 0)
            containerToAdd = foundDiv;         
      }
      else if(o.toString().indexOf("Script") > -1){
          scripts.push(o);
      }
  });  
  containerToUpdate.empty();
  containerToAdd.find('script').remove();    
  containerToUpdate.append(containerToAdd.contents());  
  $(scripts).each(function(i,o){        
      if(o.toString().indexOf("Script") > -1){
          var jsSrc = o.src;
          var jsName = jsSrc.substring(jsSrc.lastIndexOf("/") + 1);
          if(jsSrc.length > 0 && $.inArray(jsName, FullAjaxJS.executeOnceScript) == -1)
              $.ajax({ url: jsSrc, dataType: "script", async: false});
          else if(o.textContent && o.textContent.length > 0)
              eval(o.textContent);
      }
  });
  FullAjaxJS.initAnchors();
  FullAjaxJS.executeOnReady();
As you can see we search for HTMLDivElementent first and if its a div we search for our div with the containerUpdateId. We also check every element for script tags and save them to an array. After that we look for all the script tags and append them to our list. We then use jQuery getScript for external scripts or the javascript eval function for inline scripts.
Get Scripts is run synchronously such that each script properly run in order. You will also notice that we don't want to execute every scripts, some only need to be run once such as jQuery and other javascript libraries.
You can also see we are not searching for style sheets and they should all by contained inside the main head of the page anyhow.
Its very common especially if utilizing jQuery to have some $(document).ready call. If our page wants to use this we need to handle it in a special way since we the document.ready event does not get fired since we are not reloading a page and we cannot simply invoke it.
What we do is create our own onReady function.
FullAjaxJS.onReady = function(func){
    FullAjaxJS.readyFunctionList =  FullAjaxJS.readyFunctionList || [];
    FullAjaxJS.readyFunctionList.push(func);
};
Instead of using jQuery version update your onReady calls to FullAjaxJS.onReady(fn) We store our own list of functions to call, and then call them all when we have finished loading our new content after our AJAX call.
As you saw above we call our FullAjaxJS.executeOnReady()which we also have to call on our initial page load in the ready function.

Loading Overlay


Another nice thing is to give the user loading feed back so it doesn't appear that the page is frozen or stuck. All we have done is here is stripped out a loading-div and overlay it on top of our content.
When the AJAX call is successful we remove it. Simple as that.

Pros and Cons


Pro
  • Great user experience with single page load with no page reload flicker
  • Can display loading animations to user and run animations or adds in the background without disruption
  • Potentially faster on the client side, no need to redraw/reflow portions of the page
  • Pages are navigable with and without AJAX
Cons
  • Currently only functional for basic websites
  • Code needs to be modified slightly, HTML markup and JS
  • Full Page requests take more data, depending on what server technology you use you can request specific portion of the page.
Once again full source available above.
Thanks for reading!

Sample Example:http://madmanmathew.github.io/blog/fullajax/index.html
full-ajax.js:http://www.blogger.com/madmanmathew.github.com/blog/fullajax/js/full-ajax.js

No comments:

Post a Comment