O'Reilly logo

JavaScript Cookbook by Shelley Powers

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

20.3. Persisting Information Using the History.pushState Method and window.onpopevent

Problem

You’ve looked at all the ways of handling the back button and controlling page state for an Ajax application, and you’re saying to yourself, “There has to be a better way.”

Solution

There is a better way, a much better way...but it’s going to be some time before you’ll be able to incorporate the technique into your applications: using HTML5’s new history.pushState and history.replaceState methods to persist a state object, and the window.onpopevent:

window.history.pushState({ page : page}, "Page " + page, "?page=" + page);
 ...

window.onpopstate = function(event) {
     // check for event.state, if found, reload state
      if (!event.state) return;
      var page = event.state.page;
}

Discussion

Addressing the significant problems Ajax developers have had with trying to persist state through back button events or page reloads, HTML5 has new history object methods, pushState and replaceState, to persist state information, and then an associated window.onpopevent that can be used to restore the page state.

In the past, we had the ability to persist information regarding the page state, though we’ve had to be conservative in how much data we persist. A popular approach, and one demonstrated in Recipe 20.1, is to store the data in the page URL hash, which updates the page history and can be pulled via JavaScript.

The problem with this approach is that updating the hash may update the history. If you hit the back button, the URL with the hash shows in the location bar, but no event is triggered so you can grab the data and restore the page. The workaround was to use a timer to check for the new hash and then restore the page if a new hash was found. Not an attractive solution, and one most of us decided just wasn’t worth trying.

Now, you can easily store any object that can be passed to JSON.stringify. Since the data is stored locally, the early implementor, Firefox, limits the size of the JSON representation to 640k. However, unless you’re recording the state of every pixel in the page, 640k should be more than sufficient.

To see how the new event and methods work, Example 20-3 is a recreation of Example 8-2, from Recipe 8.8. The changes to the application include the removal of the use of the hash location fragment, which is replaced by history.pushState, and the window.onpopstate event handler, both of which are highlighted in the code. There’s one other minor change—in the functionOne function, also highlighted—and I’ll get into the reason why after the example.

Example 20-3. Shows Example 8-2 from Recipe 8.8 converted to using the new history.pushState and window.onpopstate event handler

<!DOCTYPE html>
<head>
<title>Remember me--new, and improved!</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<script>
   window.onload=function() {
      document.getElementById("next").onclick=nextPanel;
   }

   window.onpopstate = function(event) {
     // check for event.state, if found, reload state
      if (!event.state) return;
      var page = event.state.page;
      switch (page) {
        case "one" :
           functionOne();
           break;
        case "two" :
           functionOne();
           functionTwo();
           break;
        case "three" :
           functionOne();
           functionTwo();
           functionThree();
      }
   }

   // display next panel, based on button's class
   function nextPanel() {
      var page = document.getElementById("next").getAttribute("data-page");
      switch(page) {
         case "zero" :
            functionOne();
            break;
         case "one" :
            functionTwo();
            break;
         case "two" :
            functionThree();
       }
   }
   // set both the button class, and create the state link, add to page
   function setPage(page) {
      document.getElementById("next").setAttribute("data-page",page);
      window.history.pushState({ page : page}, "Page " + page, "?page=" + page);
   }

   // function one, two, three - change div, set button and link
   function functionOne() {
      var square = document.getElementById("square");
      square.style.position="relative";
      square.style.left="0";
      square.style.backgroundColor="#ff0000";
      square.style.width="200px";
      square.style.height="200px";
      square.style.padding="10px";
      square.style.margin="20px";
      setPage("one");
   }

   function functionTwo() {
      var square = document.getElementById("square");
      square.style.backgroundColor="#ffff00";
      square.style.position="absolute";
      square.style.left="200px";
      setPage("two");
   }

   function functionThree() {
      var square = document.getElementById("square");
      square.style.width="400px";
      square.style.height="400px";
      square.style.backgroundColor="#00ff00";
      square.style.left="400px";
      setPage("three");
   }
</script>
</head>
<body>
<button id="next" data-page="zero">Next Action</button>
<div id="square" class="zero">
<p>This is the object</p>
</div>
</body>

In this example, the state object that is stored is extremely simple: a page property and its associated value. The history.pushState also takes a title parameter, which is used for the session history entry, and a URL. For the example, I appended a query string representing the page. What is displayed in the location bar is:

http://somecom.com/pushstate.html?page=three

The history.replaceState method takes the same parameters, but modifies the current history entry instead of creating a new one.

When using the browser back button to traverse through the created history entries, or when hitting the page reload, a window.onpopstate event is fired. This is really the truly important component in this new functionality, and is the event we’ve needed for years. To restore the web page to the stored state, we create a window.onpopstate event handler function, accessing the state object from the event passed to the window handler function:

window.onpopstate = function(event) {
    // check for event.state, if found, reload state
     if (!event.state) return;
     var page = event.state.page;
     ...
  }

In the example, when you click the button three times to get to the third “page,” reload the page, or hit the back button, the window.onpopstate event handlers fires. Perfect timing to get the state data, and repair the page. Works beautifully, too. In the Firefox Minefield edition, that is.

One other change that had to be made to the older example, is that functionOne had to be modified and the following style settings added:

square.style.position = "relative";
square.style.left = "0";

The reason is that unlike Example 8-2, which goes through a complete page reload, the new state methods and event handler actually do preserve the state in-page. This means the changes going from step one to step two (setting position to absolute and moving the div element) have to be canceled out in the first function in order to truly restore the page state. It’s a small price to pay for this lovely new functionality.

Again, the example only worked with the Firefox nightly. However, the back button did seem to work with the WebKit nightly.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required