Mobile
 

Mobile Web Apps : Loading Pages (part 3) - Going Backwards

1/28/2012 5:49:54 PM

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:

listing 17. 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:

listing 18. 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:

listing 19. 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:

listing 20. 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:

listing 21. javascripts/ch4/10-back.js (excerpt)
$("#page-spot .back").click(function(e){
e.preventDefault();
transition("#page-spots", "push", true);
});

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:

listing 22. 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()transition() function, so that every page will be added before it’s shown: function, which adds an element to the end of an array). We’ll call this method from inside our

listing 23. javascripts/ch4/11-history.js (excerpt)
function transition(toPage, type, reverse) {
var toPage = $(toPage),
fromPage = $("#pages .current"),
reverse = reverse ? "reverse" : "";

visits.add(toPage);

}


Note:

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.


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:

listing 24. 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:

listing 25. 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:

listing 26. javascripts/ch4/11-history.js (excerpt)
var visits = {

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


Note:

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:

listing 27. 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:

listing 28. 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:

listing 29. 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 behavior by adding a clear() method:

listing 30. 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:

listing 31. 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.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:

listing 32. 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:

listing 33. 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);


Note:

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:

listing 34. 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:

listing 35. 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():

listing 36. 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.
 
Others
 
- Mobile Web Apps : Loading Pages (part 2) - Sliding
- Mobile Web Apps : Loading Pages (part 1) - Swapping Pages & Fading with WebKit Animations
- Personalize & Secure Your iPad : Adjusting Sounds on your iPad & Personalize Your Picture Frame
- Personalize & Secure Your iPad : Changing your Lock Screen and Home Screen Wallpapers
- Using Media in XNA Game Studio : Media Enumeration
- Using Media in XNA Game Studio : What Is Media?
- Android Application Development : Layouts (part 2) - AbsoluteLayout & RelativeLayout
- Android Application Development : Layouts (part 1) - inearLayout
- Building an Advanced Java Game on Symbian OS (part 4) - Using the Bluetooth API
- Building an Advanced Java Game on Symbian OS (part 3) - Using the Mobile 3D Graphics API
- Building an Advanced Java Game on Symbian OS (part 2) - Using the Mobile Media API & Using the Scalable 2D Vector Graphics API
- Building an Advanced Java Game on Symbian OS (part 1)
- jQuery 1.3 : Simultaneous versus queued effects (part 2) - Working with multiple sets of elements & Callbacks
- jQuery 1.3 : Simultaneous versus queued effects (part 1) - Working with a single set of elements
- iPhone 3D Programming : Crisper Text with Distance Fields (part 3) - Implementing Outline, Glow, and Shadow Effects
- iPhone 3D Programming : Crisper Text with Distance Fields (part 2) - Smoothing and Derivatives
- iPhone 3D Programming : Crisper Text with Distance Fields (part 1) - Generating Distance Fields with Python
- Mapping Well-Known Patterns onto Symbian OS : Singleton
- Mapping Well-Known Patterns onto Symbian OS : Model–View–Controller
- The Anatomy of a Mobile Site : STYLING WITH CSS - CSS Considerations for Mobile & Optimizing CSS
 
 
Most View
 
- Sharepoint 2013 : Organizing and managing information - Associating document templates with content types
- Windows 7 : Using BitLocker Drive Encryption
- Overview of Oauth in Sharepoint 2013 : Application Authorization - On-Premises App Authentication with S2S
- SQL Server 2012 : Delivering A SQL Server Health Check (part 7)
- Overview of Oauth in Sharepoint 2013 : Application Authorization - Requesting Permissions Dynamically
- Microsoft Visio 2010 : Using Visio Graphics with Other Applications
- Windows 7 : BitLocker (part 2) - How to Enable BitLocker Encryption
- Sharepoint 2013 : Managing and Configuring Profile Synchronization (part 4) - Configuring the Synchronization Connection
- Windows Server 2008 R2 Remote Desktop Services : Installing and Configuring Remote Desktop Services (part 6)
- Windows 8 : Using the Control Panel Items (part 2) - File History - Saving File History to a Network Location
 
 
Top 10
 
- Sharepoint 2013 : Developing Integrated Apps for Office and Sharepoint Solutions - The New App Model for Office
- Overview of Oauth in Sharepoint 2013 : Application Authorization - On-Premises App Authentication with S2S
- Overview of Oauth in Sharepoint 2013 : Application Authorization - Requesting Permissions Dynamically
- Microsoft Excel 2010 : Working with Graphics - Inserting a Diagram,Inserting an Object
- Microsoft Excel 2010 : Working with Graphics - Inserting WordArt, Using Smart Art in Excel
- Microsoft Excel 2010 : Working with Graphics - Using AutoShapes
- Overview of Oauth in Sharepoint 2013 : Application Authentication (part 2) - Managing Tokens in Your Application
- Overview of Oauth in Sharepoint 2013 : Application Authentication (part 1) - Using TokenHelper
- Overview of Oauth in Sharepoint 2013 : Creating and Managing Application Identities
- Overview of Oauth in Sharepoint 2013 : Introduction to OAuth