Chapter 6. Programming the Interface

The previous two chapters surveyed the UI standards and guidelines that you need to keep in mind as you design a Web application that works well on iPhone as well as iPod touch. With these design principles in hand, you are ready to apply them as you develop and program your Web app.

To demonstrate how to implement an iPhone interface, I will walk you through a case study application I am calling iRealtor. The concept of iRealtor is to provide a mobile house-hunter application for potential buyers. The current pattern for Internet-based house hunting is to search MLS listings online, print individual listing addresses, get directions, and then travel to these houses. However, with iRealtor, you can do all those tasks on the road with an iPhone-based application. The design goals of iRealtor are to provide a way for users to do the following:

  • Browse and search the MLS listings of a local realtor.

  • Get a map of an individual listing directly from its listing page.

  • Access information about the realtor and easily contact the realtor using iPhone phone or mail services.

  • Browse other helpful tools and tips.

As you look at these overall objectives, an edge-to-edge navigation design looks like an obvious choice given the task-based nature of the application. The realtor information will be relatively static, but the MLS listings need to be database-driven. Therefore, you will take advantage of Ajax to seamlessly integrate listing data into the application.

Here's an overview of the technologies used for iRealtor:

  • XHTML/HTML and CSS for presentation layer

  • JavaScript for client-side logic

  • Ajax for loading data into the application

  • PHP or other server-side technology to serve MLS listing data (not included in case study example)

As I walk you through the application, I'll examine both the custom code I am writing for iRealtor and the underlying styles and code that power it. Therefore, no matter the framework you decide to choose, you at least will have a solid grasp on the key design issues you need to consider.

Top Level of Application

The top level of iRealtor is best presented as an edge-to-edge navigation-style list that contains links to the different parts of the application. When assembled, the design will look like what is shown in Figure 6-1.

iRealtor top-level page

Figure 6-1. iRealtor top-level page

Creating index.html

To build the initial page, start with a basic XHTML document, linking the style sheet and scripting library files being used for this Web app:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>iRealtor</title> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="width=device-width; initial-scale=1.0;
maximum-scale=1.0; user-scalable=0;" />
<style type="text/css" media="screen">@import "./iui/iui.css";</style>
<script type="application/x-javascript" src="./iui/iui.js"></script>
</head>
<body>
</body>
</html>

The apple-mobile-web-app-capable instruction opens the app in full screen when a user launches it from the Home screen. The viewport meta tag tells Safari exactly how to scale the page. It sets a 1.0 scale and does not change the layout on reorientation. It also specifies that the width of the viewport is the size of the device (device-width is a constant).

These properties ensure that iRealtor behaves like a Web application, not a Web page.

Examining Top-Level Styles

The style sheet sets up several top-level styles. The body style sets up the default margin, font-family, and color. It also uses -webkit-user-select and -webkit-text-size-adjust to ensure that iRealtor behaves as an application rather than a Web page. Here's the definition:

body {
    margin: 0;
    font-family: Helvetica;
    background: #FFFFFF;
    color: #000000;
    overflow-x: hidden;
    -webkit-user-select: none;
    -webkit-text-size-adjust: none;
}

For iPhone/iPod touch applications, it is important to assign -webkit-text-size-adjust: none to override the default behavior.

All elements, except for the .toolbar class, are assigned the following properties:

body > *:not(.toolbar) {
    display: none;
    position: absolute;
    margin: 0;
    padding: 0;
    left: 0;
    top: 45px;
    width: 100%;
    min-height: 372px;
}

In landscape mode, the min-height changes for these elements:

body[orient="landscape"] > *:not(.toolbar) {
    min-height: 268px;
}

The orient attribute changes when the orientation of the viewport changes between portrait and landscape. You'll see how this works later in the chapter.

The iUI framework uses a selected attribute to denote the current page of the application. From a code standpoint, the page is typically either a div or a ul list:

body > *[selected="true"] {
    display: block;
}

Links also are assigned the selected attribute:

a[selected], a:active {
    background-color: #194fdb !important;
    background-image: url(listArrowSel.png), url(selection.png) !important;
    background-repeat: no-repeat, repeat-x;
    background-position: right center, left top;
    color: #FFFFFF !important;
}
a[selected="progress"] {
    background-image: url(loading.gif), url(selection.png) !important;
}

The a[selected="progress"] style displays an animated GIF showing the standard iPhone loading animation.

Adding the Top Toolbar

The first UI element to add is the top toolbar, which serves a common UI element throughout the application. To create the toolbar, use a div element, assigning it the toolbar class:

<!--Top iUI toolbar-->
<div class="toolbar">
    <h1 id="pageTitle"></h1>
    <a id="backButton" class="button" href="#"></a>
    <a class="button" href="#searchForm">Search</a>
</div>

The h1 element serves as a placeholder for displaying the active page's title. a backbutton is not shown at the top level of the application, but it is used on subsequent pages to go back to the previous page. The Search button allows access to the search form anywhere within the application. Here are the corresponding style definitions for each of these elements:

body > .toolbar {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    border-bottom: 1px solid #2d3642;
    border-top: 1px solid #6d84a2;
    padding: 10px;
height: 45px;
    background: url(toolbar.png) #6d84a2 repeat-x;
}
.toolbar > h1 {
    position: absolute;
    overflow: hidden;
    left: 50%;
    margin: 1px 0 0 −75px;
    height: 45px;
    font-size: 20px;
    width: 150px;
    font-weight: bold;
    text-shadow: rgba(0, 0, 0, 0.4) 0px −1px 0;
    text-align: center;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #FFFFFF;
}
body[orient="landscape"] > .toolbar > h1 {
    margin-left: −125px;
    width: 250px;
}
.button {
    position: absolute;
    overflow: hidden;
    top: 8px;
    right: 6px;
    margin: 0;
    border-width: 0 5px;
    padding: 0 3px;
    width: auto;
    height: 30px;
    line-height: 30px;
    font-family: inherit;
    font-size: 12px;
    font-weight: bold;
    color: #FFFFFF;
    text-shadow: rgba(0, 0, 0, 0.6) 0px −1px 0;
    text-overflow: ellipsis;
    text-decoration: none;
    white-space: nowrap;
    background: none;
    -webkit-border-image: url(toolButton.png) 0 5 0 5;
}
#backButton {
    display: none;
    left: 6px;
    right: auto;
    padding: 0;
    max-width: 55px;
    border-width: 0 8px 0 14px;
    -webkit-border-image: url(backButton.png) 0 8 0 14;
}

The body > .toolbar class style is set to 45px in height. The .toolbar > h1 header emulates the standard look of an application caption when in portrait mode, and body[orient="landscape"] > .toolbar > h1 updates the position for landscape mode. Notice that the limited width of the iPhone viewport dictates use of overflow:hidden and text-overflow:ellipsis.

Adding a Top-Level Navigation Menu

Once the toolbar is created, you need to create the top-level navigation menu. Under the iUI framework, use a ul list, such as the following:

<ul id="home" title="iRealtor" selected="true">
    <li><a href="featured.html">Featured Listings</a></li>
    <li><a href="listings.html">All Listings</a></li>
    <li><a href="tips.html">Buying & Tips</a></li>
    <li><a href="calc.html">Mortgage Calculator</a></li>
    <li><a href="#meet_our_team">Meet Our Team</a></li>
    <li><a href="contact_us.html">Contact Us</a></li>
    <li><a href="index.html" target="_self">Visit our Web Site</a></li>
</ul>

iUI uses the title attribute to display in the toolbar's h1 header. The selected attribute indicates that this ul element is the active block when the application loads. Each of the menu items is defined as an a link inside of li items. The href attribute can point to another div or ul block inside the same file (called a panel) using an anchor reference (such as #meet_our_team). Alternatively, you can use Ajax to load a block element from an external URL. Table 6-1 displays the four types of links you can work with inside iUI.

Table 6-1. iUI Link Types

Link Type

Description

Syntax

Internal URL

Loads a panel that is defined inside the same HTML page

<a href="#meet_our_team">

Ajax URL

Loads a document fragment via Ajax

<a href="listings.html">

Ajax URL Replace

Loads a document fragment via Ajax, replacing the contents of the calling link

<a href="listings1.html" target="_replace">

External URL

Loads an external Web link

<a href="index.html" target="_self">

The styles for the list items and links are as follows:

body > ul > li {
    position: relative;
    margin: 0;
    border-bottom: 1px solid #E0E0E0;
padding: 8px 0 8px 10px;
    font-size: 20px;
    font-weight: bold;
    list-style: none;
}
body > ul > li > a {
    display: block;
    margin: −8px 0 −8px −10px;
    padding: 8px 32px 8px 10px;
    text-decoration: none;
    color: inherit;
    background: url(listArrow.png) no-repeat right center;
}

Notice that listArrow.png is displayed at the right side of the list item's a link.

Displaying a Panel with an Internal URL

If you are linking to another block section inside the same page, you simply need to add the code. For example, the Meet Our Team item links to the following div:

<div id="meet_our_team" class="panel" title="Meet Our Team">
    <h2>J-Team Realty</h2>
    <fieldset>
    <p class="normalText">Lorem ipsum dolor sit amet, consect etuer adipis
    cing elit. Suspend isse nisl. Vivamus a ligula vel quam tinci dunt posuere.
    Integer venen atis blandit est. Phasel lus ac neque. Quisque at augue.
    Phasellus purus. Sed et risus. Suspe ndisse laoreet consequat metus. Nam
    nec justo vitae tortor fermentum interdum. Aenean vitae quam eu urna
    pharetra ornare.</p>
    <p class="normalText">Pellent esque habitant morbi tristique senectus et
    netus et malesuada fames ac turpis egestas. Aliquam congue. Pel lentesque
    pretium fringilla quam. Integer libero libero, varius ut, faucibus et,
    facilisis vel, odio. Donec quis eros eu erat ullamc orper euismod. Nam
    aliquam turpis. Nunc convallis massa non sem. Donec non odio. Sed non lacus
    eget lacus hend rerit sodales.</p>
    </fieldset>
</div>

The id attribute value of the block element is identical to the href value of the source link (except for the # sign). The div element is assigned the panel class, and the title attribute supplies the new page title for the application. Inside the div element, the h2 element provides a header, whereas the fieldset element, which is commonly used as a container inside iUI destination pages, houses the content. Figure 6-2 displays the results (based in part on additional styles that will be described shortly).

The panel class and fieldset styles are shown in the following code. In addition, the default h2 style is provided (although I will be updating this style in my own irealtor.css file):

body > .panel {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
padding: 10px;
    background: #c8c8c8 url(pinstripes.png);
}
.panel > fieldset {
    position: relative;
    margin: 0 0 20px 0;
    padding: 0;
    background: #FFFFFF;
    -webkit-border-radius: 10px;
    border: 1px solid #999999;
    text-align: right;
    font-size: 16px;
}
.panel > h2 {
    margin: 0 0 8px 14px;
    font-size: inherit;
    font-weight: bold;
    color: #4d4d70;
    text-shadow: rgba(255, 255, 255, 0.75) 2px 2px 0;
}
Destination page

Figure 6-2. Destination page

The panel class property displays the vertical pinstripes, which is a standard background for iPhone and iPod touch applications. The fieldset, used primarily for displaying rows, is employed because it provides a white background box around the text content the page will display. However, because the iui.css styles did not display the margin/padding properties of h2 or p text as I needed it to, I linked irealtor.html with a new style sheet by placing the following declaration below the iui.css declaration:

<style type="text/css" media="screen">@import "irealtor.css";</style>

Inside of irealtor.css, the following styles are defined:

.panel p.normalText {
     text-align: left;
     padding: 0 10px 0 10px;
}
.panel > h2 {
    margin: 3px 0 10px 10px;
}

Displaying Ajax Data from an External URL

You can create an entire iPhone Web application inside a single HTML page using internal links. However, this single-page approach breaks down when you begin to deal with large amounts of data. Therefore, you can use Ajax to break up your application into chunks, yet still maintain the same integrated look and feel of a single page app. When you use Ajax, iUI and other frameworks allow you to load content into your application on demand by providing an external URL. However, the document that is retrieved should be a document fragment, not a complete HTML page.

iUI fully encapsulates XMLHttpRequest() for you. Therefore, when you supply an external URL in a link that does not have target="_self" defined, it retrieves the document fragment and displays it.

In iRealtor, tapping the Featured Listings menu item (<li><a href="featured.html">Featured Listings</a></li>) should display a list of special homes that are being featured by this fictional local realtor. The contents of the file named featured.html are shown here:

<ul id="featuredListings" title="Featured">
<li><a href="406509171.html">30 Bellview Ave, Bolton</a></li>
<li><a href="306488642.html">21 Milford Ave, Brandon</a></li>
<li><a href="326425649.html">10 Main St, Leominster</a></li>
<li><a href="786483624.html">12 Smuggle Lane, Marlboro</a></li>
<li><a href="756883629.html">34 Main Ave, Newbury</a></li>
<li><a href="786476262.html">33 Infinite Loop, Princeton</a></li>
<li><a href="706503711.html">233 Melville Road, Rutland</a></li>
<li><a href="767505714.html">320 Muffly, Sliver</a></li>
<li><a href="706489069.html">1 One Road, Zooly</a></li>
</ul>

The result is a basic navigation list, as shown in Figure 6-3. Each list item specifies a unique URL in which the app loads using Ajax when selected. You'll see this MLS listing destination page shortly.

The All Listings menu item illustrates some additional capabilities that you can add to a navigation list. Figure 6-4 displays the additional details added to the navigation list item, including a thumbnail picture and summary details in a second line.

Listing data coming from Ajax

Figure 6-3. Listing data coming from Ajax

Enhanced navigational menu items

Figure 6-4. Enhanced navigational menu items

The document fragment that is loaded via Ajax is as follows:

<ul id="listings" title="Current Listings">
<li>
<img class="listingImg" src="images/406509171-sm.png" />
    <a class="listing" href="406509171.html">20 May Lane</a>
    <p class="listingDetails">Acton, MA, $318,000, Ranch</p>
</li>
<li>
    <img class="listingImg" src="images/306488642-sm.png" />
    <a class="listing" href="306488642.html">221 Bellingham</a>
    <p class="listingDetails">Borland, MA, $329,000, Colonial</p>
</li>
<li>
    <img class="listingImg" src="images/326425649-sm.png" />
    <a class="listing" href="326425649.html">210 Burlington</a>
    <p class="listingDetails">Borland, MA, $379,000, Ranch</p>
</li>
<li>
    <img class="listingImg" src="images/786483623-sm.png" />
    <a class="listing" href="786483624.html">1 Happy Bosco Lane</a>
    <p class="listingDetails">Borland, MA, $429,000, Ranch</p>
</li>
<li>
    <img class="listingImg" src="images/756883629-sm.png" />
    <a class="listing" href="756883629.html">34 Kramerica Blvd</a>
    <p class="listingDetails">Holden, MA, $529,000, Colonial</p>
</li>
<li>
    <img class="listingImg" src="images/786476262-sm.png" />
    <a class="listing" href="786476262.html">3 George Road</a>
    <p class="listingDetails">Holden, MA, $359,000, Saltbox</p>
</li>
<li>
    <img class="listingImg" src="images/706503711-sm.png" />
    <a class="listing" href="706503711.html">39 Bubble Boy Road</a>
    <p class="listingDetails">Rutland, MA, $959,000, Colonial</p>
</li>
<li>
    <img class="listingImg" src="images/767505713-sm.png" />
    <a class="listing" href="767505714.html">98 Muffin Top Road</a>
    <p class="listingDetails">Rutland, MA, $99,000, Ranch</p>
</li>
<li>
    <img class="listingImg" src="images/706489069-sm.png" />
    <a class="listing" href="706489069.html">1291 Blackjack Lane</a>
    <p class="listingDetails">Zambo, MA, $159,000, Saltbox</p>
</li>
</ul>

Each element inside the li element has a class style assigned to it. The following CSS styles are located in the irealtor.css file:

a.listing {
    padding-left: 54px;
padding-right: 40px;
    min-height: 34px;
}
img.listingImg {
    display: block;
    position: absolute;
    margin: 0;
    left: 6px;
    top: 7px;
    width: 35px;
    height: 27px;
    padding: 7px 0 10px 0;
}
p.listingDetails {
    display: block;
    position: absolute;
    margin: 0;
    left: 54px;
    top: 27px;
    text-align: left;
    font-size: 12px;
    font-weight: normal;
    color: #666666;
    text-decoration: none;
    width: 100%;
    height: 13px;
    padding: 3px 0 0 0;
}

The img.listingImg class positions the thumbnail at the far left side of the item. The p.listingDetails class positions the summary text just below the main link.

Designing for Long Navigation Lists

Although a document fragment such as the one shown previously works fine for small amounts of data, the performance would quickly drag with long lists. To deal with this issue, iUI allows you to break large lists into manageable chunks by loading an initial set of items and then providing a link to the next set (see Figure 6-5). This design emulates the way the iPhone Mail application works with incoming messages.

Loading additional listings

Figure 6-5. Loading additional listings

To provide this functionality in your application, create a link and add target="_replace" as an attribute. iUI loads the items from the URL, replacing the current link. As with other Ajax links, the URL needs to point to a document fragment, not a complete HTML file. Here's the link added to the bottom of the listings ul list:

<li><a href="listings1.html" target="_replace">Get 10 More Listings.</a></li>

When using the target="_replace" attribute, you need to use a fragment of a ul element and not a different structure. For example, the following document fragment is valid to use with a _replace request:

<li>item 1</li>
<li>item 2</li>
<li>item 3</li>

However, the following document fragment would not be correct because it is not valid inside a ul element:

<ul>
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
</ul>

Creating a Destination Page

Each of the MLS listings in iRealtor has its own destination page that is accessed by an Ajax-based link, such as the following:

<a class="listing" href="406509171.html">20 May Lane</a>

The design goal of the page is to provide a picture and summary details of the house listing. But, taking advantage of iPhone's services, you also want to add a button for looking up the address in the Map app and an external Web link to a site providing town information. Figures 6-6 and 6-7 show the end design for this destination page.

The document fragment for this page is as follows:

<div title="20 May Lane" class="panel">
  <div>
    <img src="images/406509171.png" />
  </div>
  <h2>Details</h2>
  <fieldset>
      <div class="row">
        <label>mls #</label>
        <p>406509171</p>
      </div>
      <div class="row">
        <label>address</label>
        <p>20 May Lane</p>
      </div>
      <div class="row">
        <label>city</label>
        <p>Acton</p>
      </div>
      <div class="row">
        <label>price</label>
        <p>$318,000</p>
      </div>
      <div class="row">
        <label>type</label>
        <p>Single Family</p>
      </div>
      <div class="row">
        <label>acres</label>
        <p>0.27</p>
      </div>
      <div class="row">
        <label>rooms</label>
        <p>6</p>
      </div>
      <div class="row">
<label>bath (f)</label>
        <p>1</p>
      </div>
      <div class="row">
        <label>bath (h)</label>
        <p>0</p>
      </div>
  </fieldset>
  <fieldset>
     <div class="row">
          <a  class="serviceButton" target="_self"
href="http://maps.google.com/maps?q=20+May+Lane,+Acton,+MA">Map To House</a>
      </div>
     <div class="row">
          <a  class="serviceButton" target="_self"
href="http://www.mass.gov/?pageID=mg2localgovccpage&L=3&L0=Home&L1=
State%20Government&L2=Local%20Government&sid=massgov2&selectCity=
Acton">View Town Info</a>
    </div>
  </fieldset>
</div>

There are several items of note. First, the div element is assigned the panel class, just as you did for the Meet Our Team page earlier in the chapter. Second, the individual items of the MLS listing data are contained in div elements with the row class. The set of div row elements is contained in a fieldset. Third, the button links to the map and external Web page are assigned a serviceButton class.

Top of listing page

Figure 6-6. Top of listing page

Bottom of listing page

Figure 6-7. Bottom of listing page

The styles for this page come from both iui.css and irealtor.css. First, here are the row class and label styles in iui.css. (If you recall, the fieldset is defined earlier in the chapter.)

.row  {
    position: relative;
    min-height: 42px;
    border-bottom: 1px solid #999999;
    -webkit-border-radius: 0;
    text-align: right;
}
fieldset > .row:last-child {
    border-bottom: none !important;
}
.row > label {
    position: absolute;
    margin: 0 0 0 14px;
    line-height: 42px;
    font-weight: bold;
}

The row class emulates the general look of an iPhone/iPod touch list row found in such locations as the built-in Settings and Contacts apps. The .row:last-child style removes the bottom border of the final row in a fieldset. The .row > label style defined in iui.css emulates the look of iPhone Settings, but as you will see in the following example, the code overrides this formatting to more closely emulate the Contacts look (right-aligned, black font).

The following styles are defined in irealtor.css to augment the base styles:

.panel img {
    display: block;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 10px;
    border: 2px solid #666666;
    -webkit-border-radius: 6px;
}
.row > p {
    display: block;
    margin: 0;
    border: none;
    padding: 12px 10px 0 110px;
    text-align: left;
    font-weight: bold;
    text-decoration: inherit;
    height: 42px;
    color: inherit;
    box-sizing: border-box;
}
.row > label {
    text-align: right;
    width: 80px;
    position: absolute;
    margin: 0 0 0 14px;
    line-height: 42px;
    font-weight: bold;
    color: #7388a5;
}
.serviceButton {
    display: block;
    margin: 0;
    border: none;
    padding: 12px 10px 0 0px;
    text-align: center;
    font-weight: bold;
    text-decoration: inherit;
    height: 42px;
    color: #7388a5;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
}

The .panel > img centers the image with margin-left:auto and margin-right:auto and rounds the edges of the rectangle with -webkit-border-radius. (See Chapter 4, "Designing a Usable and Navigable UI," for more on this CSS style.)

The .row > p style formats the values of each MLS listing information. It is left-aligned and starts at 110px to the right of the left border of the element. The .row > label style adds specific formatting to emulate the Contacts UI look. The .serviceButton class style defines a link with a button look.

Adding a Dialog

The application pages that have been displayed have either been edge-to-edge navigation lists or destination panels for displaying content. However, you will probably need to create a modal dialog. When users are in a dialog, they need to either perform the intended action (such as a search or submittal) or cancel out. Just as in any desktop environment, a dialog is ideal for form entry.

iRealtor needs dialog boxes for two parts of the application: Search and Mortgage Calculator. The Search dialog is accessed by clicking the Search button on the top toolbar. Here's the calling link:

<a class="button" href="#searchForm">Search</a>

The link displays the internal link #searchForm. This references the form element with an id of searchForm:

<form id="searchForm" class="dialog" action="search.php">
     <fieldset>
         <h1>Search Listings</h1>
         <a class="button leftButton" type="cancel">Cancel</a>
         <a class="button blueButton" type="submit">Search</a>
         <select name="proptype" size="1">
           <option value="">Property Type</option>
           <option value="SF">Single-Family</option>
           <option value="CC">Condo</option>
           <option value="MF">Multi-Family</option>
           <option value="LD">Land</option>
           <option value="CI">Commercial</option>
           <option value="MM">Mobile Home</option>
           <option value="RN">Rental</option>
           <option value="BU">Business Opportunity</option>
         </select>
         <label class="altLabel">Min $:</label>
         <input class="altInput" type="text" name="minPrice" />
         <label class="altLabel">Max $:</label>
         <input class="altInput" type="text" name="maxPrice" />
         <label class="altLabel">MLS #:</label>
         <input class="altInput" type="text" name="mlsNumber" />
      </fieldset>
 </form>

The dialog class indicates that the form is a dialog. The form elements are wrapped inside a fieldset. The action buttons for the dialog are actually defined as links. To be specific, the Cancel and Search links are defined as button leftButton and button blueButton classes, respectively. These two action buttons are displayed in the top toolbar of the dialog. It will also display the h1 content as the dialog title.

A select list defines the type of properties that the user wants to choose from. Three input fields are defined for additional search criteria. Because the margin and padding styles are unique for this Search dialog, unique styles are specified for the label and input elements. You'll define those in a moment.

Figure 6-8 shows the form when displayed in the viewport. Per iPhone OS guidelines, the bottom part of the form is shaded to obscure the background page. Figure 6-9 displays the iPhone-specific selection list that is automatically displayed for you when the user clicks inside the select element. Finally, Figure 6-10 shows the pop-up keyboard that is displayed when the user clicks inside the input fields.

Search dialog box

Figure 6-8. Search dialog box

Select list items

Figure 6-9. Select list items

Text input of a form

Figure 6-10. Text input of a form

Consider the CSS styles that are used to display this dialog. From the main stylesheet, there are several rules to pay attention to:

body > .dialog {
    top: 0;
    width: 100%;
    min-height: 417px;
    z-index: 2;
    background: rgba(0, 0, 0, 0.8);
    padding: 0;
    text-align: right;
}
.dialog > fieldset {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    width: 100%;
    margin: 0;
    border: none;
    border-top: 1px solid #6d84a2;
    padding: 10px 6px;
    background: url(toolbar.png) #7388a5 repeat-x;
}
.dialog > fieldset > h1 {
    margin: 0 10px 0 10px;
    padding: 0;
    font-size: 20px;
    font-weight: bold;
    color: #FFFFFF;
    text-shadow: rgba(0, 0, 0, 0.4) 0px −1px 0;
    text-align: center;
}
.dialog > fieldset > label {
    position: absolute;
    margin: 16px 0 0 6px;
    font-size: 14px;
    color: #999999;
}
input {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    width: 100%;
    margin: 8px 0 0 0;
    padding: 6px 6px 6px 44px;
    font-size: 16px;
    font-weight: normal;
}
.blueButton {
    -webkit-border-image: url(blueButton.png) 0 5 0 5;
    border-width: 0 5px;
}
.leftButton {
    left: 6px;
    right: auto;
}

The body > .dialog rule places the form over the entire application, including the top toolbar. It also defines a black background with 0.8 opacity. Notice the way in which the .dialog > fieldset > label style is defined so that the label element appears to be part of the input element. The .blueButton and .leftButton styles define the action button styles.

Three styles are defined in irealtor.css as an extension of iui.css:

.altLabel {
    position: absolute;
    margin: 16px 15px 0 6px;
    font-size: 14px;
    color: black;
}
.altInput {
  padding-left: 60px;
}
select {
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  width: 100%;
  margin: 15px 0 0 0;
  padding: 6px 6px 6px 144px;
  font-size: 16px;
  font-weight: normal;
}

The altLabel and altInput rules appropriately size and position the label and input elements. The select rule styles the select element.

When you submit this form, it is submitted via Ajax to allow the results to slide from the side to provide a smooth transition.

You may, however, have other uses for dialogs beyond form submissions. For example, iRealtor includes a JavaScript-based mortgage calculator that is accessible from the top-level navigation menu. Here's the link:

<li><a href="calc.html">Mortgage Calculator</a></li>

The link accesses the document fragment contained in an external URL that contains the following form:

<form id="calculator" class="dialog">
  <fieldset>
      <h1>Mortgage Calculator</h1>
      <a class="button leftButton" type="cancel">Back</a>
      <label class="altLabel">Loan amount</label>
      <input class="calc" type="text" name="amt_zip" id="amt" />
      <label class="altLabel">Interest rate</label>
      <input class="calc" type="text" name="ir_zip" id="ir" />
      <label class="altLabel">Years</label>
      <input class="calc" type="text" name="amt_zip" id="term" onblur="calc()" />
      <label class="altLabel">Monthly payment</label>
      <input class="calc" type="text" readonly="true" id="payment" />
      <label class="altLabel">Total payment</label>
      <input class="calc" type="text" readonly="true" id="total" />
  </fieldset>
</form>

All the styles have been discussed already except for an additional one in irealtor.css:

input.calc {
    padding: 6px 6px 6px 120px;
}

This class style overrides the default padding to account for the longer labels used in the calculator.

The three input elements have a dummy name attribute that includes zip. The zip string prompts the numeric keyboard to display rather than the alphabet keyboard.

The purpose of the form is for the user to enter information into the first three input elements and then call the JavaScript function calc(), which then displays the results in the bottom two input fields. Because the calculation is performed inside a client-side JavaScript, no submittal is needed with the server.

The JavaScript function calc() needs to reside in the document head of the main irealtor.html file, not the document fragment. Here's the scripting code:

<script type="application/x-javascript">
function calc() {
   var amt = document.getElementById('amt').value;
   var ir =  document.getElementById('ir').value / 1200;
   var term =  document.getElementById('term').value * 12;
var total=1;
   for (var i = 0; i < term; i++) {
      total = total * (1 + ir);
   }
   var mp = amt * ir / ( 1 -- (1 / total));
  document.getElementById('payment').value = Math.round(mp * 100) / 100;
  document.getElementById('total').value = Math.round(mp * term * 100) / 100;
}
</script>

This routine performs a standard mortgage calculation and returns the results to the payment and total input fields. Figure 6-11 shows the result.

Text input of a form

Figure 6-11. Text input of a form

Designing a Contact Us Page with Integrated iPhone Services

The final destination page of iRealtor is a Contact Us page that provides basic contact information for the local realtor and integrates with the Mail, Phone, and Map services of iPhone. The code is shown here.

The document fragment that is loaded by an Ajax external link is as follows:

<div id="contact" title="Contact Us" class="panel">
    <div class="cuiHeader">
      <img class="cui" src="images/jordan_willmark.png" />
      <h1 class="cui" style="text-overflow:ellipsis;">Jordan Willmark</h1>
      <h2 class="cui">J-Team Realty</h2>
</div>
    <fieldset>
        <div class="row">
            <label class="cui">office</label>
            <a class="cuiServiceLink" target="_self" href="tel:(978) 555-1212"
onclick="return (navigator.userAgent.indexOf('iPhone') != −1)">(978) 555-1212</a>
        </div>
        <div class="row">
            <label class="cui">mobile</label>
           <a class="cuiServiceLink" target="_self" href="tel:(978) 545-1211"
onclick="return (navigator.userAgent.indexOf('iPhone') != −1)">(978) 545-1211</a>
        </div>
        <div class="row">
            <label class="cui">e-mail</label>
            <a class="cuiServiceLink" target="_self"
href="mailto:[email protected]" onclick="return
(navigator.userAgent.indexOf('iPhone') != −1)">[email protected]</a>
        </div>
    </fieldset>
    <fieldset>
        <div class="rowCuiAddressBox">
            <label class="cui">work</label>
            <p class="cui">15 Louis Street</p>
            <p class="cui">Princeton, MA 01541</p>
        </div>
    </fieldset>
    <fieldset>
       <div class="row">
            <a  class="serviceButton" target="_self"
href="http://maps.google.com/maps?q=15+Louis+St,+Princeton,+MA+(J-Team+Office)"
>Map To Office</a>
        </div>
    </fieldset>
</div>

You'll notice that the code listing displays several styles prefixed with cui. These are defined in a separate style sheet called cui.css. However, to use these styles, you need to add the following style element to the document head of irealtor.html:

<style type="text/css" media="screen">@import "./iui/cui.css";</style>

Figure 6-12 shows the panel when displayed on the iPhone.

The following three listings provide a full code view of the major source files that have been discussed. Listing 6-1 displays irealtor.html, Listing 6-2 provides iui.css, and Listing 6-3 contains irealtor.css.

iPhone-enabled Contact Us page

Figure 6-12. iPhone-enabled Contact Us page

Example 6-1. irealtor.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
         "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>iRealtor</title>
<meta name="viewport" content="width=device-width; initial-scale=1.0;
maximum-scale=1.0; user-scalable=0;" />
<style type="text/css" media="screen">@import "./iui/iui.css";</style>
<style type="text/css" media="screen">@import "./iui/cui.css";</style>
<style type="text/css" media="screen">@import "irealtor.css";</style>
<script type="application/x-javascript" src="./iui/iui.js"></script>
<script type="application/x-javascript">
function calc() {
   var amt = document.getElementById('amt').value;
   var ir =  document.getElementById('ir').value / 1200;
   var term =  document.getElementById('term').value * 12;
   var total = 1;
   for (var i = 0; i < term; i++) {
      total = total * (1 + ir);
   }
   var mp = amt * ir / ( 1 -- (1 / total));
  document.getElementById('payment').value = Math.round(mp * 100) / 100;
document.getElementById('total').value = Math.round(mp * term *100) / 100;
}
</script>
</head>
<body>
     <!--Top toolbar-->
    <div class="toolbar">
        <h1 id="pageTitle"></h1>
        <a id="backButton" class="button" href="#"></a>
        <a class="button" href="#searchForm">Search</a>
    </div>
    <!--Home menu-->
    <ul id="home" title="iRealtor" selected="true">
        <li><a href="featured.html">Featured Listings</a></li>
        <li><a href="listings.html">All Listings</a></li>
        <li><a href="#">Buying & Tips</a></li>
        <li><a href="calc.html">Mortgage Calculator</a></li>
        <li><a href="#meet_our_team">Meet Our Team</a></li>
        <li><a href="contact_us.html">Contact Us</a></li>
        <li><a href="index.html" target="_self">Visit Our Web Site</a></li>
    </ul>
    <div id="meet_our_team" class="panel" title="Meet Our Team">
        <h2>J-Team Realty</h2>
        <fieldset>
        <p class="normalText">Lorem ipsum dolor sit amet, consect etuer adipis cing
elit. Suspend isse nisl. Vivamus a ligula vel quam tinci dunt posuere. Integer
venen atis blandit est. Phasel lus ac neque. Quisque at augue. Phasellus purus.
Sed et risus. Suspe ndisse laoreet consequat metus. Nam nec justo vitae tortor
fermentum interdum. Aenean vitae quam eu urna pharetra ornare.</p>
        <p class="normalText">Pellent esque habitant morbi tristique senectus et
netus et malesuada fames ac turpis egestas. Aliquam congue. Pel lentesque pretium
fringilla quam. Integer libero libero, varius ut, faucibus et, facilisis vel,
odio. Donec quis eros eu erat ullamc orper euismod. Nam aliquam turpis. Nunc
convallis massa non sem. Donec non odio. Sed non lacus eget lacus hend rerit
sodales.</p>
        </fieldset>
    </div>
    <form id="searchForm" class="dialog" action="search.php">
        <fieldset>
            <h1>Search Listings</h1>
            <a class="button leftButton" type="cancel">Cancel</a>
            <a class="button blueButton" type="submit">Search</a>
            <select name="proptype" size="1">
              <option value="">Property Type</option>
              <option value="SF">Single-Family</option>
              <option value="CC">Condo</option>
              <option value="MF">Multi-Family</option>
              <option value="LD">Land</option>
              <option value="CI">Commercial</option>
              <option value="MM">Mobile Home</option>
              <option value="RN">Rental</option>
              <option value="BU">Business Opportunity</option>
</select>
            <label class="altLabel">Min $:</label>
            <input type="text" name="minPrice" />
            <label class="altLabel">Max $:</label>
            <input type="text" name="maxPrice" />
            <label class="altLabel">MLS #:</label>
            <input type="text" name="mlsNumber" />
         </fieldset>
    </form>
</body>
</html>

Example 6-2. iui.css

body {
    margin: 0;
    font-family: Helvetica;
    background: #FFFFFF;
    color: #000000;
    overflow-x: hidden;
    -webkit-user-select: none;
    -webkit-text-size-adjust: none;
}
body > *:not(.toolbar) {
    display: none;
    position: absolute;
    margin: 0;
    padding: 0;
    left: 0;
    top: 45px;
    width: 100%;
    min-height: 372px;
}
body[orient="landscape"] > *:not(.toolbar) {
    min-height: 268px;
}
body > *[selected="true"] {
    display: block;
}
a[selected], a:active {
    background-color: #194fdb !important;
    background-image: url(listArrowSel.png), url(selection.png)
!important;
    background-repeat: no-repeat, repeat-x;
    background-position: right center, left top;
    color: #FFFFFF !important;
}
a[selected="progress"] {
    background-image: url(loading.gif), url(selection.png)
!important;
}
/***************************************************************
*********************************/
body > .toolbar {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    border-bottom: 1px solid #2d3642;
    border-top: 1px solid #6d84a2;
    padding: 10px;
    height: 45px;
    background: url(toolbar.png) #6d84a2 repeat-x;
}
.toolbar > h1 {
    position: absolute;
    overflow: hidden;
    left: 50%;
    margin: 1px 0 0 −75px;
    height: 45px;
    font-size: 20px;
    width: 150px;
    font-weight: bold;
    text-shadow: rgba(0, 0, 0, 0.4) 0px −1px 0;
    text-align: center;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: #FFFFFF;
}
body[orient="landscape"] > .toolbar > h1 {
    margin-left: −125px;
    width: 250px;
}
.button {
    position: absolute;
    overflow: hidden;
    top: 8px;
    right: 6px;
    margin: 0;
    border-width: 0 5px;
    padding: 0 3px;
    width: auto;
    height: 30px;
    line-height: 30px;
    font-family: inherit;
    font-size: 12px;
    font-weight: bold;
    color: #FFFFFF;
    text-shadow: rgba(0, 0, 0, 0.6) 0px −1px 0;
    text-overflow: ellipsis;
    text-decoration: none;
    white-space: nowrap;
    background: none;
    -webkit-border-image: url(toolButton.png) 0 5 0 5;
}
.blueButton {
-webkit-border-image: url(blueButton.png) 0 5 0 5;
    border-width: 0 5px;
}
.leftButton {
    left: 6px;
    right: auto;
}
#backButton {
    display: none;
    left: 6px;
    right: auto;
    padding: 0;
    max-width: 55px;
    border-width: 0 8px 0 14px;
    -webkit-border-image: url(backButton.png) 0 8 0 14;
}
.whiteButton,
.grayButton {
    display: block;
    border-width: 0 12px;
    padding: 10px;
    text-align: center;
    font-size: 20px;
    font-weight: bold;
    text-decoration: inherit;
    color: inherit;
}
.whiteButton {
    -webkit-border-image: url(whiteButton.png) 0 12 0 12;
    text-shadow: rgba(255, 255, 255, 0.7) 0 1px 0;
}
.grayButton {
    -webkit-border-image: url(grayButton.png) 0 12 0 12;
    color: #FFFFFF;
}
/**************************************************************
**********************************/
body > ul > li {
    position: relative;
    margin: 0;
    border-bottom: 1px solid #E0E0E0;
    padding: 8px 0 8px 10px;
    font-size: 20px;
    font-weight: bold;
    list-style: none;
}
body > ul > li.group {
    position: relative;
    top: −1px;
    margin-bottom: −2px;
    border-top: 1px solid #7d7d7d;
    border-bottom: 1px solid #999999;
    padding: 1px 10px;
    background: url(listGroup.png) repeat-x;
font-size: 17px;
    font-weight: bold;
    text-shadow: rgba(0, 0, 0, 0.4) 0 1px 0;
    color: #FFFFFF;
}
body > ul > li.group:first-child {
    top: 0;
    border-top: none;
}
body > ul > li > a {
    display: block;
    margin: −8px 0 −8px −10px;
    padding: 8px 32px 8px 10px;
    text-decoration: none;
    color: inherit;
    background: url(listArrow.png) no-repeat right center;
}
a[target="_replace"] {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    padding-top: 25px;
    padding-bottom: 25px;
    font-size: 18px;
    color: cornflowerblue;
    background-color: #FFFFFF;
    background-image: none;
}
/**************************************************************
**********************************/
body > .dialog {
    top: 0;
    width: 100%;
    min-height: 417px;
    z-index: 2;
    background: rgba(0, 0, 0, 0.8);
    padding: 0;
    text-align: right;
}
.dialog > fieldset {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    width: 100%;
    margin: 0;
    border: none;
    border-top: 1px solid #6d84a2;
    padding: 10px 6px;
    background: url(toolbar.png) #7388a5 repeat-x;
}
.dialog > fieldset > h1 {
    margin: 0 10px 0 10px;
    padding: 0;
    font-size: 20px;
    font-weight: bold;
color: #FFFFFF;
    text-shadow: rgba(0, 0, 0, 0.4) 0px −1px 0;
    text-align: center;
}
.dialog > fieldset > label {
    position: absolute;
    margin: 16px 0 0 6px;
    font-size: 14px;
    color: #999999;
}
input {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    width: 100%;
    margin: 8px 0 0 0;
    padding: 6px 6px 6px 44px;
    font-size: 16px;
    font-weight: normal;
}
/**************************************************************
**********************************/
body > .panel {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    padding: 10px;
    background: #c8c8c8 url(pinstripes.png);
}
.panel > fieldset {
    position: relative;
    margin: 0 0 20px 0;
    padding: 0;
    background: #FFFFFF;
    -webkit-border-radius: 10px;
    border: 1px solid #999999;
    text-align: right;
    font-size: 16px;
}
.row  {
    position: relative;
    min-height: 42px;
    border-bottom: 1px solid #999999;
    -webkit-border-radius: 0;
    text-align: right;
}
fieldset > .row:last-child {
    border-bottom: none !important;
}
.row > input {
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
    margin: 0;
    border: none;
    padding: 12px 10px 0 110px;
    height: 42px;
    background: none;
}
.row > label {
    position: absolute;
    margin: 0 0 0 14px;
    line-height: 42px;
    font-weight: bold;
}
.row > .toggle {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 100px;
    height: 28px;
}
.toggle {
    border: 1px solid #888888;
    -webkit-border-radius: 6px;
    background: #FFFFFF url(toggle.png) repeat-x;
    font-size: 19px;
    font-weight: bold;
    line-height: 30px;
}
.toggle[toggled="true"] {
    border: 1px solid #143fae;
    background: #194fdb url(toggleOn.png) repeat-x;
}
.toggleOn {
    display: none;
    position: absolute;
    width: 60px;
    text-align: center;
    left: 0;
    top: 0;
    color: #FFFFFF;
    text-shadow: rgba(0, 0, 0, 0.4) 0px −1px 0;
}
.toggleOff {
    position: absolute;
    width: 60px;
    text-align: center;
    right: 0;
    top: 0;
    color: #666666;
}
.toggle[toggled="true"] > .toggleOn {
    display: block;
}
.toggle[toggled="true"] > .toggleOff {
    display: none;
}
.thumb {
    position: absolute;
    top: −1px;
left: −1px;
    width: 40px;
    height: 28px;
    border: 1px solid #888888;
    -webkit-border-radius: 6px;
    background: #ffffff url(thumb.png) repeat-x;
}
.toggle[toggled="true"] > .thumb {
    left: auto;
    right: −1px;
}
.panel > h2 {
    margin: 0 0 8px 14px;
    font-size: inherit;
    font-weight: bold;
    color: #4d4d70;
    text-shadow: rgba(255, 255, 255, 0.75) 2px 2px 0;
}
/**************************************************************
**********************************/
#preloader {
    display: none;
    background-image: url(loading.gif), url(selection.png),
        url(blueButton.png), url(listArrowSel.png), url(listGroup.png);
}

Example 6-3. irealtor.css

a.listing {
    padding-left: 54px;
    padding-right: 40px;
    min-height: 34px;
}
img.listingImg {
    display: block;
    position: absolute;
    margin: 0;
    left: 6px;
    top: 7px;
    width: 35px;
    height: 27px;
    padding: 7px 0 10px 0;
}
p.listingDetails {
    display: block;
    position: absolute;
    margin: 0;
    left: 54px;
    top: 27px;
    text-align: left;
    font-size: 12px;
    font-weight: normal;
    color: #666666;
text-decoration: none;
width: 100%;
    height: 13px;
    padding: 3px 0 0 0;
}
.panel img {
    display: block;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: 10px;
     border: 2px solid #666666;
    -webkit-border-radius: 6px;
}
.row > p {
    display: block;
    margin: 0;
    border: none;
    padding: 12px 10px 0 110px;
     text-align: left;
    font-weight: bold;
    text-decoration: inherit;
    height: 42px;
    color: inherit;
    box-sizing: border-box;
   -webkit-box-sizing: border-box;
}
.row > label {
    text-align: right;
    width: 80px;
    position: absolute;
    margin: 0 0 0 14px;
    line-height: 42px;
    font-weight: bold;
    color: #7388a5;
}
.serviceButton {
    display: block;
    margin: 0;
    border: none;
    padding: 12px 10px 0 0px;
     text-align: center;
    font-weight: bold;
    text-decoration: inherit;
    height: 42px;
    color: #7388a5;
    box-sizing: border-box;
    -webkit-box-sizing: border-box;
}
/*********************************/
.panel p.normalText {
     text-align: left;
     padding: 0 10px 0 10px;
}
.panel > h2 {
    margin: 3px 0 10px 10px;
}
input.calc {
    padding: 6px 6px 6px 120px;
}
/*********************************/
.altLabel {
  position: absolute;
  margin: 16px 15px 0 6px;
  font-size: 14px;
  color: black;
}
.altInput {
  padding-left: 60px;
}
select {
  box-sizing: border-box;
  -webkit-box-sizing: border-box;
  width: 100%;
  margin: 15px 0 0 0;
  padding: 6px 6px 6px 144px;
  font-size: 16px;
  font-weight: normal;
}

Scripting UI Behavior

When you use a framework such as iUI, you'll have a main JavaScript file (such as iui.js) that powers all the UI behavior for you once you include it in your document head. However, because the framework takes control over many aspects of the environment, it is important that you have a solid understanding of the library's internals.

I'll show you iui.js as an example. It consists of the object window.iui, three listeners for load and click events, and several supporting routines. All the JavaScript code is enclosed in an anonymous function with several constants and variables defined:

(function() {
var slideSpeed = 20;
var slideInterval = 0;
var currentPage = null;
var currentDialog = null;
var currentWidth = 0;
var currentHash = location.hash;
var hashPrefix = "#_";
var pageHistory = [];
var newPageCount = 0;
var checkTimer;
// **** REST OF IUI CODE HERE ****
})();

The anonymous function creates a local scope to allow private semiglobal variables and avoid name conflicts with applications that use iui.js.

On Document Load

When the HTML document loads, the following listener function is triggered:

addEventListener("load", function(event)
{
    var page = iui.getSelectedPage();
    if (page)
        iui.showPage(page);
    setTimeout(preloadImages, 0);
}, false);

The getSelectedPage() method of the JSON object iui is called to get the selected page — the block element node that contains a selected="true" attribute. This node is then passed to iui.showPage(), which is the core routine to display content.

setTimeout() is often used when calling certain JavaScript routines to prevent timing inconsistencies. Using setTimeout(), iUI calls an image preloader function to load application images.

Getting back to iui.showPage(), its code is as follows:

showPage: function(page, backwards)
{
    if (page)
    {
        if (currentDialog)
        {
            currentDialog.removeAttribute("selected");
            currentDialog = null;
        }
        if (hasClass(page, "dialog"))
            showDialog(page);
        else
        {
           var fromPage = currentPage;
           currentPage = page;
           if (fromPage)
               setTimeout(slidePages, 0, fromPage, page, backwards);
           else
              updatePage(page, fromPage);
        }
    }
}

The currentDialog semi-global variable is evaluated to determine whether a dialog is already displayed. (currentDialog is set in the showDialog() function.) This variable would be null when the document initially loads because of the line var currentDialog = null;.

The node is then evaluated to determine whether it is a dialog (containing class="dialog" as an attribute) or a normal page. Although the opening page of an iPhone/iPod touch is often a normal page, you may want to have a login or initial search dialog.

Loading a Standard iUI Page

For normal pages, iUI assigns the value of currentPage to the variable fromPage and then reassigns currentPage to the page parameter. If fromPage is not null (that is, every page after the initial page), iUI performs a slide-in animation with a function called slidePages(). The fromPage, page, and backwards variables are passed to slidePages().

However, because this is the first time running this routine (and fromPage will equal null), the updatePage() function is called:

function updatePage(page, fromPage)
{
    if (!page.id)
        page.id = "__" + (++newPageCount) + "__";
    location.href = currentHash = hashPrefix + page.id;
    pageHistory.push(page.id);
    var pageTitle = $("pageTitle");
    if (page.title)
        pageTitle.innerHTML = page.title;
    if (page.localName.toLowerCase() == "form" && !page.target)
        showForm(page);
    var backButton = $("backButton");
    if (backButton)
    {
        var prevPage = $(pageHistory[pageHistory.length-2]);
        if (prevPage && !page.getAttribute("hideBackButton"))
        {
            backButton.style.display = "inline";
            backButton.innerHTML = prevPage.title ? prevPage.title: "Back";
        }
        else
            backButton.style.display = "none";
    }
}

The updatePage() function is responsible for updating the pageHistory array, which is required for enabling the Mobile Safari Back button to work even in single-page applications. The value of the node's title attribute is then assigned to be the innerHTML of the top toolbar's h1 pageTitle.

If the page name contains the string form, the showForm() function is called. Otherwise, the routine continues, looking to see if a backButton element is defined in the toolbar. If it is, the page history and button title are updated.

Subsequent pages will always bypass the direct call to updatePage() and use the slidePages() function instead. Here is the code:

function slidePages(fromPage, toPage, backwards)
{
    var axis = (backwards ? fromPage: toPage).getAttribute("axis");
    if (axis == "y")
        (backwards ? fromPage: toPage).style.top = "100%";
    else
toPage.style.left = "100%";
    toPage.setAttribute("selected", "true");
    scrollTo(0, 1);
    clearInterval(checkTimer);
    var percent = 100;
    slide();
    var timer = setInterval(slide, slideInterval);
    function slide()
    {
        percent -= slideSpeed;
        if (percent <= 0)
        {
            percent = 0;
            if (!hasClass(toPage, "dialog"))
                fromPage.removeAttribute("selected");
            clearInterval(timer);
            checkTimer = setInterval(checkOrientAndLocation, 300);
            setTimeout(updatePage, 0, toPage, fromPage);
        }
        if (axis == "y")
        {
            backwards
                ? fromPage.style.top = (100-percent) + "%"
             : toPage.style.top = percent + "%";
        }
        else
        {
            fromPage.style.left = (backwards ? (100-percent): (percent-100)) + "%";
            toPage.style.left = (backwards ? -percent: percent) + "%";
        }
    }
}

The primary purpose of slidePages() is to emulate the standard iPhone/iPod touch slide animation effect when you move between pages. It achieves this by using JavaScript timer routines to incrementally update the style.left property of the fromPage and the toPage. The updatePage() function (discussed previously) is called inside a setTimeout routine.

Handling Link Clicks

Because most of the user interaction with an iPhone Web application is tapping the interface to navigate the application, the event listener for link clicks is, in many ways, the "mission control center" for iui.jss. Check out the code:

addEventListener("click", function(event)
{
    var link = findParent(event.target, "a");
    if (link)
    {
        function unselect() { link.removeAttribute("selected"); }
        if (link.href && link.hash && link.hash != "#")
        {
link.setAttribute("selected", "true");
            iui.showPage($(link.hash.substr(1)));
            setTimeout(unselect, 500);
        }
        else if (link == $("backButton"))
            history.back();
        else if (link.getAttribute("type") == "submit")
            submitForm(findParent(link, "form"));
        else if (link.getAttribute("type") == "cancel")
            cancelDialog(findParent(link, "form"));
        else if (link.target == "_replace")
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, link, unselect);
        }
        else if (!link.target)
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, null, unselect);
        }
        else
            return;
        event.preventDefault();
    }
}, true);

This routine evaluates the type of link:

  • If it is an internal URL, the page is passed to iui.showPage().

  • If the backButton is clicked, history.back() is triggered.

  • Dialog forms typically contain a Submit and Cancel button. If a Submit button is clicked, submitForm() is called. If a Cancel button is clicked, cancelDialog() is called. (The submitForm() and cancelDialog() functions are discussed later in this chapter.)

  • External URLs that have target="_replace" or that do not have target defined are Ajax links. Both of these call the iui.showPageByHref() method.

  • If the link is none of these, it is external with a target="_self" attribute defined. The default iUI behavior is suspended and the link is treated as normal.

Handling Ajax Links

When a user clicks an Ajax link, the click event listener (shown previously) calls the iui.showPageByHref() method:

showPageByHref: function(href, args, method, replace, cb)
{
    var req = new XMLHttpRequest();
    req.onerror = function()
    {
        if (cb)
cb(false);
      };
      req.onreadystatechange = function()
      {
          if (req.readyState == 4)
          {
             if (replace)
                    replaceElementWithSource(replace, req.responseText);
             else
             {
                 var frag = document.createElement("div");
                 frag.innerHTML = req.responseText;
                 iui.insertPages(frag.childNodes);
             }
             if (cb)
                 setTimeout(cb, 1000, true);
          }
      };
      if (args)
   {
          req.open(method || "GET", href, true);
          req.setRequestHeader("Content-Type",
          "application/x-www-form-urlencoded");
          req.setRequestHeader("Content-Length", args.length);
          req.send(args.join("&"));
   }
   else
   {
        req.open(method || "GET", href, true);
        req.send(null);
   }
}

The routine calls XMLHttpRequest() to assign the req object. If the args parameter is not null (that is, when an Ajax form is submitted), the form data is sent to the server. If args is null, the supplied URL is sent to the server. The processing of incoming text takes place inside the onreadystatechange handler, which handles the response from the server.

If replace is true (meaning that target="_replace" is specified in the calling link), the replaceElementWithSource() function is called. As the following code shows, the calling link node (the replace parameter) is replaced with the source (the Ajax document fragment):

function replaceElementWithSource(replace, source)
{
    var page = replace.parentNode;
    var parent = replace;
    while (page.parentNode != document.body)
    {
        page = page.parentNode;
        parent = parent.parentNode;
    }
    var frag = document.createElement(parent.localName);
    frag.innerHTML = source;
page.removeChild(parent);
    while (frag.firstChild)
        page.appendChild(frag.firstChild);
}

If a click is generated from a normal Ajax link, the contents of the external URL are displayed in a new page. Therefore, a div is created and the document fragment is added as the innerHTML of the element. The iui.insertPages() method adds the new nodes to create a new page, and this page is passed to iui.showPage():

insertPages: function(nodes)
    {
        var targetPage;
        for (var i = 0; i < nodes.length; ++i)
        {
            var child = nodes[i];
            if (child.nodeType == 1)
            {
                if (!child.id)
                    child.id = "__" + (++newPageCount) + "__";
                var clone = $(child.id);
                if (clone)
                    clone.parentNode.replaceChild(child, clone);
                else
                    document.body.appendChild(child);
                if (child.getAttribute("selected") == "true" || !targetPage)
                    targetPage = child;
            }
        }
        if (targetPage)
            iui.showPage(targetPage);
    }

Loading a Dialog

If the node that is passed into the main showPage() function is a dialog (class="dialog"), the showDialog() function is called, which in turn calls showForm(). These two functions are shown in the following code:

function showDialog(page)
{
    currentDialog = page;
    page.setAttribute("selected", "true");
    if (hasClass(page, "dialog") && !page.target)
        showForm(page);
}
function showForm(form)
{
    form.onsubmit = function(event)
    {
        event.preventDefault();
        submitForm(form);
    };
form.onclick = function(event)
    {
        if (event.target == form && hasClass(form, "dialog"))
            cancelDialog(form);
    };
}

The showForm() function assigns event handlers to the onsubmit and onclick events of the form. When a form is submitted, the submitForm() function submits the form data via Ajax. When an element on the form is clicked, the dialog is closed. The following code shows the routines that are called:

function submitForm(form)
{
    iui.showPageByHref(form.action || "POST", encodeForm(form), form.method);
}
function cancelDialog(form)
{
    form.removeAttribute("selected");
}
function encodeForm(form)
{
    function encode(inputs)
    {
        for (var i = 0; i < inputs.length; ++i)
        {
            if (inputs[i].name)
                args.push(inputs[i].name + "=" + escape(inputs[i].value));
        }
    }
    var args = [];
    encode(form.getElementsByTagName("input"));
    encode(form.getElementsByTagName("select"));
    return args;
}

The entire code for iui.js is provided in Listing 6-4.

Example 6-4. iui.js

(function() {
var slideSpeed = 20;
var slideInterval = 0;
var currentPage = null;
var currentDialog = null;
var currentWidth = 0;
var currentHash = location.hash;
var hashPrefix = "#_";
var pageHistory = [];
var newPageCount = 0;
var checkTimer;
//
***************************************************************
**********************************
window.iui =
{
    showPage: function(page, backwards)
    {
        if (page)
        {
            if (currentDialog)
            {
                currentDialog.removeAttribute("selected");
                currentDialog = null;
            }
            if (hasClass(page, "dialog"))
                showDialog(page);
            else
            {
                var fromPage = currentPage;
                currentPage = page;
                if (fromPage)
                    setTimeout(slidePages, 0, fromPage, page,
                    backwards);
                else
                    updatePage(page, fromPage);
            }
        }
    },
    showPageById: function(pageId)
    {
        var page = $(pageId);
        if (page)
        {
            var index = pageHistory.indexOf(pageId);
            var backwards = index != −1;
            if (backwards)
                pageHistory.splice(index, pageHistory.length);
            iui.showPage(page, backwards);
        }
    },
    showPageByHref: function(href, args, method, replace, cb)
    {
        var req = new XMLHttpRequest();
        req.onerror = function()
        {
            if (cb)
                cb(false);
        };
        req.onreadystatechange = function()
        {
            if (req.readyState == 4)
            {
                if (replace)
                    replaceElementWithSource(replace, req.responseText);
                else
                {
                    var frag = document.createElement("div");
frag.innerHTML = req.responseText;
                    iui.insertPages(frag.childNodes);
                }
                if (cb)
                    setTimeout(cb, 1000, true);
            }
        };
        if (args)
        {
            req.open(method || "GET", href, true);
            req.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
            req.setRequestHeader("Content-Length", args.length);
            req.send(args.join("&"));
        }
        else
        {
            req.open(method || "GET", href, true);
            req.send(null);
        }
    },
    insertPages: function(nodes)
    {
        var targetPage;
        for (var i = 0; i < nodes.length; ++i)
        {
            var child = nodes[i];
            if (child.nodeType == 1)
            {
                if (!child.id)
                    child.id = "__" + (++newPageCount) + "__";
                var clone = $(child.id);
                if (clone)
                    clone.parentNode.replaceChild(child, clone);
                else
                    document.body.appendChild(child);
                if (child.getAttribute("selected") == "true"
                || !targetPage)
                    targetPage = child;
             --i;
            }
        }
        if (targetPage)
            iui.showPage(targetPage);
    },
    getSelectedPage: function()
    {
        for (var child = document.body.firstChild; child; child =
        child.nextSibling)
        {
            if (child.nodeType == 1 &&
            child.getAttribute("selected") == "true")
return child;
        }
    }
};
//
***************************************************************
**********************************
addEventListener("load", function(event)
{
    var page = iui.getSelectedPage();
    if (page)
        iui.showPage(page);
    setTimeout(preloadImages, 0);
    setTimeout(checkOrientAndLocation, 0);
    checkTimer = setInterval(checkOrientAndLocation, 300);
}, false);
addEventListener("click", function(event)
{
    var link = findParent(event.target, "a");
    if (link)
    {
        function unselect() { link.removeAttribute("selected"); }
        if (link.href && link.hash && link.hash != "#")
        {
            link.setAttribute("selected", "true");
            iui.showPage($(link.hash.substr(1)));
            setTimeout(unselect, 500);
        }
        else if (link == $("backButton"))
            history.back();
        else if (link.getAttribute("type") == "submit")
            submitForm(findParent(link, "form"));
        else if (link.getAttribute("type") == "cancel")
            cancelDialog(findParent(link, "form"));
        else if (link.target == "_replace")
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, link,
            unselect);
        }
        else if (!link.target)
        {
            link.setAttribute("selected", "progress");
            iui.showPageByHref(link.href, null, null, null,
            unselect);
        }
        else
            return;
        event.preventDefault();
    }
}, true);
addEventListener("click", function(event)
{
    var div = findParent(event.target, "div");
if (div && hasClass(div, "toggle"))
    {
        div.setAttribute("toggled", div.getAttribute("toggled")
        != "true");
        event.preventDefault();
    }
}, true);
function checkOrientAndLocation()
{
    if (window.innerWidth != currentWidth)
    {
        currentWidth = window.innerWidth;
        var orient = currentWidth == 320 ? "profile": "landscape";
        document.body.setAttribute("orient", orient);
        setTimeout(scrollTo, 100, 0, 1);
    }
    if (location.hash != currentHash)
    {
        var pageId = location.hash.substr(hashPrefix.length)
        iui.showPageById(pageId);
    }
}
function showDialog(page)
{
    currentDialog = page;
    page.setAttribute("selected", "true");
    if (hasClass(page, "dialog") && !page.target)
        showForm(page);
}
function showForm(form)
{
    form.onsubmit = function(event)
    {
        event.preventDefault();
        submitForm(form);
    };
    form.onclick = function(event)
    {
        if (event.target == form && hasClass(form, "dialog"))
            cancelDialog(form);
    };
}
function cancelDialog(form)
{
    form.removeAttribute("selected");
}
function updatePage(page, fromPage)
{
    if (!page.id)
        page.id = "__" + (++newPageCount) + "__";
    location.href = currentHash = hashPrefix + page.id;
    pageHistory.push(page.id);
var pageTitle = $("pageTitle");
    if (page.title)
        pageTitle.innerHTML = page.title;
    if (page.localName.toLowerCase() == "form" && !page.target)
        showForm(page);
    var backButton = $("backButton");
    if (backButton)
    {
        var prevPage = $(pageHistory[pageHistory.length-2]);
        if (prevPage && !page.getAttribute("hideBackButton"))
        {
            backButton.style.display = "inline";
            backButton.innerHTML = prevPage.title ?
            prevPage.title: "Back";
        }
        else
            backButton.style.display = "none";
    }
}
function slidePages(fromPage, toPage, backwards)
{
    var axis = (backwards ? fromPage: toPage).getAttribute("axis");
    if (axis == "y")
        (backwards ? fromPage: toPage).style.top = "100%";
    else
        toPage.style.left = "100%";
    toPage.setAttribute("selected", "true");
    scrollTo(0, 1);
    clearInterval(checkTimer);
    var percent = 100;
    slide();
    var timer = setInterval(slide, slideInterval);
    function slide()
    {
        percent -= slideSpeed;
        if (percent <= 0)
        {
            percent = 0;
            if (!hasClass(toPage, "dialog"))
                fromPage.removeAttribute("selected");
            clearInterval(timer);
            checkTimer = setInterval(checkOrientAndLocation, 300);
            setTimeout(updatePage, 0, toPage, fromPage);
        }
        if (axis == "y")
        {
            backwards
                ? fromPage.style.top = (100-percent) + "%"
             : toPage.style.top = percent + "%";
        }
        else
        {
            fromPage.style.left = (backwards ? (100-percent):
            (percent-100)) + "%";
toPage.style.left = (backwards ? -percent: percent)
            + "%";
        }
    }
}
function preloadImages()
{
    var preloader = document.createElement("div");
    preloader.id = "preloader";
    document.body.appendChild(preloader);
}
function submitForm(form)
{
    iui.showPageByHref(form.action || "POST", encodeForm(form),
    form.method);
}
function encodeForm(form)
{
    function encode(inputs)
    {
        for (var i = 0; i < inputs.length; ++i)
        {
            if (inputs[i].name)
                args.push(inputs[i].name + "=" +
                escape(inputs[i].value));
        }
    }
    var args = [];
    encode(form.getElementsByTagName("input"));
    encode(form.getElementsByTagName("select"));
    return args;
}
function findParent(node, localName)
{
    while (node && (node.nodeType != 1 ||
    node.localName.toLowerCase() != localName))
        node = node.parentNode;
    return node;
}
function hasClass(self, name)
{
    var re = new RegExp("(^|\s)"+name+"($|\s)");
    return re.exec(self.getAttribute("class")) != null;
}
function replaceElementWithSource(replace, source)
{
    var page = replace.parentNode;
    var parent = replace;
    while (page.parentNode != document.body)
    {
        page = page.parentNode;
        parent = parent.parentNode;
}
    var frag = document.createElement(parent.localName);
    frag.innerHTML = source;
    page.removeChild(parent);
    while (frag.firstChild)
        page.appendChild(frag.firstChild);
}
function $(id) { return document.getElementById(id); }
function ddd() { console.log.apply(console, arguments); }
})();

Summary

The focus of this chapter was to "roll up your sleeves" and dive into the nitty gritty programming details of a typical iPhone Web app user interface. Using iUI, I walked through the development of a fictional app called iRealtor. I explained how to create a side-to-side navigation page, a detail page, a contact form, as well as how to work with several different types of controls found on these pages.

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

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