Mobile Web Apps: Loading Pages

This is the fourth excerpt from the book “Build Mobile Websites and Apps for Smart Devices” by Earle Castledine, Myles Eftos and Max Wheeler. BuildMobile is exclusively publishing a complete chapter from the book, the chapter on Mobile Web Apps, and this section is called “Loading Pages”.

4. Loading Pages

Now that we’ve learned the basics of handling touch screens, and picked up a couple of quick wins by optimizing our links and forms, it’s time to roll up our sleeves and get to work on the biggest component of just about any mobile web app. Unless your application is very basic, chances are you’re going to need more than a single page, and therefore, you need to think about how to switch between pages. As we discussed earlier, our client is unlikely to be impressed with the web-like page reloads that currently exist in our app, so we need a way to hide the request/response cycle from the user. There are three main ways we can do this:

  1. Putting everything on one page, and then hiding and displaying sections as required
  2. Loading in new pages via Ajax
  3. Including only the complete skeleton of the app up front, and then bringing in data as required

The approach you take will depend heavily on the application. We’ll start by looking at the first (and simplest) approach, and load all our content up front. This will let us look at how we can handle transitions between the various states. Of course, for a real app that depended on the user’s location to fetch up-to-date information, you’d want to opt for one of the other two methods; we’ll look at them at the end of this chapter.

4.1. Swapping Pages

If all our content is loaded in a single HTML page, a “page” from the point of view of our application is no longer a full HTML document; it’s merely a DOM node that we’re using as a container. We need to choose a suitable container and an appropriate way to group our pages, so that our scripts can manipulate them consistently.

We’ll start by creating a container div (called pages), which contains a number of child div elements that are the actual pages. There can only be one page visible at a time, so we’ll give that element a class of current. This class will be passed to whichever page is the active one:

<div id="pages"> <div id="page-spots" class="current"> <!-- Spots Index --> </div> <div id="page-spot"> <!-- Spot Detail --> </div> <div id="page-sightings"> <!-- Add Sighting Form --> </div> <div id="page-stars"> <!-- Stars Index --> </div> <div id="page-star"> <!-- Star Detail --> </div> </div> 

This list of pages will sit below the tab bar-so no need to change the markup of our navigation. We have, however, hooked up the links to point to the various sections by way of their id attributes. This will let us use a sneaky trick to show pages in the next step:

<ul id="tab-bar"> <li> <a href="#spots">Spots</a> </li> <li> <a href="#sightings">Add a sighting</a> </li> <li> <a href="#stars">Stars</a> </li> </ul> 

After this, we need a couple of styles for hiding and showing pages. In our markup, every page is a first-level child of the main #pages container, so we can rely on that fact and use a child selector (>). First, we’ll hide all the pages; then we’ll unhide the page that has the current class:

Example 4.15. stylesheets/transitions.css (excerpt)

#pages > div { display: none; } #pages > div.current { display: block; } 

To actually select some pages, we need to intercept the navigation menu clicks. We’ll be using the code we wrote earlier to capture the event and prevent the browser from navigating to the link:

Example 4.16. javascripts/ch4/07-swap.js (excerpt)

$("#tab-bar a").bind('click', function(e) { e.preventDefault(); // Swap pages! }); 

And here’s the trick: the links point to our page elements by using the anchor syntax of a hash symbol (#), followed by a fragment identifier. It coincidently happens that jQuery uses that exact same syntax to select elements by id, so we can funnel the hash property of the click event directly into jQuery to select the destination page. Very sneaky:

Example 4.17. javascripts/ch4/07-swap.js (excerpt)

$("#tab-bar a").bind('click', function(e) { e.preventDefault(); var nextPage = $(e.target.hash); $("#pages .current").removeclass("current"); nextPage.addclass("current"); }); 

With the target page acquired, we can hide the current page by removing the current class and passing it to the destination page. Swapping between pages now works as expected, but there’s a slight problem: the selected icon in the tab bar fails to change when you navigate to another page. Looking back at our CSS, you’ll remember that the tab bar’s appearance is due to a class set on the containing ul element; it’s a class that’s the same as the current page div element’s id. So all we need to do is slice out the hash symbol from our string (using slice(1) to remove the first character), and set that as the ul’s class:

Example 4.18. javascripts/ch4/07-swap.js (excerpt)

$("#tab-bar a").bind('click', function(e) { e.preventDefault(); var nextPage = $(e.target.hash); $("#pages .current").removeclass("current"); nextPage.addclass("current"); $("#tab-bar").attr("className", e.target.hash.slice(1)); }); 

4.2. Fading with WebKit Animations

The page swap we just implemented is as straightforward as it gets. This has its advantages-it stays out of our users’ way, for one. That said, well-placed transitions between pages not only make your apps sexier, they can provide a clear visual cue to the user as to where they’re being taken.

After the original iPhone was released, web developers leapt to re-implement the native transition effects in JavaScript, but the results were less than ideal, often containing lags and jumps that were very noticeable and distracting to users. The solution largely was to ditch JavaScript for moving large DOM elements, and instead turn to the new and hardware-accelerated CSS3 transitions and animations.

Before we worry about the transitions, though, we need to lay some groundwork. To fling DOM elements around, we need to be able to show, hide, and position them at will:

Example 4.19. stylesheets/transitions.css (excerpt)

#pages { position: relative; } #pages > div { display:none; position: absolute; top: 0; left: 0; width: 100%; } 

By positioning the elements absolutely, we’ve moved every page up into the top-left corner, giving us a neat stack of invisible cards that we can now shuffle around and animate. They’re not all invisible, though remember that in our HTML, we gave our default page the class of current which sets its display property to block.

The difference this time is that we’re going to apply CSS animations to the pages. The incoming (new) page, and the outgoing (current) page will have equal but opposite forces applied to them to create a smooth-looking effect. There are three steps required to do this:

  • Set up the CSS animations.
  • Trigger the animation by setting the appropriate classes on the pages.
  • Remove the non-required classes when the animation is finished, and return to a non-animating state.

Let’s start on the CSS. There are many approaches you can take with the problem of emulating native page transitions. We’ll adopt a flexible method that’s adapted from the jQTouch library. This is a modular approach, where we control transitions by applying and removing the relevant parts of an animation to each page.

Before we dive into that, though, a quick primer on CSS3 animations. These are currently supported only in WebKit browsers with -webkit- vendor prefixes. A CSS3 animation is made up of a series of keyframes grouped together as a named animation, created using the @-webkit-keyframes rule. Then we apply that animation to an element using the -webkit-animation-name property. We can also control the duration and easing of the animation with the -webkit-animation-duration and -webkit-animation-timing-function properties, respectively. If you’re new to animations, this is probably sounding more than a little confusing to you right now; never mind, once you see it in practice, it’ll be much clearer.

So let’s apply some animations to our elements. First up, we’ll set a timing function and a duration for our animations. These dictate how long a transition will take, and how the pages are eased from the start to the end point:

Example 4.20. stylesheets/transitions.css (excerpt)

.in, .out { -webkit-animation-timing-function: ease-in-out; -webkit-animation-duration: 300ms; } 

We’ve placed these properties in generic classes, so that we can reuse them on any future animations we create.

Next, we need to create our keyframes. To start with, let’s simply fade the new page in:

Example 4.21. stylesheets/transitions.css (excerpt)

@-webkit-keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } 

In the above rule, fade-in is the name of the animation, which we’ll refer to whenever we want to animate an element using these keyframes. The from and to keywords allow us to declare the start and end points of the animation, and they can include any number of CSS properties you’d like to animate. If you want more keyframes in between the start and end, you can declare them with percentages, like this:

Example 4.22. stylesheets/transitions.css (excerpt)

@-webkit-keyframes fade-in-out { from { opacity: 0; } 50% { opacity: 1; } to { opacity: 0; } } 

With our keyframes declared, we can combine them with the previous direction classes to create the final effect. For our fade, we’ll use the animation we defined above, and also flip the z-index on the pages to make sure the correct page is in front:

Example 4.23. stylesheets/transitions.css (excerpt)

.fade.in { -webkit-animation-name: fade-in; z-index: 10; } .fade.out { z-index: 0; } 

By declaring -webkit-animation-name, we’re telling the browser that as soon as an element matches this selector, it should begin the named animation.

With this CSS in place, we can move to step two. We’ll start by applying our animation to a single navigation item, then broaden it out later so that it will work for our whole tab bar.

The page we’re fading to (#sightings) will need to have three different classes added to it: current to make the page visible, fade to add our animation, and in to apply our timing function and duration. The page we’re fading from (#spots) is visible, so it will already have the current class; we only need to add the fade and out classes:

var fromPage = $("#spots"), toPage = $("#sightings"); $("#tab-sighting a").click(function(){ toPage.addclass("current fade in"); fromPage.addclass("fade out"); }); 

This gives us a nice fading effect when we click on the “Add a sighting” tab, but now the pages are stuck-stacked atop one another. This is because those class names are still there, so the pages now have current and they’re both visible. Time to remove them! We’ll do this by binding to the webkitAnimationEnd event, which fires when the transition is complete. When this event fires, we can remove all three classes from the original page, and the fade and in classes from the new page. Additionally, we must remember to unbind the webkitAnimationEnd event so that we don’t go adding on extra handlers the next time we fade from the page:

var fromPage = $("#spots"), toPage = $("#sightings"); $("#tab-sighting a").click(function(){ toPage .addclass("current fade in") .bind("webkitAnimationEnd", function(){ // More to do, once the animation is done. fromPage.removeclass("current fade out"); toPage .removeclass("fade in") .unbind("webkitAnimationEnd"); }); fromPage.addclass("fade out"); }); 

There we go. Our page is now fading nicely; however, there are a few problems with our code. The first is structural. It will become quite ugly if we have to replicate this same click handler for each set of pages we want to transition to! To remedy this, we’ll make a function called transition() that will accept a page selector and fade from the current page to the new one provided.

While we’re at it, we can replace our bind() and unbind() calls with jQuery’s one() method. This method will accomplish the same task-it binds an event, and then unbinds it the first time it’s fired-but it looks a lot cleaner:

Example 4.24. javascripts/ch4/08-fade.js (excerpt)

function transition(toPage) { var toPage = $(toPage), fromPage = $("#pages .current"); toPage .addclass("current fade in") .one("webkitAnimationEnd", function(){ fromPage.removeclass("current fade out"); toPage.removeclass("fade in") }); fromPage.addclass("fade out"); } 

Generalizing Functions

You might spy that we’ve hardcoded the current page selector inside our function. This makes our code smaller, but reduces the reusability of the function. If you are building a larger framework intended for more general use, you’d probably want to accept the fromPage as a parameter, too.

Great. Now we have a reusable function that we can employ to fade between any of the pages in our app. We can pull the link targets out of the tab bar the same way we did earlier, and suddenly every page swap is a beautiful fade:

Example 4.25. javascripts/ch4/08-fade.js (excerpt)

$("#tab-bar a").click(function(e) { e.preventDefault(); var nextPage = $(e.target.hash); transition(nextPage); $("#tab-bar").attr("className", e.target.hash.slice(1)); }); 

There’s still a major problem, though, and it’s one you’ll notice if you try to test this code on a browser that lacks support for animations, such as Firefox. Because we’re relying on the webkitAnimationEnd event to remove the current class from the old page, browsers that don’t support animations-and therefore never fire that event-will never hide the original page.

Browser Testing

This bug-which would render the application completely unusable on non-WebKit browsers- highlights the importance of testing your code on as many browsers as possible. While it can be easy to assume that every mobile browser contains an up-to-date version of WebKit (especially if you own an iPhone or Android), the real mobile landscape is far more varied.

This problem is easy enough to solve. At the end of our transition() function, we’ll drop in some feature detection code that will handle the simplified page swap in the absence of animations:

Example 4.26. javascripts/ch4/08-fade.js (excerpt)

function transition(toPage) { ... // For non-animatey browsers if(!("WebKitTransitionEvent" in window)){ toPage.addclass("current"); fromPage.removeclass("current"); return; } } 

With this code in place, our app now produces our beautiful fade transition on WebKit browsers, but still swaps pages out effectively on other browsers.

There’s still one slightly buggy behaviour, one you might notice if you become a little excited and start clicking like crazy. If you click on the link to the current page-or if you tap quickly to start an animation when the previous one has yet to complete-the class attributes we’re using to manage the application’s state will be left in an inconsistent state. Eventually, we’ll end up with no pages with the current class-at which point we’ll be staring at a blank screen.

It’s relatively easy to protect against these cases. We just need to ensure our toPage is different from our fromPage, and that it doesn’t already have the current class on it. This safeguard goes after the variable declaration, and before any class manipulations:

Example 4.27. javascripts/ch4/08-fade.js (excerpt)

function transition(toPage) { var toPage = $(toPage), fromPage = $("#pages .current"); if(toPage.hasclass("current") || toPage === fromPage) { return; }; ... 

4.3. Sliding

Awesome work-our first animated transition is up and running! Let’s move on to the next one: an extremely common mobile interaction for master-detail pages, such as our Spots and Stars listings. On mobile devices, it’s common for the detail page to slide in from the right side of the screen, as if it had been hiding there the whole time. When the user returns to the master page, the transition works in reverse, providing the user with a clear visual model of the app’s information hierarchy.

Creating a slide or “push” transition is very easy now that we’ve learned to fade-we just need to update our CSS animations. To perform a push, we need to animate the current page off the screen to the left while simultaneously moving the new page in from the right. It’s a bit trickier than just fading, but only just.

First, we’ll create an animation for moving the current screen away. We’ll use the -webkit-transform property to animate the div element’s horizontal location via the translateX command.

CSS Transforms

If you’re unfamiliar with CSS3 transforms, don’t worry. They’re simply an easy way to act on the position and shape of an element as its rendered. In addition to the translateX transform we’re using here, you also have access to translateY (unsurprisingly), translate (for both X- and Y-axis translation), as well as rotate, skew, and scale functions. For the full list and examples of how they can be used, check out the W3C’s CSS 2D Transforms Module.

The screen starts at X=0 and then moves out of sight by translating to –100%. This is a nifty trick. If we translate by a negative amount, the screen moves to the left. By moving 100% to the left, the screen is gone. Once we’ve defined the keyframes, we can then assign the animation to the .push.out selector (remember that our existing out class provides a default duration and timing function for our animations):

Example 4.28. stylesheets/transitions.css (excerpt)

/* Screen pushes out to left */ @-webkit-keyframes outToLeft { from { -webkit-transform: translateX(0); } to { -webkit-transform: translateX(-100%); } } .push.out { -webkit-animation-name: outToLeft; } 

Similarly, we’ll define an animation for the new screen to fly in from the right, taking the place of the old one:

Example 4.29. stylesheets/transitions.css (excerpt)

/* Screen pushes in from the right */ @-webkit-keyframes inFromRight { from { -webkit-transform: translateX(100%); } to { -webkit-transform: translateX(0); } } .push.in { -webkit-animation-name: inFromRight; } 

For the JavaScript, we could recycle the transition function we made for the fade, calling it transition_push(), for example. We’d then just need to change all the instances of fade to push. And then again for flip when we want to implement flip transitions, and so on. On second thoughts, it would be nicer to pass the transition type in as a parameter to our transition() function:

Example 4.30. javascripts/ch4/09-slide.js (excerpt)

function transition(toPage, type) { ... toPage .addclass("current " + type + " in") .one("webkitAnimationEnd", function(){ fromPage.removeclass("current " + type + " out"); toPage.removeclass(type + " in"); }); fromPage.addclass(type + " out"); } 

Now when we create CSS animations for a new transition, they’ll automatically be available to use in our script. We just pass the new name in:

transition(nextPage, "push"); 

We’d like the push transition to be used for navigating down from a list to a detail page, so we need to add some new click handlers for the list items:

Example 4.31. javascripts/ch4/09-slide.js (excerpt)

$("#spots-list li").click(function(e){ e.preventDefault(); transition("#page-spot", "push"); }); $("#stars-list li").click(function(e){ e.preventDefault(); transition("#page-star", "push"); }); 

With this, the detail pages will slide in from the right to replace the list pages. As we outlined earlier, though, what we’d also like is for the reverse to occur when the user is navigating back up the app’s hierarchy. Next up, we’ll look at building out that functionality, and, while we’re at it, adding support for going “back.”

4.4. Going Backwards

The user is now looking at a page of details about some crazy celebrity, and they’re bored. They want a new crazy celebrity to read about, so they go looking for the Back button. But going back is more than just swapping the source and destination pages again, because the animations we applied need to be reversed: the old page needs to slide back from the left into view.

But that’s getting ahead of ourselves; first, we need a Back button. We’ve provided one up in the header of each page in the form of an a element that’s styled to look all button-like:

Example 4.32. ch4/10-back.html (excerpt)

<div class="header"> <h1>Spots</h1> <a href="#" class="back">Back</a> </div> 

And of course, we must have a handler to perform an action when the button is clicked:

Example 4.33. javascripts/ch4/10-back.js (excerpt)

$("#spot-details .back").click(function(){ // Do something when clicked ... }); 

Next, we need to recreate all our CSS animations-but in reverse. We’ve already created inFromRight and outFromLeft animations; we need to add two more to complement them: inFromLeft and outToRight. Once these are defined, they have to be attached to our elements with CSS selectors. We’ll continue the modular approach, and use a combination of class selectors to leverage our existing properties:

Example 4.34. stylesheets/transitions.css (excerpt)

@-webkit-keyframes inFromLeft { from { -webkit-transform: translateX(-100%); } to { -webkit-transform: translateX(0); } } .push.in.reverse { -webkit-animation-name: inFromLeft; } @-webkit-keyframes outToRight { from { -webkit-transform: translateX(0); } to { -webkit-transform: translateX(100%); } } .push.out.reverse { -webkit-animation-name: outToRight; } 

The next step is to work the new class into our transition() function. We’ll add a third parameter, reverse, that accepts a Boolean value. If the value is false, or if it’s not provided at all, we’ll do the forward version of the transition. If the value is true, we’ll append the reverse class to all the class manipulation operations:

Example 4.35. javascripts/ch4/10-back.js (excerpt)

function transition(toPage, type, reverse){ var toPage = $(toPage), fromPage = $("#pages .current"), reverse = reverse ? "reverse" : ""; if(toPage.hasclass("current") || toPage === fromPage) { return; }; toPage .addclass("current " + type + " in " + reverse) .one("webkitAnimationEnd", function(){ fromPage.removeclass("current " + type + " out " + reverse); toPage.removeclass(type + " in " + reverse); }); fromPage.addclass(type + " out " + reverse); } 

If we pass in true now, the new page will be assigned the class attribute push in reverse, and the old page will be assigned push out reverse-which will trigger our new backwards animations. To see it in action, we’ll add a call to transition() in our Back button hander:

Example 4.36. javascripts/ch4/10-back.js (excerpt)

$("#page-spot .back").click(function(e){ e.preventDefault(); transition("#page-spots", "push", true); }); 

4.4.1. Managing History

The Back button works, but it’s a bit “manual” at the moment. For every page in our app, we’d have to hook up a separate handler to go back. Worse still, some pages could be reached via a number of different routes, yet our current solution only goes back to a fixed page. To combat these problems, we’ll create our very own history system that will keep track of each page users visit, so that when they hit the Back button, we know where we should send them.

To start with, we’ll create a visits object, which will contain a history array and some methods to manage it:

Example 4.37. javascripts/ch4/11-history.js (excerpt)

var visits = { history: [], add: function(page) { this.history.push(page); } }; 

Our visits object will maintain a stack of visited pages in the history array. The add() method takes a page and prepends it to the stack (via the JavaScript push() function, which adds an element to the end of an array). We’ll call this method from inside our transition() function, so that every page will be added before it’s shown:

Example 4.38. javascripts/ch4/11-history.js (excerpt)

function transition(toPage, type, reverse) { var toPage = $(toPage), fromPage = $("#pages .current"), reverse = reverse ? "reverse" : ""; visits.add(toPage); ... } 

Centralizing Page Changes

The assumption that every transition corresponds to a page change is convenient for us, otherwise we’d have to call visits.add() everywhere we do a transition. However, there might be times when you want to do a transition to a new page, but not include it as a page change-for example, if you have some kind of slide-up dialog. In this case, you could create a changePage() function that handles both history management and transitioning. We’ll be doing this in the next section.

The next item to think about is our Back button. We only want it to be shown if there’s a history item to revert to. We’ll add a helper method to the visits object to check for us. Because the first page in the history will be the current page, we need to check that there are at least two pages:

Example 4.39. javascripts/ch4/11-history.js (excerpt)

var visits = { ... hasBack: function() { return this.history.length > 1; } } 

Now that we have this helper, we can use it in our transition code to show or hide the Back button accordingly. The toggle() jQuery function is very useful here; it accepts a Boolean value, and either shows or hides the element based on that value:

Example 4.40. javascripts/ch4/11-history.js (excerpt)

function transition(toPage, type, reverse) { var toPage = $(toPage), fromPage = $("#pages .current"); reverse = reverse ? "reverse" : ""; visits.add(toPage); toPage.find(".back").toggle(visits.hasBack()); ... 

Good! Now we need some logic in our visits object to handle a back event. If there is history, we’ll pop the first item (the current page) off the top of the stack. We don’t actually need this page-but we have to remove it to reach the next item. This item is the previous page, and it’s the one we return:

Example 4.41. javascripts/ch4/11-history.js (excerpt)

var visits = { ... back: function() { if(!this.hasBack()){ return; } var curPage = this.history.pop(); return this.history.pop(); } } 

Push and Pop

The push() and pop() methods add or remove an element from the end of an array, respectively. Both methods modify the original array in place. The pop() method returns the element that has been removed (in our example, we use this to get the previous page), whereas the push() method returns the new length of the array.

Finally, we can wire up all our application’s Back buttons. When a request to go back is issued, we grab the previous page and, if it exists, we transition back to it. We just replace our hardcoded click handler with a general-purpose one:

Example 4.42. javascripts/ch4/11-history.js (excerpt)

$(".back").live("click",function(){ var lastPage = visits.back(); if(lastPage) { transition(lastPage, "push", true); } }); 

There’s still a problem, though: we never add the initial page to the history stack, so there’s no way to navigate back to it. That’s easy enough to fix-we’ll just remove the current class from the initial div, and call our transition function to show the first page when the document loads:

Example 4.43. javascripts/ch4/11-history.js (excerpt)

$(document).ready(function() { ... transition($("#page-spots"), "show"); }); 

To hook up that “show” transition, we’ll reuse our fade animation, but with an extremely short duration:

Example 4.44. stylesheets/transitions.css (excerpt)

.show.in { -webkit-animation-name: fade-in; -webkit-animation-duration: 10ms; } 

Many native apps only track history between master and details pages; in our case, for example, a list of stars leads to the star’s details, and the Back button allows you to jump back up to the list. If you change areas of the application (for example, by clicking on one of the main navigation links), the history is reset. We can mimic this behaviour by adding a clear() method:

Example 4.45. javascripts/ch4/11-history.js (excerpt)

var visits = { ... clear: function() { this.history = []; } } 

This simply erases our history stack. We’ll call this method whenever the user moves to a new section:

Example 4.46. javascripts/ch4/11-history.js (excerpt)

$("#tab-bar a").click(function(e){ // Clear visit history visits.clear(); ... }); 

This has a very “app” feeling, and, as an added bonus, we don’t have to wire up so many Back button events!

4.4.2. Back with Hardware Buttons

Our current Back button system is good, but it doesn’t take into account the fact that a mobile device will often have its own Back button-either in the form of a physical button, or a soft button in the browser. As it stands, if a user hits their device’s Back button after clicking a few internal links in our app, the browser will simply move to the last HTML page it loaded, or exit completely. This will definitely break our users’ illusion of our site as a full-fledged app, so let’s see if we can find a fix for this problem.

What we really need is to be able to listen to, and modify, the browser’s built-in history, instead of our own custom stack of pages. To accomplish this, the HTML5 History API is here to help us out.

The History API lets us add pages to the history stack, as well as move forward and backwards between pages in the stack. To add pages, we use the window.history.pushState() method. This method is analogous to our visits.add() method from earlier, but takes three parameters: any arbitrary data we want to remember about the page; a page title (if applicable); and the URL of the page.

We’re going to create a method changePage() that combines both adding a page using the history API, and doing our regular transition. We’ll keep track of the transition inside the history, so that when the user presses back, we can look at the transition and do the opposite. This is nicer than our previous version, where we’d only ever do a reverse slide for the back transition.

Here’s a first stab at writing out this new method:

Example 4.47. javascripts/ch4/12-hardware-back.js (excerpt)

function changePage(page, type, reverse) { window.history.pushState({ page: page, transition: type, reverse: !!reverse }, "", page); // Do the real transition transition(page, type, reverse) } 

The first parameter to pushState() is referred to as the state object. You can use it to pass any amount of data between pages in your app in the form of a JavaScript object. In our case, we’re passing the page, the transition type, and whether or not it’s a reverse transition.

To use this new function in our code, we merely change all occurrences of transition() to changePage(), for example:

changePage("#page-spots", "show"); 

Now, as the user moves through our application, the history is being stored away. If they hit the physical Back button, you can see the page history in the URL bar, but nothing special happens. This is to be expected: we’ve just pushed a series of page strings onto the history stack, but we haven’t told the app how to navigate back to them.

The window.onPopState event is fired whenever a real page load event happens, or when the user hits Back or Forward. The event is fed an object called state that contains the state object we put there with pushStack() (if the state is undefined, it means the event was fired from a page load, rather than a history change-so it’s of no concern). Let’s create a handler for this event:

Example 4.48. javascripts/ch4/12-hardware-back.js (excerpt)

window.addEventListener("popstate", function(event) { if(!event.state){ return; } // Transition back - but in reverse. transition( event.state.page, event.state.transition, !event.state.reverse ); }, false); 

Where’s jQuery?

For this example, we’ve just used a standard DOM event listener rather than the jQuery bind() method. This is just for clarity for the popstate event. If we bound it using $(window).bind("popstate", ...), the event object passed to the callback would be a jQuery event object, not the browser’s native popstate event. Usually that’s what we want, but jQuery’s event wrapper doesn’t include the properties from the History API, so we’d need to call event.originalEvent to retrieve the browser event. There’s nothing wrong with that-you can feel free to use whichever approach you find simplest.

Fantastic! The animations all appear to be working in reverse when we hit the browser Back button … or are they? If you look closely, you might notice something strange. Sometimes we see “slide” transitions that should be simple “show” transitions, and vice versa. What’s going on?

Actually, we have an off-by-one error happening here: when moving backwards, we don’t want to use the transition of the page we are transitioning to, but the page we are transitioning from. Unfortunately, this means we need to call pushState() with the next transition that happens. But we’re unable to see the future … how can we know what transition is going to happen next?

Thankfully, the History API provides us with another method, replaceState(). It’s almost identical to pushState(), but instead of adding to the stack, it replaces the current (topmost) page on the stack. To solve our problem, we’ll hang on to the details of the previous pushState(); then, before we add the next item, we’ll use replaceState() to update the page with the “next” transition:

Example 4.49. javascripts/ch4/12-hardware-back.js (excerpt)

var pageState = {}; function changePage(page, type, reverse) { // Store the transition with the state if(pageState.url){ // Update the previous transition to be the NEXT transition pageState.state.transition = type; window.history.replaceState( pageState.state, pageState.title, pageState.url); } // Keep the state details for next time! pageState = { state: { page: page, transition: type, reverse: reverse }, title: "", url: page } window.history.pushState(pageState.state, pageState.title, pageState.url); // Do the real transition transition(page, type, reverse) } 

We also need to update our pageState variable when the user goes back; otherwise, it would fall out of sync with the browser’s history, and our replaceState() calls would end up inserting bogus entries into the history:

Example 4.50. javascripts/ch4/12-hardware-back.js (excerpt)

window.addEventListener("popstate", function(event) { if(!event.state){ return; } // Transition back - but in reverse. transition( event.state.page, event.state.transition, !event.state.reverse ); pageState = { state: { page: event.state.page, transition: event.state.transition, reverse: event.state.reverse }, title: "", url: event.state.page } }, false); 

There we go. The physical Back button now works beautifully. But what about our custom application Back button? We can wire that up to trigger a history event, and therefore tie into all that History API jazz we just wrote using a quick call to history.back():

Example 4.51. javascripts/ch4/12-hardware-back.js (excerpt)

$(".back").live("click",function(e){ window.history.back(); }); 

Now our application Back button works exactly like the browser or physical Back button. You can also wire up a Forward button and trigger it with history.forward(), or skip to a particular page in the stack with history.go(-3). You might have noticed that we’ve been a bit quiet on the Forward button handling. There are two reasons for this: first, most mobile browsers lack a Forward button, and second, it’s impossible to know if the popstate event occurred because of the Back or the Forward button.

The only way you could get around this pickle would be to combine the popstate method with the manual history management system we built in the previous section, looking at the URLs or other data to determine the direction of the stack movement. This is a lot of work for very little return in terms of usability, so we’ll settle for the history and back functionality we’ve built, and move on to the next challenge.

Build Mobile Book

You can purchase the book “Build Mobile Websites and Apps for Smart Devices” from Sitepoint. Read the whole of Chapter 4. Mobile Web Apps, exclusively here at BuildMobile, for free, in the following sections.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Ben Barber

    Wow, this is a really excellent article. The History API examples are spot-on.

    I have a very minor, soapboxy complaint with Example 4.15. It seems that this one line is preventing you from having a fully-functional, navigable and accessible non-JavaScript fall-back. If you hid “#pages > div” with JavaScript instead (and removed the “page-” prefix typo in your page ids), you’d be golden.

    • http://www.mrspeaker.net/ Mr Speaker

      Hey Ben – to tell you the truth, I hadn’t thought about the nice, non-JavaScript fallback because I was writing it in the context of a bigger web app where JavaScript is necessary – and I missed the bonus that “web is web”: we can write the component parts in a way that does better than just supporting the latest and greatest.

      So, yep… good idea – hide the divs with JavaScript!

  • PhilD

    What a killer article
    – I’ll be using this as a reference for next mobile gig.
    Thanks

  • http://www.kreativedesign.lu Nuno

    There are a lot of great tips in here. Many of these will have to be put in place in the near future… Thanks

  • Johannes Kristjansson

    Hi !

    Fantastic tutorial :-) I am new to both jQuery and JavaScript, but have only had little trouble following this tutorial. It is very well explained!

    I only have one problem, that I have now struggled with for a while. In Example 4.47, I am getting this error in my console:
    Uncaught Error: DataCloneError: DOM Exception 25

    It has something to do with this line in particular:
    window.history.pushState(pageState.state, pageState.title, pageState.url);

    Can anyone help?
    – Thanks :-)