Jquery Site Setup (Positioning) 4 of 4

Posted on Tuesday, May 6, 2014


This tutorial is going to be split into 4 sections.  The goal of this tutorial is to create a simple web site that has a top bar with a static Height, a left tool bar with a static width, and a variable "display area" which will take up the rest of the space.

This tutorial will start where the last tutorial finished which added some ajaxy goodness using jQuery load.  Now it's time to use the HTML 5 history tool.
One problem with the web app at this point is its history, there is none!

If you click the back button you are taken to the last site you were at.  This can be very problematic.   Lucky for us in HTML 5 there is a very nice history API we can use for Browser history management.

I am not going to go into the specifics a great deal here, but rather show a practical example.

For more details check out these sites.


Also check out http://caniuse.com/history [3] to see current browser support for history.




Install History.js


First install History.js https://github.com/browserstate/history.js/ [1]

History.js provides cross-browser support for html5 browser history management and also provides tools for handling browser history for html 4 browsers.  (I will not be going over the html 4 browser history in this tutorial)

Create a directory and download the History.js for jQuery  (these links may change in the future if so just head to the main github site and search for download)


> mkdir js/history
> cd js/history
> wget https://github.com/browserstate/history.js/zipball/master
> unzip master
> mv browserstate-history.js-cce9589/scripts/uncompressed/history.js .
> mv mv browserstate-history.js-cce9589/scripts/uncompressed/history.adapter.jquery.js .
> rm -r browserstate-history.js-cce9589/
> rm master
> cd ../..




Edit the index.html to add the new javascripts


> vi index.html


Update it to the following


<html>
<head>
<script type="text/javascript">
    document.write("<base href='http://"
       + document.location.host + "/004/' />");
</script>
<link rel="stylesheet" href="css/main.css" type="text/css"/>
</head>
<body>

<div id="nav">
  <div id="nav-top">
    <h1>This is the Top Area</h1>
  </div>
  <div id="nav-left-outer">
     <div id="nav-left">
      <img id="nav-1" src="img/handshake-128.png" class="nav-left"/>
      <img id="nav-2" src="img/plus-128.png" class="nav-left"/>
      <img id="nav-3" src="img/youtube-2-128.png" class="nav-left"/>
     </div>
  </div>
  <div id="nav-display-area-outer">
    <div id="nav-display-area">
        <h1>Resizeable Display Area</h1>
    </div>
  </div>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script src="js/underscore.js"></script>
<script src="js/history/history.js"></script>
<script src="js/history/history.adapter.jquery.js"></script>
<script src="js/history.js"></script>
<script src="js/main.js"></script>
<script src="js/animate.js"></script>
<script src="js/ajax-load.js"></script>


</body>
</html>


The first script is just there to set the base directory, this simplifies things later when you are using relative paths in the history (you could use absolute paths and not worry about this, but my site is located at .com/004 so I chose this method)


<script type="text/javascript">
    document.write("<base href='http://"
       + document.location.host + "/004/' />");
</script>



Now add your own history.js file


> vi js/history.js


And place the following in it


(function (window, undefined) {
    History.Adapter.bind($(this), 'statechange', function(){
        alert("A History State has changed")
    })
})(this)



… What does this do?   The statechange event will fire whenever a

pushState
popState
replaceState

event occurs.

You could have bound a function to each event but I think it makes more sense just to use the single event listener for all of them.





Edit ajax-load.js



> vi js/ajax-load.js


And place the following in it


(function (window, undefined) {
    $(function () {
        $("#nav-1").click(function () {
            $("#nav-display-area").load("shaking-hands")
        })
        $("#nav-2").click(function () {
            window.History.pushState(
                   null, null, "add-tool/adder/")
            $('<link/>', {
                rel: 'stylesheet',
                type: 'text/css',
                href: 'css/add.css'
            }).appendTo('head')
            $("#nav-display-area").load("tools/add")
            $.getScript("js/add.js")
        })
        $("#nav-3").click(function () {
            $("#nav-display-area").load("youtube/")
        })
    })
})(this)


Reload the page and click on the + icon




The URL will change and the alert will fire off.


What is going on here? 




The pushState function is pushing the URL http://demo.whiteboardcoder.com/004/add-tool/adder  onto the history stack.


            window.History.pushState(
                   null, null, "add-tool/adder/")


After the push occurs an event is fired which is picked up by the listener bound to 'statechange'
Whose function has a simple alert message

Now… because of some unintended side effects the div is not updated like it should be.  (Basically since we changed the URL the relative locations of the files are in the wrong location)

Now if you click the back button it works…. Kinda….

You will not be taken out of the site, with a single click, but nothing really changes either….

In this simple case all that really occurred was you added a URL to the history that did not update the page.  I could add 100 URLs like this and all it would create is the need to hit the back button 100 times to get out of the site. 

To get this point across a little clearer some code changes are needed.




Just URLs


Edit history.js


> vi js/history.js


And place the following in it


(function (window, undefined) {
    History.Adapter.bind($(window), 'statechange',
     function () {
        var State = History.getState()
        alert("The current URL on the history stack is \n'"
               + State.url + "'")
    })
})(this)




Edit ajax-load.js


> vi js/ajax-load.js


And place the following in it


(function (window, undefined) {
    $(function () {
        $("#nav-1").click(function () {
            window.History.pushState(null, null, "shake/")
        })
        $("#nav-2").click(function () {
            window.History.pushState(null, null, "add-tool/adder/")
        })
        $("#nav-3").click(function () {
            window.History.pushState(null, null, "youtube/")
        })
    })
})(this)



Reload the page and click on the icon buttons




Now the URL updates in the browser and you get an alert showing what the current URL is.

No part of the page updates but there is now a history you can go back and forth through using the forward and back button.



Great! So I have a history that is correct but the page does not update…. What is the use of that?

You need to make use of the data field in the function




Create and use the State data Object


The State data Object you can pass to pushState can help solve this.
You can store some json data in the State data Object that can be used during the event listener.


Edit ajax-load.js


> vi js/ajax-load.js


And place the following in it


(function(window, undefined) {
  $(function () {
    $("#nav-1").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "shaking-hands"
      }
      window.History.pushState(data,
                   null, "shake/")
    })
    $("#nav-2").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "tools/add"
      }
      window.History.pushState(data,
                   null, "add-tool/adder/")
    })
    $("#nav-3").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "youtube/view"
      }
      window.History.pushState(data,
                      null, "youtube/")
    })
  })
})(this)



Edit history.js


> vi js/history.js


And place the following in it


(function (window, undefined) {
  History.Adapter.bind($(window),
            'statechange', function () {
    var State = History.getState()

    if (State.data.hasOwnProperty("elemId")
             && State.data.hasOwnProperty("url")) {
      $(State.data.elemId).load(State.data.url)
    }
  })
})(this)


Reload the page and click on some of the icon buttons.





Now the web app is updating like it should and the URL history is being added.

Since the instructions for loading the ajaxy data is in the State Object when the forward or back button is clicked the 'statechange' event is triggered and the page is updated correctly.  Try clicking the buttons then clicking on the forward and back button a few times.



Loading CSS and javascript dynamically


The + tool has its own CSS and javascript that need to be loaded dynamically this too can be handled with the 'statechange' event.

Edit ajax-load.js


> vi js/ajax-load.js


And place the following in it


(function(window, undefined) {
  $(function () {
    $("#nav-1").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "shaking-hands"
      }
      window.History.pushState(data,
                   null, "shake/")
    })
    $("#nav-2").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "tools/add",
        css: "css/add.css",
        script: "js/add.js"
      }
      window.History.pushState(data,
                   null, "add-tool/adder/")
    })
    $("#nav-3").click(function() {
      var data = {
        elemId: "#nav-display-area",
        url: "youtube/view"
      }
      window.History.pushState(data,
                      null, "youtube/")
    })
  })
})(this)



Edit history.js


> vi js/history.js


And place the following in it


(function (window, undefined) {
    History.Adapter.bind($(window),
           'statechange', function () {
        var State = History.getState()

        if (State.data.hasOwnProperty("css")) {
            $('<link/>', {
                rel: 'stylesheet',
                type: 'text/css',
                href: State.data.css
            }).appendTo('head')
        }
        if (State.data.hasOwnProperty("elemId")
                && State.data.hasOwnProperty("url")) {
            $(State.data.elemId).load(State.data.url)
        }
        if (State.data.hasOwnProperty("script")) {
            $.getScript(State.data.script)
        }
    })
})(this)


Reload the page and click on some of the icon buttons.





Now the JavaScript and CSS are loaded when you click the + button.





One last problem… the URLs


The last part to be dealt with is the actual URLs themselves.  If you click on the + button the URL will be updated to http://demo.whiteboardcoder.com/004/add-tool/adder/  in my example.  If you reload this page or try to open the location in another window.




You will get the 404 error since the actual URL does not exist.
The URL needs to be legitimate and to open the web app to the correct tool….

I will attempt to fix this with some static pages, just to show it can be done.



> mkdir shake
> mkdir add-tool
> mkdir youtube
> ln -s `pwd`/index.html shake/index.html
> ln -s `pwd`/index.html youtube/
> ln -s `pwd`/index.html add-tool/adder/



Reload the page click on the + button then reload the page





Well at least there is no 404 error, but reloading the page sets you back to square one, when it should open the tool again.

To fix that some we need some more JavaScript Code.


Edit main.js


> vi js/main.js


And place the following in it


(function (window, undefined) {
    $(window).load(function () {
        handleURL()
        setSizes()
    })

    $(window).resize(_.debounce(function () {
        setSizes()
    }, 250))

    function setSizes() {
        priorHeight = $("#nav-display-area").height()
        priorWidth = $("#nav-display-area").width()

        $("#nav-display-area").height(
           $(window).height() -
           $("#nav-top").height() - 10)
           .width($(window).width() -
           $("#nav-left-outer").width() - 10)

        console.log("(" + $(window).width() + ", "
            + $(window).height() + ")")

        if (priorHeight > $("#nav-display-area").height()
          || priorWidth > $("#nav-display-area").width())          
        {
            //Call setSizes again
            setSizes()
        }
    }

    function handleURL() {
        switch (location.pathname.toLowerCase()) {
            case '/004/shake/': loadPage("shake/",
                  { url: "shaking-hands" }); break;
            case '/004/add-tool/adder/': loadPage("add-tool/adder/",
                  { url: "tools/add", css: "css/add.css",
                  script: "js/add.js" }); break;
            case '/004/youtube/': loadPage("youtube/",
                  { url: "youtube/view" }); break;
        }
    }

    function loadPage(url, data) {
        //Add the elemId and a random element to assure
        //it will load (if the state Object is identical
        //to one on top of the stack it won't replace it.
        $.extend(data, { elemId: "#nav-display-area",
            ran: window.Math.random() })
        window.History.replaceState(data, null, url)
    }

})(this)


When loaded the handleURL function will determine the current pathname and if it's one of the tool URLS it will replace the current state on the history stack.

Reload the page and try it and copy and paste a URL and try it.


That is the end of this tutorial, hope you found it helpful.



References
[1]        Pushing and Popping with the History API
Mike Robinson
            http://html5doctor.com/history-api/
                Accessed 03/2014
[2]        HTML5 History: Clean URLs for Deep-linking Ajax Applications
            Craig Shoemaker
                Accessed 03/2014
[3]        Can I use Session history management?
            http://caniuse.com/history
                Accessed 03/2014
[4]        History.js github site
                Accessed 03/2014


No comments:

Post a Comment