9. Changing Browser History

This chapter discusses updates to the history interface in HTML5, specifically with two new methods in the History API (pushState and replaceState), and includes several recipes for incorporating them into your session navigation. In addition, you will learn about the state event, using the History API to store more than just page navigation, and advanced topics such as security and extended libraries.

History Basics

The History API, a JavaScript API, has been used in sites since JavaScript 1.0 and has not been updated significantly until HTML5. With the advent of Ajax and pageless navigation, the use of the history object to go forward, to go backward, or to go to a specific session entry became problematic. In fact, several frameworks including YUI incorporated their own browser session management techniques.

Previously, to add a page into the history of a browser without actually changing pages, you needed to change the URL by adding a hash to the URL with the # symbol. With the history.pushState and history.replaceState methods, you can now add and modify history, respectively, and when you combine them with the window popstate event, you can provide improved navigation for the user. By exposing the ability to add and modify page history for a site, the History API provides better back-button support in rich applications that use Ajax technologies. You can now change the “view state” of the application by simply pushing a virtual page, or context, into the history.

You can think of the browser session history, or pages to which the viewer has navigated in your site, as a stack, where pages are pushed onto the top of the pile when viewed. When the user clicks or taps the browser’s back button, the pages are “popped” off the stack one by one. Using just JavaScript, you can move forward, backward, or a particular number of pages forward or backward using these present commands:

window.history.forward

window.history.back

window.history.go([delta])

These methods remain available in the History API, but in addition, you can now add to the history dynamically, catch navigation events, and even control the context of pages through the HTML5 History API.

Browser Compatibility

The History API extensions are supported currently in the browser platforms listed in Table 9.1.

Table 9.1 History API Browser Compatibility

image

Beginner Recipe: Adding to History with pushState

You use the pushState method of the History API to add a new entry into the browser session stack by the current page in the browser. The method takes two parameters, data and title, with an optional third parameter of url:

history.pushState(data, title [,url])

The parameters of the pushState method are as follows:

data: A string or object passed to represent the state of the page

title: A string for the page displayed in the browser heading

url: (Optional) The new URL to add to the history stack

The data parameter represents the state of the page and is automatically associated with the new entry. This can be retrieved through a window popstate event, as you will see later. By state, we mean the current context of the display of the page. For example, this could be the recipe that the person is currently viewing from a database call made by Ajax or the like.

Some browsers impose limits on the size of the data parameter since a state object is stored by the browser locally on the user’s disk. In Firefox, this limit is 640,000 characters of the serialized representation of the state object. If you plan on, or could have the potential of, reaching this limit, you should use sessionStorage or localStorage instead (see Chapter 11, Client-Side Storage).


Note

Some browsers, such as Firefox, ignore the title parameter and will not display the value in the history session list for the user. This is browser-specific, and we hope it will be rectified in future updates.


If the url parameter is provided, this will replace the URL in the address bar of the browser but will not cause the browser to request the page from the website. This allows users to bookmark the URL and return to it later. You will, of course, need logic on your server-side pages to handle bookmarked addresses that do not represent real pages. The URL passed may be a relative or absolute path; however, the path must be in the same domain as the current URL. If a relative path is used, then the path will be based on the current document location. The default path for not including a url parameter or supplying only a querystring (such as "?page=5") is the current URL of the document.

The pushState method is similar to the use of a hash for controlling the context of a page, which is common in dynamic, Ajax-based applications:

window.location = "#foo";

However, the pushState method provides more flexibility than the hash access:

pushState allows you to remain on the same page or change the URL. With the hash method, the browser remains on the same URL.

pushState allows you to keep contextual information in the state of the history. This can be quite helpful, as you will see later in the chapter.

This last point is important to emphasize, because it is a major improvement over the hash addition method to URLs. With each entry in the history, pushState allows you to store an object of data that can hold contextual information about the state of the page. The data can be as simple as a string value or as complex as a serialized JSON object. For example, say you have a page that shows the slides of a presentation. Each slide may have particulars about how it is to be displayed, such as a subtitle, a frame, credits, and so on. The state object can hold this information, making it easy to change the style of the page based on the slide you are on. You will see a recipe later in this chapter on leveraging this state object.

Let’s use the history pushState method to add a new entry into the history session. The page checks, upon loading, for the availability of the History API in the browser by calling typeof on the history.pushState method. If the method results in “undefined,” then the browser does not support the History API. You can use this check to employ a different method to implement the history or limit functionality for the user based on their current browser:

1. Create a blank HTML page with a div to show the current exhibit (exhibit).

2. Add a button that, when clicked, launches the JavaScript function nextExhibit.

3. Insert the nextExhibit function to execute the pushState method.

4. Add the pushState method call to add the context of the meerkat exhibit into the history:

history.pushState('Meerkat','Meerkat Exhibit','meerkats.html'),

5. Update the exhibit div to show the user they are at the meerkat exhibit:

document.getElementById("exhibit").innerHTML = "At the Meerkats.";

Listing 9.1 contains the entire page.

Listing 9.1 pushState to Add Pages to History


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>9_1 At the Zoo</title>
</head>
<body>
<script>
// initialize the button handler
function init() {
  // attach the click button handler
  var btnNextExhibit = document.getElementById('btnNextExhibit'),
  btnNextExhibit.addEventListener('click',nextExhibit,false);
}

function nextExhibit() {
  // Check to see if the history pushState API is available

  if (typeof history.pushState !== "undefined") {
    // Execute the pushState method

    history.pushState('Meerkat','Meerkat Exhibit','meerkats.html'),

    document.getElementById("exhibit").innerHTML = "At the Meerkats.";

  } else {

    // the History API is not available
    alert('The History API is not available in this browser.'),

  }
}
// Add the listener to initialize the page
window.addEventListener('load',init,false);

</script>
Welcome to the zoo.
<div id="exhibit">You are at the Zoo entrance.</div>
<button id="btnNextExhibit">Visit the Meerkats</button>
</body>
</html>


When you load the page in your browser, you will see that you are at the zoo entrance based on the title of the page and the message displayed on the page. If you click the Visit the Meerkats button, the page will push into the history the current page and then change the title and URL of your browser window without physically navigating to a new page. The JavaScript will change the message, informing the user of the meerkat exhibit. For all purposes, it will appear to the user that a new page has been loaded from the server. However, this is simply a new context you have added to history via the pushState.


Tip

In some cases, providing a url parameter in the pushState that has a value of an invalid page will cause a security exception in specific browsers such as Firefox. To rectify this, either provide a valid URL or employ the optional state parameter, which removes the exception.


If you view your browser history list, you will see the At the Zoo page as the last page, and if you click the back button, you will be taken back to the page. The message on the page will not have changed to the “At the zoo entrance,” but you will learn later how to fix that.

Remember that the URL is optional but can be extremely advantageous for returning users. You will, of course, need logic on your server to handle bookmarked entries that have no true page on the server.

Beginner Recipe: Creating an Image Viewer

In the previous recipe, which walked you through your very first pushState call, you pushed into the history session one simple entry. This recipe will take this a step further by giving the user an option of multiple choices and storing each into a growing history session.

Numerous sites are available that allow users to browse a series of photos, videos, or other content by providing thumbnail links and dynamically replacing a selection. In this recipe, you will show the user a series of image thumbnails, allow them to select an image to view, and add a new history entry to track what has been viewed and allow the user to return to those images. Listing 9.2 shows the full code of the page to create the image viewer using history entries. The images and other assets referenced in the code listings are available from the book’s website for your use. Place the images in an images folder so they can be referenced in the showImage function of the code properly.

Listing 9.2 Image Viewer: Creating Several History Entries


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Image 1</title>
<style>
div.imgView { height: 300px; }
div.imgView img { height: 300px; }
div.imgRow img { height: 100px; }
a { cursor: pointer }
</style>
</head>
<body>
<script>
// variable to keep track of the current image
var currentImg = 1;

// navigate to the next slide
function showImage(imgNum) {

  // check if the History API is available
  if ( typeof history.pushState !== "undefined" ) {

    // verify the image selected is not the current one
    if (currentImg != imgNum) {

      // set the image title
      var imgTitle = 'Image ' + imgNum;

      // set next slide in history entries with state object and defaults
      history.pushState( imgNum, imgTitle, '?img=' + imgNum );
      document.getElementById('imgSlide').src = 'images/slide' + imgNum + '.jpg';
      document.getElementById('imageInfo').innerHTML = imgTitle;

      // set the current page title
      document.title = 'Image ' + imgNum;
      var stateInfo = document.getElementById('stateInfo')
      stateInfo.innerHTML = imgTitle + "<br>" + stateInfo.innerHTML;
      // set the current image to the image selected
      currentImg = imgNum;
    }
  } else {
    // History API is not available
    alert('The History API is not available in this browser.'),
  }
}
</script>

<!-- image view and title - set to first image -->
<div id='imgView' class='imgView'><img id='imgSlide' src="images/slide1.jpg" style='height:300px'></div>
<div id='imageInfo'>Image 1</div>

<!-- thumbnail image row -->
<div id='imgRow' class='imgRow'></div>
<script>
  // create row of img links
  var newImg;
  var imgRow = document.getElementById('imgRow'),
  for (var i=1; i<=5; i++ ) {
    document.getElementById('imgRow').add
    newImg = '<a onclick="showImage('+i+'),"><img class="thumbnail" src="images/slide'+i+'.jpg"></a>';
    imgRow.innerHTML += newImg;

}
</script>

<!-- history state display area - set to first image (page when loaded)-->
<div id='stateInfo'>Image 1</div>
</body>
</html>


If you run the previous recipe and select thumbnails to view, you will see the entries pushed into the history of the browser, as if the browser were loading individual pages. In Figure 9.1, we have selected images 3, 5, 4, and 2. On the left, the figure shows the drop-down menu of the browser’s back button; on the right, it shows what is displayed on the page for user-selected images after selecting the images. Notice how “Image 1” is at the bottom of the list, since that is the original title of the HTML page. After the first image, the title you create dynamically gets added to the history list. The last image selected and currently shown (Image 2) is not listed in the history because this is the current context.

Figure 9.1 Back button’s drop-down list of history matching our selections

image

Note

In the previous listing, we used inline scripting to create the thumbnail buttons upon loading of the page. Normally, this would be handled by a domReady event through a framework such as jQuery. To save space and keep the code agnostic, we have scripted inline.


When you run this recipe, you will notice that if you click the back button in your browser, the page listing in the pageInfo div remains the same. Wouldn’t it be nice if you could catch an event when the user clicks the back button to update the page displayed? HTML5 provides you with the pop state event for exactly this purpose, which is covered in the next recipe.

Intermediate Recipe: Popping State in the Image Viewer

To work in conjunction with the new methods of the History API, HTML5 defines a new window event called popstate. This event is triggered when the browser window’s active history session entry changes. The history entry could change based on the browser’s back or forward button being clicked by the user or JavaScript history object methods such as back and go being called. In either case, you can perform logic based on an event handler to catch this event. The syntax for the event handler takes the following format in JavaScript:

window.addEventListener('popstate',funcRef,false);

The pop state event is triggered on the JavaScript window object through the popstate event and is associated to a handler function, referenced in the previous line as funcRef. Thus, when a user navigates their history by using the back button, you can perform any necessary logic to load the correct context in the page HTML.

You will remember from the pushState call that with each entry pushed you can store data, or state, for that entry. The popstate event passes the state you stored to the event handler and allows for the state object to be accessed by the script. You can use this state data by accessing the read-only attribute of the event through event.state.

In Listing 9.2, you created a simple image viewer that allows the user to select a thumbnail image. When selected, the image is loaded into the viewing area, and a new page entry is pushed into the history. However, having the page in the history is not beneficial unless you can show the correct image when the user navigates with the back button in the browser or the script calls history.back or history.go. In this recipe, you will add the pop state event handler to catch the navigation event and load the right image and detail.

Add the highlighted section of code in Listing 9.3 to Listing 9.2 between the existing script lines as shown.

Listing 9.3 Image Viewer with Pops: Catching the Pop State Event


...
<script>

// Set up the popstate page handler
window.addEventListener('popstate',popPage,false);

// variable to keep track of the current image
var currentImg   = 1;

// history pop state event handler function
function popPage(event) {

  // get the state from the history
  currentImg = event.state;

  // set the image and title
  var imgTitle = 'Image ' + currentImg;
  document.getElementById('imgSlide').src = 'images/slide' + currentImg + '.jpg';
  document.getElementById('imageInfo').innerHTML = imgTitle;
  document.title = imgTitle;

  // show we popped a history event and the popped state
  var stateInfo = document.getElementById('stateInfo')
  stateInfo.innerHTML = 'History popped : ' + imgTitle + ' : state: ' + JSON.stringify(event.state) + "<br>" + stateInfo.innerHTML;

}

// navigate to the next slide
function showImage(imgNum) {
...


After incorporating the event handler function in Listing 9.3, run the code. When the page is loaded, select the second thumbnail and then select the third thumbnail. This performs a pushState for each, adding them to the history entry list. By adding the previous popstate event handler, whenever the user clicks the back or forward button in the browser, you catch the event and can then load the right image for the user based on the stored state on the event.

Now click the back button in the browser. The popstate event handler will be called. You will first retrieve the state by assigning the event state to the current image and then use this to display the correct image and set the title. For verification, you will then update the stateInfo div so you can see what was popped in the history (see Figure 9.2).

Figure 9.2 The pushing and popping of the history state

image

Note that the event is triggered even by selecting one of the entries from the history drop-down in your browser. Play around with the code to see how the pushState and popstate events handle the history traversal.


Tip

A user can navigate back in the history of the browser any number of steps via the history menu or the history API commands such as go and back in scripting. It is best to tie in the data element of the history entry to a key that lets you know exactly what the context of the entry is. For example, this could be a unique number to an item, a slide number, or some other unique index. In this way, when the popstate event is handled, the code will know exactly which element to work with instead of just the last one visited.


Beginner Recipe: Changing History with replaceState

The replaceState method of the History API is used to replace the current entry in the browser history with a new entry. The parameters are the same as pushState: data, title, and an optional URL field. The replaceState method is beneficial for updating the state of a history entry as the context changes or setting the initial state of a page. The replaceState method takes the following form:

history.replaceState(data, title [,url])

When a page is loaded fresh in a browser, the title and URL are stored in the history entry. However, no context data is stored along with this information. To store data along with the title and URL, you can call the replaceState when the page loads with the same title, URL, and your additional state data for storage in the history entry. This will in essence overwrite the current entry with the same page information and your additional data. Once a page has been loaded, a user could refine search results as an example, and after each refinement, you could replace the settings stored with the replaceState method.

This recipe uses replaceState to demonstrate how the page does not reload but changes the state of the current history entry. After the page loads, the user is able to click a button that replaces the current state, which is the key “page” and value idxCounter with an updated counter. The counter simply increments each time the state is replaced. To construct the page in Listing 9.4, follow these steps:

1. Create a blank HTML page with an empty div for stateInfo. This will be used to see what you are setting each time.

2. Add a button that, when clicked, launches a JavaScript function called nextState.

3. Insert the nextState function with a history replaceState:

history.replaceState({page: idxCounter}, "page "+idxCounter,
"?page="+idxCounter);

4. Update the stateInfo div, in the nextState showing you that replaceState has been performed, and increment the idxCounter count.

Listing 9.4 Replacing the Current State


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Page</title>
<script>
var idxCounter = 1;    // counter to keep track of page state

// initialize the button handler
function init() {
  // attach the click button handler
  var btnNextState = document.getElementById('btnNextState'),
  btnNextState.addEventListener('click',nextState,false);
}

// our replaceState wrapper function
function nextState() {

  // replace the current page with the next one
  history.replaceState({page: idxCounter}, 'page '+idxCounter, '?page=' +
    idxCounter);
  // update our page state div
  var strStateInfo = document.getElementById('stateInfo').innerHTML;
  document.getElementById('stateInfo').innerHTML = strStateInfo +
    '<br>Replaced state ' + idxCounter;

  // increment our counter
  idxCounter++;

}

// Add the listener to initialize the page
window.addEventListener('load',init,false);

</script>
</head>
<body>
<button id="btnNextState">Replace State</button>
<div id="stateInfo"></div>
</body>
</html>


Each time you click the Replace State button, you will notice that you can verify that the state is being replaced because the URL will show the querystring you have added: page=<idxCounter>. If you look at the history drop-down menu of the browser back button, you will also notice that each time replaceState is called, a new history entry is not added to the stack. Note that you have used a different style of data for the state in replaceState. You are using a JSON-style serialized object with the key, page, and a value of the idxCounter. You are not limited to just strings for the data element of the push or replace but can have complex objects stored.

Intermediate Recipe: Changing the Page History

This next recipe performs several actions on the browser history when the page loads, employing pushState, replaceState, and back and forward methods. The popstate event is used to show when the event is fired with history methods. Listing 9.5 shows the code for this recipe.

Listing 9.5 Pushing and Popping


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Page</title>
<script>
// popstate event handler function
function popPage(event) {
  var strState = 'POP - location: ' + document.location + ', state: ' + JSON.stringify(event.state);
  document.getElementById('stateInfo').innerHTML += strState + '<br>';
};

function loadPages() {

  logAction('pushing page 1'),
  history.pushState({page: 1}, 'page 1', '?page=1'),

  logAction('pushing page 2'),
  history.pushState({page: 2}, 'page 2', '?page=2'),

  logAction('replacing page 2 with page 3'),
  history.replaceState({page: 3}, 'page 3', '?page=3'),

  logAction('taking one step back'),
  history.back();

  logAction('taking one step back again'),
  history.back();

  logAction('taking two steps forward'),
  history.go(2);

}

function logAction(strAction) {
  document.getElementById('stateInfo').innerHTML += strAction + '<br>';
  alert(strAction);
}

// Add our window event listeners
window.addEventListener('popstate',popPage,false);
window.addEventListener('load',loadPages,false);

</script>
</head>
<body>
<div id="stateInfo"></div>
</body>
</html>


Upon loading, the script calls the loadPages function, which pushes two subsequent pages, performs a replaceState, executes two history back commands, and then moves forward in the history two steps. The following is the output for the recipe from a Firefox browser window:

pushing page 1
pushing page 2
replacing page 2 with page 3
taking one step back
POP - location: 9_4_page_flow.html?page=1, state: {"page":1}
taking one step back again
POP - location: 9_4_page_flow.html, state: null
taking two steps forward
POP - location: 9_4_page_flow.html?page=3, state: {"page":3}

With replaceState occurring after pushing page 1 and page 2, you end up with page 1 and page 3 in the browser history, with page 3 being the current state. Now that you have some entries in the history stack, you go back in history by calling history.back. Calling history.back fires the popstate event and puts you back to page 1. History.back is called once more, which takes you back to the original page with no state. Notice that the state is null since the page was loaded through the browser and not via a pushState or replaceState.

Finally, you perform a history.go and move forward in the browser stack by two pages. This brings you back to page 3 by jumping from the original page to page 1 and then to page 3 (since page 2 was replaced earlier with page 3). This can be confusing, and you may find it easier to draw this on a piece of paper as the alerts are popped. The easiest way we have found is to create a drawing of a tower of blocks with each being a pushed entry. Replacing an entry replaces a block, while moving through history just moves the current pointer to a block in the stack (see Figure 9.3).

Figure 9.3 History stack entry and current pointer during recipe

image

It is important to note that the popstate event will not fire until after the window onload event. If methods are called prior to the window’s onload event that normally would trigger the popstate event, then typically only the last popstate event will be triggered in the browser.


Tip

This recipe demonstrates how the replaceState method can replace the state of a page after a pushState has been performed. The state of the original page as you have seen is null since the original page was loaded through the browser and not a pushState. If you need to associate a state with the original page, perform a replaceState when the page loads to associate your state data.


Advanced Recipe: Using Advanced State Data Objects to Pass Information Across Pages

Based on the syntax of the pushState and replaceState methods, the first parameter can take either a string or an object for the data that represents the state of the page. So far, you have passed only strings or a small key and value object for the data parameter. In many cases, there is much more information about a page’s context that would be nice to keep with the history entry so that when the user returns to the page, you can render the content without making calls for the information outside the browser.

Objects can be passed to the methods through JSON representation, as shown in the following example with one variable in a JSON format:

var stateObj = { page: 1 };
history.pushState(stateObj, 'Title 1', 'page1.html'),

The following code shows how more complex objects can be passed:

var stateObj = { page: 1, title: "My Slide #1", author: "Savel"};
history.pushState(stateObj, 'Slide 1', 'slide1.html'),

The state data object is a great way to store user selections, actions, or preferences performed on a page that is then entered into the history. Note, though, that these state data objects are lost if the user purges the browser history and can have limitations set by each browser on the length of data, but for most usages this should not be an issue.

In this recipe, you will learn how to store data with history entries in a JSON format and then pull the data back through the window.onpopstate event. Listing 9.6 creates a simple slide show using slide images and user preferences. The user preferences are stored with each history entry so that when the user navigates with the back button to a prior slide, the preferences are restored. The restoration of the preferences happens in the popstate event handler.

Listing 9.6 Slide Presentation: Pushing Pages with Data


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Slide 1</title>
</head>
<body>
<script>
// set first and last slide numbers
var minSlide    = 1;
var maxSlide    = 5;
// initialize fields used
var currentSlide   = 1;
var currentTitle   = "My Slide 1";
var borderOn       = 0;  // 0 is off, 1 is on
var slideNote      = "";

// initialize our first slide state by replacing current state
var stateObj = { slide: currentSlide, border: borderOn, note: slideNote };
history.replaceState(stateObj, currentTitle, '?slide=' + currentSlide);

// history pop state handler
window.onpopstate = function(event) {

  // show the location URL and string display of the event.state
  document.getElementById('stateInfo').innerHTML = "location: " + document.location + "<br>state: " + JSON.stringify(event.state);

  // retrieve state object data
  currentSlide   = event.state.slide;
  borderOn       = event.state.border;
  slideNote      = event.state.note;

  // show the current slide
  showSlide();
}

// navigate to the next slide
function nextSlide() {

  // check if the History API is available
  if ( typeof history.pushState !== "undefined" ) {

    // validate that we are not at the end of the presentation
    if ( currentSlide < maxSlide ) {

      // retrieve any notes that have been entered
      slideNote = document.getElementById('txtNote').value;

      // set the state object with the current options
      var currentStateObj = { slide: currentSlide, border: borderOn, note: slideNote };

      // replace the current slide properties in the current history entry
      history.replaceState( currentStateObj, 'Slide ' + currentSlide + ' ' + slideNote, "?slide=" + currentSlide);

      // increment the current slide index
      currentSlide++;
      // set global variables to next slide and reset to defaults
      borderOn    = 0;
      slideNote   = "";
      document.getElementById('stateInfo').innerHTML = "";

      // set next slide in history entries with state object and defaults
      var nextStateObj = { slide: currentSlide, border: borderOn, note: slideNote };
      history.pushState( nextStateObj, 'Slide ' + currentSlide, "?slide=" + currentSlide );

      // show the now current slide
      showSlide();
    }
  } else {
    // History API is not available
    alert('The History API is not available in this browser.'),
  }
}

// navigate to previous slide
function prevSlide() {

  // validate that we are not at the beginning already
  if (currentSlide>minSlide) {

    // move back one step in history
    history.back();
  }
}

// show the current slide, title, and options
function showSlide() {

  // set the current slide and title
  document.getElementById('imgSlide').src = "images/slide" + currentSlide + ".jpg";
  document.getElementById('slideInfo').innerHTML = "Slide " + currenSlide;

  // set the current page title
  document.title = "Slide " + currentSlide;

  // set the current slide options
  if (borderOn == 1) {
    document.getElementById('imgSlide').style.border = "5px solid #000000";
    document.getElementById('chkBorder').checked = 1;
  } else {
    document.getElementById('imgSlide').style.border = "";
    document.getElementById('chkBorder').checked = 0;
  }
  document.getElementById('txtNote').value = slideNote;
}

// handle the change of the image border option
function setImgBorder() {

  // set border based on checkbox and global property
  if (document.getElementById('chkBorder').checked == 1) {
    document.getElementById('imgSlide').style.border = "5px solid #000000";
    borderOn = 1;
  } else {
    document.getElementById('imgSlide').style.border = "";
    borderOn = 0;
  }
}
</script>

<!-- slide image and title -->
<div id='slide' style='height:100px;'><img id='imgSlide' src="images/slide1.jpg"></div>
<div id='slideInfo'>Slide 1</div>

<!-- slide options -->
<input type="checkbox" id="chkBorder" onChange="setImgBorder();">Border<br> Note: <input type="text" id="txtNote" value=""><br>

<!-- slide navigation buttons -->
<input type="button" onclick="prevSlide();" value="Previous Slide" />
<input type="button" onclick="nextSlide();" value="Next Slide" />

<!-- history state display area -->
<div id='stateInfo'></div>
</body>
</html>


This code allows the user to set an image border and a note on each slide. You could provide any number of options. A debug div, titled stateInfo, shows the context of the data as history entries are popped. You are able to display the JSON state object with the JSON.stringify method. To reference each stored state object value, you simply reference it by the key. To get the border state value, you would call event.state.border.

When the user clicks for the next slide, you create an object with the current settings and slide number. This is then passed to the replaceState call so that you store the state prior to pushing the next slide. You then reset the settings and push the next slide for display. Of course, this does not take into consideration previously viewed slides that appear after the current slide since each next slide gets pushed fresh into the history entries. To solve this, you can use some new client storage techniques of HTML5, which you will see in Chapter 11, Client-Side Storage.

Intermediate Recipe: Testing History Security

Any time you are able to modify the browser history, page title, and URL address, you need to think about security. Changing URL addresses has historically been one of the more common phishing methods, also known as website forgery. The new History API provides developers for the first time a method to change the content of a URL without actually loading a page. However, the HTML5 specification includes safeguards for the various browsers to follow and protect against the misuse of the History API:

• A script cannot set a domain in the URL of pushState and replaceState different from the current domain.

• The popstate event can reference only state objects stored in the history by pages with the same domain origin in order to maintain privacy across sites.

• A limit is placed on the number of entries a page may add to the browser history stack through the pushState method to prevent “flooding” the history of the user’s browser.

Through these browser policies, the possible malicious use of the History API should be minimized. Let’s verify one of these policies by trying to change the URL to a different domain.


Note

Browsers may impose limits and trim the history stack to prevent an overload from potential “flooding” attacks. The number of entries is determined by each browser, but the order of removal follows the first in, first out (FIFO) methodology.


Much has been debated about allowing JavaScript to control the URL display of the browser, without actually changing the page or causing a new page to load. The main concern is that a method such as pushState or replaceState may be used to phish for personal and confidential information by making it appear that the user is at a different location. You can imagine the havoc if you were able to change the address to anything you like. However, browsers are required by the specification of the History API to validate the address used in the url parameter. If an absolute path is used, then the address must be of the same origin as the original page. Let’s verify that browsers protect against this possible misuse by attempting to push a different domain page into the history. Listing 9.7 provides a very simple page for pushing a new state. Try it and verify that it works in your browser by following these steps and creating a copy of Listing 9.7:

1. Create a blank HTML page that has a button that launches a pushPage function when clicked.

2. Add the pushPage function that checks for the history pushState method availability and then pushes a new context of page.html.

Listing 9.7 A Simple Push in the Same Origin


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>9.7 Push across domains</title>
</head>
<body>
<script>
// initialize the button handler
function init() {
  // attach the click button handler
  var btnPushPage = document.getElementById('btnPushPage'),
  btnPushPage.addEventListener('click',pushPage,false);
}

// push the new state into history
function pushPage() {

  // we check to see if the History API is available
  if (typeof history.pushState !== "undefined") {

    // push the new state
    history.pushState(null, 'Good Page', 'page.html'),
  } else {

    // the History API is not available
    alert('History API not available in this browser'),
  }
}

// Add the listener to initialize the page
window.addEventListener('load',init,false);

</script>
<button id="btnPushPage">Try Push</button>
</body>
</html>


Let’s now take the previous listing and make a minor modification to see what will happen if someone tried to use the pushState to falsify a domain URL. Change the url parameter of the pushState method, as shown in Listing 9.8, to be an absolute path of a domain different from the one you are currently running in. We have chosen www.asite.com just as an example.

Listing 9.8 Setting a Different Origin


<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>9.8 Setting a Different Origin</title>
</head>
<body>
<script>
// initialize the button handler
function init() {
  // attach the click button handler
  var btnPushPage = document.getElementById('btnPushPage'),
  btnPushPage.addEventListener('click',pushPage,false);
}

// push the new state into history
function pushPage() {

  // we check to see if the History API is available
  if (typeof history.pushState !== "undefined") {

    // push the new state
    history.pushState(null, 'Bad Page', 'http://www.asite.com/fish.html'),
  } else {

    // the History API is not available
    alert('History API not available in this browser'),
  }
}

// Add the listener to initialize the page
window.addEventListener('load',init,false);

</script>
<button id="btnPushPage">Try Push</button>
</body>
</html>


When pushing a new state into the history with the pushState method, the browser will verify the URL passed. If the URL is a full path and the domain is different from which it is being “pushed,” the call will fail, throw an exception, or simply not do anything based on the browser. The same holds true for the replaceState method.

Helpful Libraries

As web developers, we need to be concerned not only about the security aspects of the History API but also the support of the API across browsers and within a browser across versions. With several of the new APIs of HTML5, the level of implementation by the various browsers differs greatly, and backward compatibility will be a problem for the foreseeable future, at least until the majority of users migrate to new versions. For the History API, this means you will need to continue to support the hash address method and hashChange event for backward compatibility of page states:

window.onhashchange = function() {
  alert("hash changed!");
};
window.location.hash = Math.random();

However, this does not mean you have to give up the benefits of the new HTML5 history functions. As with most browser-compatibility issues, other developers have recognized this shortcoming and created libraries to handle the differences not only between browsers but also between versions of browsers. For the History API, the leading library at this time is history.js.

You could still program your own logic, but the history.js library is available on GitHub (https://github.com/balupton/History.js) and provides an easy wrapper JavaScript library that attempts to use the HTML5 history methods if supported but falls back to the hash code method automatically if needed. Overall, the syntax is similar to the History API methods, event, and attributes you have seen in this chapter. Unfortunately, we do not have the space to play with the library here, but the library provides the following:

• Multiple browser support

• Framework support, including jQuery, MooTools, and Prototype

• Backward compatibility to older browsers with the use of hash tags

Summary

The History API available in JavaScript is extremely powerful and provides web developers with the opportunity to change the user’s history at a site without changing the actual page. Sites such as GitHub and Flickr have already put the History API to great use, providing more user-friendly functionality.

In this chapter, you examined how the pushState and replaceState methods and popstate event work in conjunction with the history entry list (see Figure 9.4).

Figure 9.4 History API with pushState and replaceState methods and popstate event

image

The following are the main technical features that HTML5 now adds to the history:

• Pushing new entries into the browser entry history

• Replacing the current history entry state data

• Managing navigation event handling and retrieving state

You should now have some ideas of how you can employ these features in your own website or application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset