jQuery Mobile Masterclass: Build a Simple, Attractive Twitter App for iPhone

Learn how to create a complete, good-looking mobile web app using HTML, CSS, JavaScript and jQuery Mobile. Demo and full code download included.

jQuery Mobile Masterclass: Build a Simple, Attractive Twitter App for iPhone

Open on your iPhone or iPad!

jQuery Mobile is a great framework for building mobile web apps, and as the 1.0 release gets nearer it's really starting to mature into a stable platform.

In my previous jQuery Mobile article, I gave a quick overview of jQuery Mobile's headline features. In this tutorial you'll delve deeper into jQuery Mobile. You'll build a simple, nice-looking, easy-to-use iPhone Twitter app called "CloseFriends" that displays your closest Twitter friends' latest tweets.

You'll explore the following topics as you work through the tutorial:

  • How to build a web app using just HTML, CSS and JavaScript (no server-side code).
  • Making an app that can be installed and launched from the iPhone home screen.
  • Creating rich mobile interfaces using jQuery Mobile, including page transitions, list views, buttons and dialogs.
  • Using Scott Jehl's excellent pagination plugin to create a sequence of draggable "latest tweet" pages, much like the native iOS Weather app.
  • Tweaking the pagination plugin to create a nicer swipe experience.
  • Working with the Twitter API using JavaScript.
  • How to use HTML5 local storage to store data persistently in the browser.
  • Creating your own jQuery Mobile swatches to theme your app.
  • Working with jQuery Mobile events.
  • Adapting to different device screen sizes and orientations.
  • ...and lots more!

You can try out the finished app on your iOS device by tapping the View Demo button above, and download the complete code and image files by clicking the Download Code button.

To get the most from this tutorial, it helps to have some familiarity with JavaScript, jQuery and jQuery Mobile. A great way to get up to speed quickly with jQuery Mobile is to read my comprehensive eBook, Master Mobile Web Apps with jQuery Mobile.

Ready to build the CloseFriends app? Let's go!

Step 1: Download the pagination plugin

The app will link directly to the jQuery and jQuery Mobile files on the Google CDN, so you don't need to download those files. However, the pagination plugin isn't hosted on a CDN, so you need to grab it and save it on your computer.

You need to download two files from the plugin's GitHub page:

  • jquery.mobile.pagination.css
  • jquery.mobile.pagination.js

Create a folder for your app called CloseFriends somewhere on your computer, and save these two files inside that folder.

Step 2: The markup

Pretty much all web apps start with some markup, usually in the form of an index.html page that sets up the basic DOM and loads all the required JavaScript and CSS files.

Save the following file as index.html in your CloseFriends folder:

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <link rel="apple-touch-icon" href="icon.png"/>
	<title>CloseFriends</title>
	<link rel="stylesheet" href="http://code.jquery.com/mobile/1.0rc2/jquery.mobile-1.0rc2.min.css" />
	<link rel="stylesheet" href="jquery.mobile.pagination.css">
	<link rel="stylesheet" href="CloseFriends.css">	
	<script src="http://code.jquery.com/jquery-1.6.4.min.js"></script>
	<script src="http://code.jquery.com/mobile/1.0rc2/jquery.mobile-1.0rc2.min.js"></script>
	<script src="jquery.mobile.pagination.js"></script>
	<script src="CloseFriends.js"></script>
</head>
<body>

<div data-role="page" id="manageFriends" data-theme="a">

  <div data-role="header">
    <h2>CloseFriends</h2>
    <a href="#addFriendDialog" id="addFriend" data-icon="plus" data-iconpos="notext">Add Friend</a>
    <a id="done" data-theme="b" value="Done" class="ui-btn-right">Done</a>
  </div>

  <ul data-role="listview" id="friendsList" data-split-icon="delete" data-split-theme="f">
  </ul>

</div>

<div data-role="dialog" id="addFriendDialog" data-theme="b">

  <div data-role="header">
    <h2>Add Friend</h2>
  </div>

  <div data-role="content">

    <div data-role="fieldcontain">
      <label for="screenName">Twitter Screen Name:</label>
      <input type="text" name="screenName" id="screenName">
    </div>

    <p><a href="#" id="addFriendConfirm" data-role="button" data-transition="slidedown">Add Friend</a></p>
    <p><a href="#manageFriends" data-role="button" data-rel="back">Cancel</a></p>

  </div>
    
</div>

<div id="dots"></div>

</body>
</html>

The index.html file contains the following sections:

  • The head section. This contains a viewport meta tag to ensure that the page zooms into 100% on the device; an apple-mobile-web-app-capable meta tag, which tells the iPhone that this app can be installed as an icon on the home screen; and an apple-touch-icon link tag that tells the iPhone what icon to use for the app on the home screen. (You'll create this icon later in the tutorial.) The head section also includes links to the various stylesheets and JavaScript files required for the jQuery and jQuery Mobile libraries, as well as the pagination plugin and our CloseFriends app code.

    Find out more about mobile viewports, and the viewport meta tag, in Responsive Web Design Demystified.

  • The "Manage Friends" page. The first element inside the page body is a jQuery Mobile data-role="page" div, with an id of manageFriends. This page will display the list of Twitter friends that the user has added in the app. The div has a jQuery Mobile data-theme="a" attribute to make the page use the "a" (white-on-dark-grey) swatch. The page's header contains the app name, a "plus" button to add a new friend, and a "Done" button to return to the tweet pages. The main part of the page contains a jQuery Mobile list view widget — created using the data-role="listview" attribute — with an id of friendsList. This widget will hold the list of Twitter usernames.
  • The "Add Friend" dialog. The body also contains a div with an id of addFriendDialog. This appears when the user taps the "plus" button in the "Manage Friends" page. The div has a data-role="dialog" attribute which makes jQuery Mobile style the page as a dialog, and a data-theme="b" attribute to give the dialog the "b" (black on light grey) swatch. The dialog comprises an "Add Friend" header, a screenName field for entering the new friend's Twitter screen name, an #addFriendConfirm button to add the friend, and a Cancel button that just closes the dialog.

    The "plus" button in the "Manage Friends" page links to this dialog using <a href="#addFriendDialog">...</a>. When the user taps the button, jQuery Mobile intercepts the tap, notices that the target div uses data-role="dialog", and automatically opens the div as a dialog. No JavaScript required!

  • A container for the dots. Lastly, the body contains a div with an id of dots. This will contain the small pagination dots that sit below the tweet pages, allowing the user to orient themselves in the list of pages.

Step 3: The CSS

As well as the stylesheets for jQuery Mobile and the pagination plugin, the index.html file also links to a custom stylesheet for the CloseFriends app. Let's create this stylesheet now.

Save the following code as CloseFriends.css inside your CloseFriends folder:


/* The page body colours */

body {
  background: #222;
  color: #fff;
}


/* Stretch mobile pages to fill the browser window */

.ui-page {
  height: 100%;
}


/* Main content in tweet page */

.tweetPageContent {
  width: 87%;
  height: 83%;
  margin: 30px auto;
  border-radius: 10px;
  border: 4px solid #627188;
  background: 			#354866;
  color: 					#fff;
  text-shadow: 0 1px 0 #000;
  font-weight: normal;
  background-image: -webkit-gradient(linear, left top, left bottom, from(#304360), to(#3a4e6c));
  background-image: -webkit-linear-gradient(top, #304360, #3a4e6c);
  background-image:    -moz-linear-gradient(top, #304360, #3a4e6c);
  background-image:     -ms-linear-gradient(top, #304360, #3a4e6c);
  background-image:      -o-linear-gradient(top, #304360, #3a4e6c);
  background-image:         linear-gradient(top, #304360, #3a4e6c);
  text-align: center;
}


/* Gloss effect on tweet page */

.gloss {
  width: 87%;
  height: 7%;
  margin: 0 auto;
  left: 0;
  right: 0;
  top: 30px;
  padding: 0 4px;
  position: absolute;
  z-index: 9999;
  background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,.4)), to(rgba(255,255,255,.05)));
  background-image: -webkit-linear-gradient(top, rgba(255,255,255,.4), rgba(255,255,255,.05));
  background-image:    -moz-linear-gradient(top, rgba(255,255,255,.4), rgba(255,255,255,.05));
  background-image:     -ms-linear-gradient(top, rgba(255,255,255,.4), rgba(255,255,255,.05));
  background-image:      -o-linear-gradient(top, rgba(255,255,255,.4), rgba(255,255,255,.05));
  background-image:         linear-gradient(top, rgba(255,255,255,.4), rgba(255,255,255,.05));  
  border-radius: 10px 10px 0 0;

}


/* Tweet timestamp in bottom left corner of tweet page */

.tweetTime {
  position: absolute;
  bottom: 7px;
  left: 6px;
  opacity: .5;
}


/* Manage Friends 'i' button in bottom right corner of tweet page */

.manageFriendsButton {
  position: absolute;
  bottom: -3px;
  right: 0px;
  color: #333;
  background: #fff;
  border: none;
}


/* Twitter profile image */

.profileImage {
  width: 100px;
  height: 100px;
  margin: 20px auto;
  border-radius: 5px;
}


/* Twitter screen name */

.tweetPageContent h1 {
  text-align: center;
  font-size: 100%;
  margin-top: -10px;
}


/* Tweet */

.tweetPageContent .recentTweet {
  margin: -10px 20px;
  font-family: "Georgia", "Times New Roman", serif;
  font-size: 125%;
  min-height: 55%;
  background: url("quoteMarks.png") no-repeat 50% 50%;
  display: -webkit-box;
  display: -moz-box;
  display: box;
  -webkit-box-align: center;
  -moz-box-align: center;
  box-align: center;
    
}

.tweetPageContent .recentTweet blockquote {
  text-align: center;
  width: 100%;
  margin: 0;
}


/* Links within the tweet */

.tweetPageContent blockquote a {
  color: #fff;
  opacity: .7;
  text-decoration: none;
}


/* Put the pagination buttons at the bottom and make them invisible */

.ui-pagination {
  position: absolute;
  top: auto;
  z-index: 9999;
  bottom: 52px;
}

.ui-pagination .ui-pagination-prev, .ui-pagination .ui-pagination-next {
  top: 0;
  width: 49%;
}

.ui-pagination .ui-pagination-prev {
  left: 0;
}

.ui-pagination .ui-pagination-next {
  right: 0;
}

.ui-pagination .ui-pagination-prev .ui-btn, .ui-pagination .ui-pagination-next .ui-btn {
  width: 100%;
  height: 80px;
  margin-top: 0;
  opacity: 0;
}


/* The pagination dots at the bottom of the page */

#dots {
  position: absolute;
  z-index: 9999;
  bottom: 8px;
  height: 30px;
  line-height: 0;
  color: #888;
  width: 100%;
  text-align: center;
  font-size: 55px;
  letter-spacing: 1px;
}

#dots .highlight {
  color: #fff;
}


/*** Media queries to handle different iOS devices and orientations ***/


/* iPhone, standalone */

@media only screen and (max-device-width: 480px) and (orientation: portrait) {
  .tweetPageContent { height: 380px; }
}

@media only screen and (max-device-width: 480px) and (orientation: landscape) {
  .tweetPageContent { height: 230px; font-size: 95%; }
  .profileImage { width: 60px; height: 60px; }
  .tweetPageContent .recentTweet { min-height: 80px; font-size: 95%; background-size: 25%; }
}


/* iPhone, running in Safari */

@media only screen and (max-height: 360px) and (orientation: portrait) {
  .tweetPageContent { height: 330px; font-size: 85%; }
  .tweetPageContent .recentTweet { font-size: 120%; margin-top: -20px; }
  #dots { bottom: -50px; }
  .ui-pagination { bottom: 65px; }
}

@media only screen and (max-height: 260px) and (orientation: landscape) {
  .tweetPageContent { font-size: 95%; margin-top: 10px; }
  .profileImage { width: 60px; height: 60px; }
  .gloss { top: 10px; }
  #dots { bottom: -63px; }
  .ui-pagination { bottom: 72px; }
}


/* iPad */

@media only screen and (device-width: 768px) and (orientation: portrait) {
  .tweetPageContent { height: 92%; font-size: 150%; }
}

@media only screen and (device-width: 768px) and (orientation: landscape) {
  .tweetPageContent { height: 89%; font-size: 150%; }
}


/*** jQuery Mobile swatches ***/

/* A swatch */
.ui-body-a {
  background: #000;
}

/* F swatch */

.ui-btn-up-f {
  border: 1px solid #b11;
  background: #a00;
  font-weight: bold;
  color: #fff;
  cursor: pointer;
  text-shadow: 0 -1px 1px #000;
  text-decoration: none;
  background-image: -moz-linear-gradient(top, #e55, #a00);
  background-image: -webkit-gradient(linear,left top,left bottom, color-stop(0, #f66), color-stop(1, #a00));
  -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#f66', EndColorStr='#a00')";
}
.ui-btn-up-f a.ui-link-inherit {
  color: #fff;
}
.ui-btn-hover-f {
  border: 1px solid #a00;
  background: #b11;
  font-weight: bold;
  color: #fff;
  text-shadow: 0 -1px 1px #000;
  text-decoration: none;
  background-image: -moz-linear-gradient(top, #f66, #b11);
  background-image: -webkit-gradient(linear,left top,left bottom, color-stop(0, #f66), color-stop(1, #b11));
  -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#f66', EndColorStr='#b11')";
}
.ui-btn-hover-f a.ui-link-inherit {
  color: #fff;
}
.ui-btn-down-f {
  border: 1px solid #800;
  background: #e55;
  font-weight: bold;
  color: #fff;
  text-shadow: 0 -1px 1px #000;
  background-image: -moz-linear-gradient(top, #800, #e55);
  background-image: -webkit-gradient(linear,left top,left bottom, color-stop(0, #800), color-stop(1, #e55));
  -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorStr='#800', EndColorStr='#e55')";
}
.ui-btn-down-f a.ui-link-inherit {
  color: #fff;
}
.ui-btn-up-f,
.ui-btn-hover-f,
.ui-btn-down-f {
  font-family: Helvetica, Arial, sans-serif;
}

Let's take a brief look at each ruleset in the CSS:

  • The page body colours: This sets the document's body background to dark grey so that it matches the "a" swatch used for pages in the app.
  • Stretch mobile pages to fill the browser window: This ruleset ensures that the jQuery Mobile page stretches vertically to fill the browser window, which lets us use percentage heights for other page elements.
  • Main content in tweet page: This styles the .tweetPageContent divs, which are the blue rounded rectangles containing the user's profile picture, screen name, tweet, timestamp and info button. We make these containers fill most of the browser window; add a rounded blue border; and set a white foreground colour and a blue gradient background.
  • Gloss effect on tweet page: This adds the shiny gloss effect at the top of the tweet page, much like the iOS Weather widget. It's positioned absolutely on top of the other page content, and centred horizontally. We give it a semitransparent gradient using the rgba colour format, which lets us add an alpha (opacity) value to each colour stop, where 1 is 100% opaque and 0 is 100% transparent.
  • Tweet timestamp in bottom left corner of tweet page: We position this absolutely, and make it semitransparent so it doesn't distract from the tweet text.
  • Manage Friends 'i' button in bottom right corner of tweet page: Again, this is positioned absolutely. We tweak the standard jQuery Mobile button style slightly to make the button blend in with the page design.
  • Twitter profile image: This sits at the top of the tweet page. It's centred horizontally and given rounded corners.
  • Twitter screen name: This goes below the profile image. It's also centred, and we give it a negative top margin to move it closer to the image.
  • Tweet: The tweet text itself is set in large Georgia font, centred horizontally, and given a background "quote marks" image (which you'll create later). We use the CSS3 display: box and box-align: center properties to centre the text vertically on top of the "quote marks" image.

    Read more about the CSS3 flexible box model.

  • Links within the tweet: These are made semitransparent so that they stand out from the other tweet text.
  • Put the pagination buttons at the bottom and make them invisible: By default, the pagination plugin creates tappable previous/next buttons on the left and right side of the screen. We want our app to behave more like the iOS Weather app, where you can tap the areas to the left and right of the dots at the bottom of the screen to move between pages. So this CSS moves the pagination buttons down to the bottom, and makes them invisible (but still tappable).
  • The pagination dots at the bottom of the page: We'll create these simply by inserting '.' characters in the #dots div. We position the div at the bottom centre of the page, use z-index to raise it above the other page elements, set various dimensions, and give it a mid grey font colour.
  • Media queries to handle different iOS devices and orientations: Although the earlier .tweetPageContent ruleset is flexible enough to work OK across different screen sizes — even desktop browsers — we want everything to look just right on an iPhone, in both portrait and landscape orientations. In addition, there are some auto-hiding address bar quirks when running the app in iPhone's Safari browser — as opposed to running full-screen from the home screen — so we need to deal with those also. While we're at it, we might as well make the app look good on iPads too. Fortunately, CSS3 media queries make it easy to do all these things. The CSS contains six media queries, as follows:

    • @media only screen and (max-device-width: 480px) and (orientation: portrait) { ... }
      This media query targets iPhones running the app from the home screen in portrait orientation. It adjusts the height of the .tweetPageContent div to sit perfectly within the phone's screen.
    • @media only screen and (max-device-width: 480px) and (orientation: landscape) { ... }
      This media query is triggered for iPhones running the app from the home screen in landscape orientation. It adjusts the profile image size, tweet box and tweet font size to fit the available space.
    • @media only screen and (max-height: 360px) and (orientation: portrait) { ... }
      This query specifically targets iPhones running the app in Mobile Safari in portrait orientation, by looking at the available viewport height. Due to the interaction between jQuery Mobile and Safari's auto-hiding URL bar, we need to tweak the app's page layout to reduce the tweet page's height and adjust the vertical positions of the dots and pagination buttons. We also reduce the font size to squeeze the page content into the new smaller layout.
    • @media only screen and (max-height: 260px) and (orientation: landscape) { ... }
      This does much the same as the previous query, but it reduces the size of the page content area even further to fit into the small space available on Mobile Safari in landscape orientation.
    • @media only screen and (device-width: 768px) and (orientation: portrait) { ... }
      This query targets iPad devices in portrait orientation. It simply adjusts the content height and font size to sit nicely on an iPad screen.
    • @media only screen and (device-width: 768px) and (orientation: landscape) { ... }
      This does the same as the previous query, but for portrait orientation.
    The figures below show how these media queries help the app to accommodate different iOS devices and orientations.

    Find out more about media queries in Responsive Web Design Demystified.

  • jQuery Mobile swatches: The last section of the stylesheet deals with jQuery Mobile swatches, which let you customize the look of your app by tweaking colours, fonts and other elements. The first ruleset adjusts the page body style for the "a" swatch so that it has a black background (the default is a dark grey gradient). The remaining rulesets create a new white-on-red "f" swatch that we'll use later to style the "delete" buttons in the app.
Screenshot of the app running on iPhone, portrait
Using media queries to adapt to different devices, screen sizes and orientations. Here's the app running on iPhone in standalone mode (left) and in Safari (right), in portrait orientation.
Screenshot of the app running on iPhone, portrait
And here's the app running standalone on iPhone in landscape orientation (left), and on iPad in portrait (right).

Step 4: The closeFriends() function

Now that you've created the markup and CSS for the app, you're ready to start writing the JavaScript code. Create a file called CloseFriends.js inside your CloseFriends folder. Start by adding the following function to the file:

/**
 * The CloseFriends app code. It's wrapped in a function
 * to provide namespacing.
 */

function closeFriends() {

  var PROFILE_IMAGE_CACHE_TIMEOUT = 60 * 60 * 1000; // in milliseconds
  var friends = ['ElatedDotCom'];
  var currentFriend = friends[0];

  init();
  
}

// All set! Run the app.
$( closeFriends );

This is the main function that will contain all of our app code. We start by adding a constant, PROFILE_IMAGE_CACHE_TIMEOUT, to specify how long to cache the profile image URLs (1 hour by default). We also create an array, friends, to hold the user's Twitter friends list, and add a default friend to the array, 'ElatedDotCom'. Next we create a variable, currentFriend, to hold the array index of the tweet page that the user is currently viewing. We set this to 0 initially, which refers to the ElatedDotCom friend.

Now that the closeFriends() function has set up these values, it calls the init() function to start the app itself. You'll create this function next.

Once we've defined the closeFriends() function, we tell jQuery to run closeFriends() when the DOM is ready by calling $(closeFriends).

Step 5: The init() function

You've now created your bare-bones closeFriends() function. It's time to add the rest of the app's code to the function.

Start by defining the init() function. Add this inside the closeFriends() function, right after the call to init(), like this:

function closeFriends() {

  var PROFILE_IMAGE_CACHE_TIMEOUT = 60 * 60 * 1000; // in milliseconds
  var friends = ['ElatedDotCom','simonmeek','stephenfry'];
  var currentFriend = friends[0];

  init();


  /**
   * Initializes the app. Called when the document is ready.
   */

  function init() {

    // Improve scrolling responsiveness
    $.mobile.touchOverflowEnabled = true;

    // If the user already has a friends list in local storage,
    // grab it, as well as the current friend they were viewing previously
    if ( localStorage['friends'] && JSON.parse( localStorage['friends'] ).length > 0 ) {
      friends = JSON.parse( localStorage['friends'] );
      if ( localStorage['currentFriend'] ) currentFriend = localStorage['currentFriend'];
    }

    // Store the friends list and current friend in local storage
    localStorage['friends'] = JSON.stringify( friends );
    localStorage['currentFriend'] = currentFriend;

    // Add some click handlers to various buttons
    $('.manageFriendsButton').live( 'click', manageFriends );
    $('#addFriendConfirm').live( 'click', addFriend );
    $('#done').live( 'click', showTweetPages );

    // Clear the "screen name" field in the "Add Friend" dialog when it opens
    $('#addFriendDialog').live( 'pagebeforeshow', function() {
      $('#screenName').val('');
    } );

    // Create and show the tweet pages
    showTweetPages();
  }

}

Why do we put all our app's code inside the closeFriends() function, rather than in the top level of the CloseFriends.js file? The answer is that it lets us create a namespace for all the variable and function names in our app, to keep them separate from other JavaScript code that might be included in the page. For example, if we had created the init() function outside the closeFriends() function then its name might have clashed with another function called init(), created by a different script in the page.

We can do this because JavaScript lets us create nested functions — a great feature of the language.

Let's look at the sections of code inside the init() function:

  • Improve scrolling responsiveness
    The function starts by setting the jQuery Mobile option $.mobile.touchOverflowEnabled to true. This is a new feature as of jQuery Mobile Beta 3 that allows smoother page transitions for mobile browsers that support the overflow and overflow-scrolling: touch CSS properties (at the time of writing, only iOS 5 supports these properties). Setting this value to true helps to speed up the swipe and drag actions in the app.
  • If the user already has a friends list in local storage, grab it, as well as the current friend they were viewing previously
    In order to make our app settings persist even when the app is closed, we need to store them in the browser. We do this using HTML5's local storage feature. This is easy to use: the browser provides a localStorage array in which we can store the values that we want to persist. The only caveat is that the array can only hold string values, so to store our friends array we need to use the JavaScript JSON functions stringify() and parse() to convert the array to a string when storing it, and back to an array when retrieving it.

    So the code first looks to see if the friends element exists in the localStorage array. If it does, the code turns the element's string value into an array by calling JSON.parse(). If the resulting array has at least one element, then we retrieve the array's values and store them in our local friends array. At the same time, if the currentFriend element exists in local storage then we retrieve that value too, and store it in currentFriend.
  • Store the friends list and current friend in local storage
    If this is the first time the user has run the app, there won't be any values in local storage. Therefore the function now stores the current values of friends and currentFriend, so they're available next time the app starts. Since friends is an array, the function calls JSON.stringify() to turn it into a string before storing it.
  • Add some click handlers to various buttons
    Next, the function uses the jQuery live() method to add various click handlers to buttons that will be created in the page. The "Manage Friends" button will trigger the manageFriends() function when clicked; the "Add Friend" dialog button will trigger addFriend(); and the "Done" button in the Manage Friends page will trigger showTweetPages(). You'll create all these functions in a moment.
  • Clear the "screen name" field in the "Add Friend" dialog when it opens
    Since jQuery Mobile keeps pages cached in the DOM, the "screen name" field in the "Add Friend" dialog retains its value, even after the dialog is closed and reopened. We want the field to be empty each time the dialog opens, so that the user can add a new friend. So we create a simple event handler function for the dialog page's pagebeforeshow event that clears the field's value. pagebeforeshow is a jQuery Mobile event that is triggered just before the page — or, in this case, the dialog — is displayed.

  • Create and show the tweet pages
    Finally, the init() function calls showTweetPages() to start creating and showing the friends' tweet pages. You'll create this function in the next step.

Step 6: The showTweetPages() function

The next function to create is showTweetPages(). Add this code inside the closeFriends() function, right after the init() function you created in the previous step:

  /**
   * Prepares the DOM, adds the tweet pages, and displays the
   * current friend's tweet page.
   */

  function showTweetPages() {

    // Don't do anything if there are no friends
    if ( friends.length == 0 ) return;

    // Remove all old tweet pages
    $('.tweetPage').remove();

    // Add the new tweet pages
    addPages();

    // Switch to the current friend's tweet page
    $.mobile.changePage( "#" + currentFriend, { transition: 'flip', reverse: true } );
  }

This function is very simple. First, if there are no friends to display for some reason, the function exits immediately. Assuming there are friends in the list, the function first removes all previous tweet pages from the DOM by selecting all elements with the CSS class tweetPage, then calling the jQuery remove() method on the resulting set. Then it calls addPages() to add the new tweet pages to the DOM. You'll create this function in the next step.

Finally, the function calls the jQuery Mobile changePage() method to display the current friend's tweet page; this page's ID will be set to the screen name of the friend, so we locate it using '#' + currentFriend. We display the page using the reverse 'flip' transition.

Although jQuery Mobile automatically intercepts links and form submissions to create smooth transitions between mobile pages, changePage() is a handy way to trigger a page change and transition using JavaScript when required. Find out more about changePage() in my book or in the online documentation.

Step 7: The addPages() function

The addPages() function's job is to create all the "current tweet" pages for the friends in the friends array. Here's the code — add it inside the closeFriends() function, after the showTweetPages() function:

  /**
   * Adds all the tweet pages to the DOM, populating them as it goes.
   * Called from showTweetPages().
   */

  function addPages() {

    // Store a reference to the previously added page
    var prevPage = null;

    // Create each page's markup
    for ( var i=0; i<friends.length; i++ ) addPage( friends[i] );

    // Render the pages
    for ( var i=0; i<friends.length; i++ ) $("#"+friends[i]).page();

    // Add a handler that runs whenever the tweet page is shown...
    $(".tweetPage").bind( "pageshow", function() {

      // Update the dots at the bottom of the screen to
      // highlight the new current page
      
      $("#dots").empty();

      for ( var i=0; i<friends.length; i++ ) {
        if ( friends[i] == this.id ) {
          $("#dots").append( '<span class="highlight">.</span>' );
        } else {
          $("#dots").append( '.' );
        }
      }

      // Store the page's associated screen name in currentFriend
      currentFriend = localStorage['currentFriend'] = $.mobile.activePage.attr('id');
    } );

    // Hide the dots if there's only one page
    if ( friends.length < 2 ) {
      $("#dots").hide();
    } else {
      $("#dots").show();
    }

  }

Let's look at each section in the addPages() function:

  • Store a reference to the previously added page
    We set up a variable called prevPage to record the ID of the previously-added page. We'll use this to add the "Next" link to the previous page's markup. Since we haven't created any pages yet, we set prevPage to null.
  • Create each page's markup
    Next the function loops through each element in the friends array, calling the addPage() function with the element's value, which is the friend's screen name. addPage() creates the markup for a single tweet page; you'll create this function in the next step.
  • Render the pages
    Now that each page's markup has been created and added to the DOM, we need to make sure that jQuery Mobile goes through the markup and enhances the page, adding appropriate CSS classes and creating the necessary widgets. Normally this happens automatically when the HTML page first loads, but since we've added our own markup using JavaScript, we need to tell jQuery Mobile to enhance it. To do this, we call jQuery Mobile's page() method on each friend page in turn.
  • Add a handler that runs whenever the tweet page is shown
    Next the function binds a handler function to each tweet page's pageshow event. This is a jQuery Mobile event that triggers after a page has been transitioned to.

    The handler starts by clearing out any previous dots from the #dots div at the bottom of the tweet page, then adding the dots back into the div, highlighting the current page's dot by wrapping a span with a class of highlight around it. (This class is given a white colour in the CSS.)

    Once it's done that, the handler updates the currentFriend variable — and its counterpart in local storage — with the ID of the new current tweet page. It retrieves the current tweet page from the jQuery Mobile activePage property, which contains the DOM element of the currently shown mobile page.
  • Hide the dots if there's only one page
    Finally, the addPages() function checks how many friends are currently in the list. If there's only one friend, the function calls the jQuery hide() method on the #dots div to hide the dots. Otherwise, it calls the show() method to display the dots.

Step 8: The addPage() function

You're now ready to create the addPage() function to add an individual tweet page to the DOM. Put this function inside the addPages() function you created in Step 7, after the rest of the addPages() code:

    /**
     * Adds a single tweet page to the DOM, and populates it with
     * the info from Twitter.
     * Called from its parent function, addPages().
     */

    function addPage( pageId ) {

      // Don't add the page if it's already in the DOM
      if ( document.getElementById( pageId ) ) return;

      // Create the basic markup for the new page
      var pageMarkup = '<div class="tweetPage" data-role="page" data-theme="a" id="' + pageId + '" data-url="' + pageId + '" data-title="CloseFriends">';
      pageMarkup += '<div class="gloss"></div>';
      pageMarkup += '<div class="tweetPageContent">';
      pageMarkup += '<a href="http://twitter.com/' + pageId + '"><img class="profileImage" src="loading.png" alt="Profile image"></a>';
      pageMarkup += '<h1>@' + pageId + '</h1>';
      pageMarkup += '<div class="tweetTime"></div>';
      pageMarkup += '<a href="#" class="manageFriendsButton" data-role="button" data-icon="info" data-iconpos="notext" data-transition="flip">Manage Friends</a>';
      pageMarkup += '</div>';
      pageMarkup += '<ul data-role="pagination">';
      if ( prevPage ) pageMarkup += '<li class="ui-pagination-prev"><a href="#' + prevPage + '">Prev</a></li>';
      pageMarkup += '</ul>';
      pageMarkup += '</div>';

      // Add the page to the DOM
      $('body').append( $(pageMarkup) );

      // If this isn't the first page we've added,
      // add a  "Next" link to the previous page's nav
      if ( prevPage ) {
        var nextLinkMarkup = '<li class="ui-pagination-next"><a href="#' + pageId + '">Next</a></li>';
        $('#' + prevPage + ' ul:jqmData(role="pagination")').append( nextLinkMarkup );
      }

      // Is there a fresh copy of the friend's profile image URL in local storage?
      if ( !localStorage['profileImage_'+pageId] || ( new Date().getTime() - localStorage['profileImage_'+pageId+'_timestamp'] > PROFILE_IMAGE_CACHE_TIMEOUT ) ) {

        // No: fetch the image URL and add it to the page when fetched.
        // Also store the (possibly changed) image URL and fetch time in local storage.
        $.ajax( {
          url: "http://api.twitter.com/1/users/show.json?screen_name=" + pageId,
          success: function(data) { 
            var profile_image_url = data['profile_image_url'].replace( /_normal/, "" );
            $('#'+pageId+' .tweetPageContent .profileImage').attr( 'src', profile_image_url );
            localStorage['profileImage_'+pageId] = profile_image_url;
            localStorage['profileImage_'+pageId+'_timestamp'] = new Date().getTime();
          },
          dataType: "jsonp"
        } );

      } else {
        // Yes: add the cached image URL to the page
        $('#'+pageId+' .tweetPageContent .profileImage').attr( 'src', localStorage['profileImage_'+pageId] );
      }

      // Get the friend's latest tweet
      
      $.ajax( {
        url: "http://search.twitter.com/search.json?q=from%3A" + pageId,
        success: function(data) { 

          // Add the tweet text to the page
          var latestTweet = data["results"].length ? data["results"][0]["text"] : "No recent tweets found.";
          var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
          latestTweet = latestTweet.replace( exp,"<a href='$1'>$1</a>" ); 
          exp = /\@(\w+)/ig;
          latestTweet = latestTweet.replace( exp,"<a href='http://twitter.com/$1'>@$1</a>" ); 
          $('#'+pageId+' .tweetPageContent').append( '<div class="recentTweet"><blockquote>' + latestTweet + '</div></blockquote></div>' );

          // Add the tweet timestamp to the page
          var tweetTime = new Date( data["results"][0]["created_at"] ).getTime();
          var currentTime = new Date().getTime();
          var tweetTimeDiff = "";
          var tweetTimeString = "";

          if ( currentTime - tweetTime < 60 * 1000 ) {
            tweetTimeDiff = Math.floor( ( currentTime - tweetTime ) / 1000 ) ;
            tweetTimeString = tweetTimeDiff + " second" + ( tweetTimeDiff == 1 ? "" : "s" ) + " ago";
          } else if ( currentTime - tweetTime < 60 * 60 * 1000 ) {
            tweetTimeDiff =  Math.floor( ( currentTime - tweetTime ) / ( 60 * 1000 ) );
            tweetTimeString = tweetTimeDiff + " minute" + ( tweetTimeDiff == 1 ? "" : "s" ) + " ago";
          } else if ( currentTime - tweetTime < 60 * 60 * 24 * 1000 ) {
            tweetTimeDiff =  Math.floor( ( currentTime - tweetTime ) / ( 60 * 60 * 1000 ) );
            tweetTimeString = tweetTimeDiff + " hour" + ( tweetTimeDiff == 1 ? "" : "s" ) + " ago";
          } else {
            tweetTimeDiff =   Math.floor( ( currentTime - tweetTime ) / ( 60 * 60 * 24 * 1000 ) );
            tweetTimeString = tweetTimeDiff + " day" + ( tweetTimeDiff == 1 ? "" : "s" ) + " ago";
          }
            
          $('#'+pageId+' .tweetTime').html( tweetTimeString );
        },
        dataType: "jsonp"
      } );

      // All done - this page now becomes the new "previous page"
      prevPage = pageId;
    }

We've put addPages() inside addPage() mainly so that it has access to addPage()'s prevPage local variable. A function nested inside a parent function has access to the scope of its parent. This approach also nicely encapsulates addPage() inside addPages(), since only addPages() needs to know about addPage().

addPage() is quite a meaty function. Let's break it down step by step to see how it works:

  • Don't add the page if it's already in the DOM
    addPage() accepts a single parameter: pageId, the ID (screen name) of the page to add. The first thing the function does is use getElementById() to check if the tweet page already exists in the DOM. If it does then there's nothing to do, so the function exits.
  • Create the basic markup for the new page
    Next the function creates a pageMarkup variable that stores the basic markup for the tweet page. This consists of:

    • A jQuery Mobile data-role="page" wrapper div with a class of tweetPage and an ID matching the screen name. We also give the div the data-theme="a" attribute to set the page's swatch to "a"; a data-url attribute with the page ID as its value; and a data-title attribute with the value "CloseFriends", which ensures that "CloseFriends" always appears in the browser's title bar.

      data-url lets jQuery Mobile know the unique URL of the page. Normally jQuery Mobile sets this attribute itself, but since we're creating the markup dynamically, we need to add this attribute ourselves. Find out more about data-url in Appendix A of my book, or in the online docs.

      The wrapper div contains:
    • A div with with a class of gloss to create the gloss effect.
    • A .tweetPageContent div containing the actual tweet page content: the profile image (linked to the friend's Twitter URL); the screen name; the tweet timestamp; and the "Manage Friends" button. (Noticeably absent is the tweet itself, which we'll pull from the Twitter API asynchronously in a moment.)

      The profile image's src attribute is initially set to loading.png, a placeholder image. Once the profile image has been loaded from Twitter, it will replace the placeholder. You'll create this placeholder image later in the tutorial.

    • An unordered list with the data-role="pagination" attribute. Within this list we create an li with a class of ui-pagination-prev that links to the previous page (if any). This attribute and class are picked up automatically by the pagination plugin and used to create the pagination buttons, as well as link the tweet pages together.
  • Add the page to the DOM
    Now that we've created our basic page markup, we add the page to the DOM by wrapping the markup in the jQuery function, $(), then calling append() on the page's body element to append the page div to the body.
  • If this isn't the first page we've added, add a "Next" link to the previous page's nav
    If prevPage isn't null then we've added a previous page to the DOM already. In this case, the function creates markup for the "Next" navigation link that points to the current page, and stores the string in nextLinkMarkup. Then it selects the data-role="pagination" ul element in the previous page, and calls append() to add the newly-created li element to it.
  • Is there a fresh copy of the friend's profile image URL in local storage?
    Now the function checks the local storage for an element with the index 'profileImage_'+pageId, which stores the cached profile image URL. If it finds one, it checks the time stored in the corresponding 'profileImage_'+pageId+'_timestamp' element, and compares it to the current time. If it's older than PROFILE_IMAGE_CACHE_TIMEOUT milliseconds then the URL is no longer considered fresh.

    • No: fetch the image URL and add it to the page when fetched. Also store the (possibly changed) image URL and fetch time in local storage.
      If the image URL didn't exist in local storage, or wasn't fresh, then the function fetches the image URL using the Twitter REST API. It uses jQuery's ajax() method to make an Ajax call to http://api.twitter.com/1/users/show.json?screen_name=pageId, where pageId is the page ID / screen name of the user we're creating the tweet page for.

      Ajax calls are asynchronous, which means we need to supply a "success" function that will be called as soon as the data is returned from the Twitter server. The success function extracts the profile image URL from the returned data, and removes the "_normal" part of the URL string — this converts the "normal" size image URL into the URL for the "large" profile image. Then the success function sets the profile image's src attribute in the tweet page to the new URL, thereby placing the image in the page. It then stores the new URL and timestamp in local storage so that it can be retrieved next time.

      We cache the profile image URLs like this because the Twitter API has a rate limit on calls to the REST API. Currently this is around 150 requests per hour. If we pulled the user data from the REST API each time we built the tweet pages then we would quickly exceed this limit.

      Notice that we also set the dataType option of the Ajax call to "jsonp". This makes jQuery use a JSONP request instead of a standard JSON request. A JSONP request is needed in this case because we're making an Ajax call to a domain (dev.twitter.com) that is different from our web page's domain. With regular Ajax requests, the current page and the requested resource both need to be in the same domain.
    • Yes: add the cached image URL to the page
      If the image URL was already in our local storage cache, and it's still fresh, then we simply set the profile image's src attribute to this image URL, without requesting the URL from Twitter.
  • Get the friend's latest tweet
    The next chunk of code makes another Ajax request — this time using Twitter's Search API — to retrieve the friend's latest tweet. To do this, it retrieves the URL http://search.twitter.com/search.json?q=from:pageId, where pageId is the friend's screen name. As with the profile image Ajax call above, the call is asynchronous, so we provide a "success" function to handle the response. This function does two things:

    • Add the tweet text to the page
      The "success" function starts by retrieving the first entry in the response data's results object, and storing this entry's text property in a latestTweet variable. (If the results object is empty then latestTweet is set to "No recent tweets found." instead.) Then the function passes the text through two regular expressions to convert URLs and Twitter screen names in the text to tappable links, before appending the text to the tweet page, wrapped in <blockquote> and <div class="recentTweet"> elements.
    • Add the tweet timestamp to the page
      Finally, the "success" function retrieves the tweet timestamp from the entry's created_at property, and subtracts it from the current time. It then converts this time difference to an appropriate text string — for example, "6 seconds ago" or "45 minutes ago" — and inserts the resulting tweetTimeString variable into the page's .tweetTime div.

    As before, we set the Ajax request's dataType option to "jsonp" to send a JSONP request to Twitter, rather than a standard JSON request.

  • All done - this page now becomes the new "previous page"
    We've now finished adding the tweet page. The last line of our addPage() function sets the prevPage variable to the page's ID, so that the next call to addPage() can update this page's "Next" navigation link.

Step 9: The manageFriends() function

manageFriends() is called when the user taps the "Manage Friends" (info) button in the bottom right corner of a tweet page. It creates the list view containing the Twitter friends that the user has added in the app, and displays the "Manage Friends" page, including the list view.

Screenshot of the Manage Friends page
The "Manage Friends" page lets the user view, add and delete Twitter friends.

Add this function inside your closeFriends() function, after the addPages() function:

  /**
   * Displays the "Manage Friends" page when the user clicks the
   * "info" button.
   */

  function manageFriends() {

    // Get the "friends" list view and empty it
    var friendsList = $('#friendsList');
    friendsList.empty();

    // Add the friends to the list view
    for ( var i=0; i<friends.length; i++ ) {
      var li = $( '<li><a href="#" class="view">' + friends[i] + '</a><a href="#" class="delete">Delete</a></li>' );
      friendsList.append( li );
      li.find('a[class="view"]').jqmData( 'screenName', friends[i] ).click( viewFriend );
      li.find('.delete').jqmData( 'screenName', friends[i] ).click( deleteFriend );
    }

    // Display the Manage Friends page and update the list view widget
    $.mobile.changePage( '#manageFriends', { transition: "flip" } );
    friendsList.listview('refresh');
    $('#dots').empty();
  }

manageFriends() is fairly straightforward. Here's how it works:

  • Get the "friends" list view and empty it
    First the function retrieves the list view's ul element, which has an id of friendsList, and stores it in a variable called friendsList. It then removes all the child li elements from the list by calling jQuery's empty() method.
  • Add the friends to the list view
    Next the function loops through the friends in the friends array. For each friend, it creates an li element containing the screen name, as well as two links: one with a class of view, which the user can tap to view the friend's tweet page, and the other with a class of delete, which the user can tap to remove the friend from the list. It then adds this li element to the friendsList ul element. Finally, the block of code uses the jQuery find() method to locate the two links. For each link, it uses the jqmData() method to attach a screenName data item to the link, containing the friend's screen name, then adds a click handler to the link to call the viewFriend() or deleteFriend() method as apprpriate.

    jqmData() is a special jQuery Mobile version of jQuery's data() method. It works much like data(), except it also allows for any jQuery Mobile namespace that may be in use.

  • Display the Manage Friends page and update the list view widget
    Finally, the manageFriends() function calls the jQuery Mobile changePage() method to reveal the "Manage Friends" page using the "flip" transition; calls the refresh method of the jQuery Mobile listview plugin on the list view; and removes the dots from the bottom of the page, since these should only appear on the tweet pages.

    Why the call to listview('refresh')? Normally, jQuery Mobile turns any ul element with a data-role="listview" attribtue into a list view automatically when the page loads. However, since we've updated the items inside the list view using JavaScript after the page has loaded, we need to call the refresh method so that jQuery Mobile can enhance and style the new list items.

Step 10: The addFriend() function

addFriend() is responsible for adding a new friend to the user's friends list. It's called when the user taps the "Add Friend" button after entering the friend's Twitter screen name in the "Add Friend" dialog (see figure below).

Screenshot of the Add Friend dialog
The "Add Friend" dialog. addFriend() runs when the user enters a friend's screen name and taps the "Add Friend" button.

Here's the code — as usual, place it in the closeFriends() function at the end:

  /**
   * Adds a new friend to the user's friends list.
   * Called when the user taps the "Add Friend" button
   * in the "Add Friend" dialog.
   */

  function addFriend() {

    // Get the screen name that the user entered
    var screenName = $("#screenName").val().replace(/\W/ig, "");

    // Does the friend already exist in the list?
    if ( friends.indexOf( screenName ) == -1 ) {

      // No: add the friend to the list
      friends.push( screenName );

      // Close the "Add Friend" dialog
      $('#addFriendDialog').dialog('close');

      // Add an item for the friend in the list view and update the widget
      var li = $( '<li><a href="#" class="view">' + screenName + '</a><a href="#" class="delete">Delete</a></li>' );
      var friendsList = $('#friendsList');
      friendsList.append( li );
      li.find('a[class="view"]').jqmData('screenName', screenName ).click( viewFriend );
      li.find('.delete').jqmData( 'screenName', screenName ).click( deleteFriend );
      friendsList.listview('refresh');

      // Make sure the "Done" button isn't greyed out
      $('#done').css('opacity',1);

      // Store the updated friends list in local storage
      localStorage['friends'] = JSON.stringify( friends );
    } else {

      // Yes: warn the user and do nothing
      alert( "You've already added that friend to your list." );
    }
  }

Here's how this function works:

  • Get the screen name that the user entered
    First, the function retrieves the screen name of the friend to add from the #screenName field in the dialog. It passes it through a regular expression to remove any non-word characters that the user may have entered (such as an '@' symbol), then stores the result in a screenName variable.
  • Does the friend already exist in the list?
    Next the function uses the JavaScript indexOf() function to see if the screen name already exists in the friends array. indexOf() returns the array index if the screen name was found, or -1 if it wasn't.

    • No: add the friend to the list
      Assuming the friend isn't already in the array, the function first adds the friend to the array using the push() method.
    • Close the "Add Friend" dialog
      Next the function closes the dialog by calling the close method of the jQuery Mobile dialog plugin. This automatically returns the user to the "Manage Friends" page that opened the dialog.
    • Add an item for the friend in the list view and update the widget
      This code is similar to the code in manageFriends(). It adds an li item to the list view, containing the friend's screen name and the links to view and delete the friend, then refreshes the list view to update the widget.
    • Make sure the "Done" button isn't greyed out
      Since the "Done" button in the header may have been greyed out by the deleteFriend() function (see Step 11), we make sure it isn't greyed out any more by setting its opacity to 1.
    • Store the updated friends list in local storage
      The list of friends has changed, so this line of code stores the new list in local storage so it's available to the app after restarting.
    • Yes: warn the user and do nothing
      If the friend did already exist then we display an alert box to the user to warn them.

Step 11: The deleteFriend() function

deleteFriend() removes a friend from the user's friends list. It's called when the user taps a Delete button in the Manage Friends page.

Save the following code inside the closeFriends() function, after the addFriend() function:

  /**
   * Deletes a friend from the user's friends list.
   * Called when the user taps a "Delete" button
   * in the "friends" list view.
   */

  function deleteFriend() {

    // Get the delete button that was tapped
    var deleteButton = $(this);

    // Retrieve the array index of the friend to delete
    var friendIndex = friends.indexOf( deleteButton.jqmData("screenName") );

    // Remove the friend from the array
    friends.splice( friendIndex, 1 );

    // Remove the item from the list view and update the widget
    deleteButton.parent().remove();
    $('#friendsList').listview('refresh');

    // If there are no more friends in the list, grey out the Done button
    if ( friends.length == 0 ) $('#done').css('opacity',.5);

    // Store the updated friends list in local storage
    localStorage["friends"] = JSON.stringify( friends );

    // Reset the current friend to the first friend in the list
    currentFriend = localStorage["currentFriend"] = friends[0];
  }

deleteFriend() works like this:

  • Get the delete button that was tapped
    The function starts by retrieving the button element that was clicked from the this variable, turning it into a jQuery object for easier manipulation, and storing the object in deleteButton.

    When an event occurs on a DOM element — such as a click on a button — JavaScript usually makes the element available to the event handler in the this variable.

  • Retrieve the array index of the friend to delete
    Next, the function retrieves the screen name associated with the Delete button using the jqmData() method, then searches the friends array for the screen name, storing the found element's index in friendIndex.
  • Remove the friend from the array
    Now that we have the index of the friend to remove, it's easy to remove it from the array using JavaScript's splice() method. This takes the index to start removing elements from, and the number of elements to remove (1 in this case).
  • Remove the item from the list view and update the widget
    Next the function finds the parent li item that contains the button, removes it from the list, and calls the list view's "refresh" method to update the list view.
  • If there are no more friends in the list, grey out the Done button
    The friends list needs to have at least one friend in it for the app to work properly, so if the user has deleted all their friends, we grey out the Done button by setting its opacity to 0.5. (If the user then attempts to tap the Done button, the first line of code in showTweetPages() prevents the user returning to the tweet pages.)
  • Store the updated friends list in local storage
    As with addFriend(), we need to update the copy of the friends array in local storage, since the array has been altered.
  • Reset the current friend to the first friend in the list
    Since the current friend may have been deleted, we change the current friend to be the first friend in the friends list, and store the new value in local storage.

Step 12: The viewFriend() function

The viewFriend() function is very short. It simply sets the current friend to the screenName value stored in the list item that was tapped, then calls showTweetPages() to display the friend's tweet page.

Here's the code — place it inside closeFriends(), after the deleteFriend() function:

  /**
   * Displays the selected friend's tweet page.
   * Called when the user taps a friend item
   * in the "friends" list view.
   */

  function viewFriend() {
    currentFriend = $(this).jqmData('screenName');
    showTweetPages();
  }

Step 13: Handling orientation changes

We need to make one more small tweak to the app. When running the app inside Safari on the iPhone, rotating the device from portrait to landscape doesn't always re-render the page correctly. Therefore, we need to write a short handler function for the jQuery Mobile orientationchange event. Add this code inside the closeFriends() function, after viewFriend():

  /**
   * If the user rotates their device, and we're running
   * the app inside Mobile Safari, then reload the page to
   * avoid rendering glitches.
   */

  $(window).orientationchange( function( e ) {
    if ( !window.navigator.standalone ) window.location.reload( false );
  } ); 

This function is triggered whenever the user rotates their device to a different orientation. It checks the window.navigator.standalone property, which iOS devices set to true if the app is running from the home screen (outside Safari). If this value is false then the app is running in Safari, so the function reloads the page to re-render it.

Step 14: Improving the pagination plugin

The pagination plugin works pretty well, but it does have one drawback. In order to move between pages by swiping, you have to swipe more than 50% of the way from left to right (or right to left). It would be nice if you could change pages with quick, short swipes, as well as dragging the pages slowly if you prefer.

To achieve this, we can modify the code that decides whether to move to the previous or next page after the swipe has completed. If the swipe happened quickly — in under, say, 600 milliseconds — then we move to the new page, even if the swipe wasn't more than 50% of the way across the screen.

First, in jquery.mobile.pagination.js, add a line of code to the touchstart handler to record the time that the drag started:

              if( !dragStart ){
                dragStart = true;
                $page.trigger( "dragstart.pagination" );
                this.dragStartTime = new Date().getTime();
              }

Then change this chunk of code:

          //bind touch handlers
          $page
            .bind( "gesturestart.pagination touchend.pagination", function(){
              $page.unbind( ".pagination" );
            })
            .bind( "touchmove.pagination", moveHandler )
            .one( "touchend", function( e ){            
              var pOffset = $page.offset().left,
                absOS = Math.abs( pOffset ),
                toGo  = $page.width() - absOS;
      
              snapTo( absOS > 150 ? pOffset + ( pOffset > 0 ? toGo : -toGo ) : 0, absOS < 10 );
            });

...to this:

          //bind touch handlers
          $page
            .bind( "gesturestart.pagination touchend.pagination", function(){
              $page.unbind( ".pagination" );
            })
            .bind( "touchmove.pagination", moveHandler )
            .one( "touchend", function( e ){            
              var pOffset = $page.offset().left,
                absOS = Math.abs( pOffset ),
                toGo  = $page.width() - absOS;

              var newOffset = absOS > 150 ? pOffset + ( pOffset > 0 ? toGo : -toGo ) : 0;

              // If the swipe was quick then always snap to the previous/next page,
              // even if the swipe was over a short distance
              if ( new Date().getTime() - this.dragStartTime &lt; 600 ) {
                newOffset = absOS > 10 ? pOffset + ( pOffset > 0 ? toGo : -toGo ) : 0;
              }
      
              snapTo( newOffset, absOS &lt; 10 );
            });

Step 15: Creating the images

We've now built our app. Let's add a bit of polish by creating a few images:

  • loading.png: The profile image placeholder in tweet pages
  • quoteMarks.png: The "quote marks" image that sits behind the tweet text in tweet pages
  • icon.png: An icon for the app when it's added to the iOS home screen

loading.png should be 100 x 100 pixels, and quoteMarks.png needs to be about 200 x 130 pixels. icon.png should be 114 x 114 pixels, which is the required icon size for iOS retina displays. (iOS automatically scales the icon down for lower-resolution screens.)

You can create the images yourself in Photoshop, or grab the ones I made:

loading.png quoteMarks.png icon.png



Save the three image files as transparent PNG-24 images in your CloseFriends folder.

Step 16: Try it out!

Your app is all finished, so you're now ready to try it out. There are a few different ways you can run the app:

  • In a desktop browser: You can just double-click your index.html file to open the app in a desktop browser. This should work OK, at least in WebKit browsers, although you won't be able to swipe due to the lack of touch events. (You can still click below the tweet pages, or use the left and right arrow keys, to move between pages.) It also won't look that great due to the unpredictable dimensions of your browser window.

  • In an iOS device: You'll need to upload your CloseFriends folder to a publicly-accessible website. Then browse to the app's URL on your iPhone or iPad. For best results, tap the icon at the bottom of the Safari window and choose "Add to Home Screen" (see figure below), then run the app full-screen from your home screen.
  • Using the iOS Simulator: This option is good if you don't have a publicly-accessible site to upload your app to. The iOS Simulator comes with Xcode, and lets you simulate iPhone and iPad devices in any resolution and orientation. It's also faster than using a real iOS device. Just fire up the simulator, then drag your app's index.html file on top of the simulator to open in Safari. Then add the app to your home screen as above.

If you're in a hurry then you can, of course, try out my copy of the app hosted on the Elated server. Just open this article in Mobile Safari on your iPhone, iPad or iOS Simulator; tap the button below to open the app; then add it to your home screen:

Adding the app to the iOS home screen
To test the app properly on an iOS device or the iOS Simulator, browse to the app's index.html file in Mobile Safari, tap the icon shown above, then tap "Add to Home Screen". Tap the icon on the home screen to run the app.

Once you've launched the app, click the "i" button and start adding some Twitter friends, as shown in the figure below. Drag a tweet page slowly right-to-left to see it move smoothly to the next page. You can also swipe quickly, or tap the bottom left or bottom right areas of the screen, to move quickly between pages.

Testing the app
Testing the app. Try adding some friends (left), then dragging to move between friends' latest tweets (right).

Summary

In this detailed tutorial, you've walked through all the steps needed to create a simple, but fully functional iPhone app, using HTML, CSS, JavaScript, jQuery Mobile, and the pagination plugin. You've:

  1. Downloaded and installed the pagination plugin
  2. Created the index.html file for the app
  3. Built the app's CSS stylesheet
  4. Created the main closeFriends() function
  5. Added the init() function to kick off the app
  6. Written the showTweetPages() function, which is called whenever we need to display the tweet page
  7. Created the addPages() function for adding all the tweet pages to the DOM
  8. Built the addPage() function, which creates and adds a single tweet page
  9. Written the manageFriends() function, which displays the "Manage Friends" page
  10. Created the addFriend() function for adding new friends to the list
  11. Added the deleteFriend() function, which removes a friend from the list
  12. Added the viewFriend() function for viewing an individual friend's latest tweet
  13. Written a handler to re-render the page after orientation changes
  14. Modified the pagination plugin for nicer swiping
  15. Created the app images, and
  16. Tested the final product.

Along the way, you've explored various jQuery Mobile features, including mobile pages, dialogs, buttons and list views, as well as various events, methods and properties of the jQuery Mobile API, such as the pagebeforeshow, pageshow and orientationchange events; the changePage() method; and the activePage property.

Although this app is quite simple, and is designed purely for iOS devices, it gives you a starting point for building more feature-rich, cross-device web apps. For example, it should be possible to adapt the app to work well on Android devices too. (I concentrated on iOS mainly because I don't have an Android device to develop and test with, and the Android emulator is painfully slow!)

I hope you enjoyed reading this tutorial, and that it has inspired you to have a go at creating some mobile web apps using the excellent jQuery Mobile framework. Happy coding!

Master Mobile Web Apps with jQuery Mobile

Find Out More!

Want to get the most from jQuery Mobile? Check out my detailed book, Master Mobile Web Apps with jQuery Mobile. It contains all the info you need to start building great-looking mobile web apps with ease!

You can learn more about the book over on the Elated Store. Have fun!

— Matt

Follow Elated

Related articles

Responses to this article

2 responses (oldest first):

26-Dec-11 06:51
hello there... =)

i'm interested in this topic...hmm, can i possibly create this kind of app? because i have doubts if i can make this thing.... hope you can help me....
31-Jan-12 11:07
This app is just kind of a website, interesting!

Post a response

Want to add a comment, or ask a question about this article? Post a response.

To post responses you need to be a member. Not a member yet? Signing up is free, easy and only takes a minute. Sign up now.

Top of Page