With this tiny bit of HTML, CSS, and
JavaScript, we have essentially turned an entire website into a
single-page application. However, it still leaves quite a bit to be
desired. Let’s slick things up a bit.1. Progress Indicator
Since we are not allowing the browser to navigate from page to
page, the user will not see any indication of progress while data is
loading (Figure 1). We need to
provide some feedback to users to let them know that something is, in
fact, happening. Without this feedback, users may wonder if they
actually clicked the link or missed it, and will often start clicking
all over the place in frustration. This can lead to increased server
load and application instability (i.e., crashing).
Thanks to jQuery, providing a progress indicator only takes two
lines of code. We’ll just append a loading div to the body
when loadPage() starts and remove the loading
div when hijackLinks() is done.
Example 1. Adding a simple progress indicator to the page
$(document).ready(function(){ loadPage(); }); function loadPage(url) { $('body').append('<div id="progress">Loading...</div>'); if (url == undefined) { $('#container').load('index.html #header ul', hijackLinks); } else { $('#container').load(url + ' #content', hijackLinks); } } function hijackLinks() { $('#container a').click(function(e){ e.preventDefault(); loadPage(e.target.href); }); $('#progress').remove(); }
|
If you are testing this web application on a local network, the
network speeds will be so fast you won’t ever see the progress
indicator. If you are using Mac OS X, you can slow all incoming web traffic by
typing a couple of ipfw commands at the terminal. For
example, these commands will slow all web traffic to 4 kilobytes per
second: sudo ipfw pipe 1 config bw 4KByte/s sudo ipfw add 100 pipe 1 tcp from any to me 80
You should use your computer’s hostname or
external IP address in the URL (for example,
mycomputer.local rather than localhost).
When you’re done testing, delete the rule with sudo ipfw delete
100 (you can delete all custom rules with ipfw
flush). You can do similar things on Linux and Windows as well. For Linux, check out the
following links: If you are using Windows, see the following: If you are using the Android emulator, you can configure it to limit its speed
using the -netspeed command-line option. For example,
invoking the emulator with the arguments -netspeed edge will simulate real-world
EDGE network speeds (118.4 kilobits per second upstream, 236.8
kilobits per second downstream). Run emulator
-help-netspeed at the command line to see a list of all
supported speeds. |
See Example 2 for the CSS you need to add
to android.css to style the progress div.
Example 2. CSS added to android.css used to style the
progress indicator
#progress { -webkit-border-radius: 10px; background-color: rgba(0,0,0,.7); color: white; font-size: 18px; font-weight: bold; height: 80px; left: 60px; line-height: 80px; margin: 0 auto; position: absolute; text-align: center; top: 120px; width: 200px; }
|
2. Setting the Page Title
Our site happens to have a single h2 at the beginning
of each page that would make a nice page title (see Figure 2). To be more
mobile-friendly, we’ll pull that title out of the content and put it in
the header (see Figure 3-3). Again,
jQuery to the rescue: you can just add three lines to the
hijackLinks() function to make it happen. Example 3-6 shows the hijackLinks function
with these changes.
Example 3. Using the h2 from the target page as the toolbar
title
function hijackLinks() { $('#container a').click(function(e){ e.preventDefault(); loadPage(e.target.href); }); var title = $('h2').html() || 'Hello!'; $('h1').html(title); $('h2').remove(); $('#progress').remove(); }
|
Note:
I added the title lines before the line
that removes the progress indicator. I like to remove the progress
indicator as the very last action because I think it makes the
application feel more responsive.
The double pipe (||) in the first line of inserted code
(shown in bold) is the JavaScript logical operator OR. Translated into
English, that line reads, “Set the title variable to the HTML contents
of the h2 element, or to the string ‘Hello!’ if there is no
h2 element.” This is important because the first page load
won’t contain an h2 because we are just grabbing the nav uls.
Note:
This point probably needs some
clarification. When users first load the
android.html URL, they are only going to see the
overall site navigation elements, as opposed to any site content. They
won’t see any site content until they tap a link on this initial
navigation page.
3. Handling Long Titles
Suppose we had a page on our site with a title too long to fit in the
header bar (Figure 4). We could just let the
text break onto more than one line, but that would not be very
attractive. Instead, we can update the #header h1 styles
such that long text will be truncated with a trailing ellipsis (see Figure 5
and Example 4). This might be my favorite
little-known CSS trick.
Example 4. Adding an ellipsis to text that is too long for its
container
#header h1 { color: #222; font-size: 20px; font-weight: bold; margin: 0 auto; padding: 10px 0; text-align: center; text-shadow: 0px 1px 1px #fff; max-width: 160px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
Here’s the rundown: max-width: 160px instructs the browser not
to allow the h1 element to grow wider than
160px. Then, overflow: hidden instructs the browser to
chop off any content that extends outside the element borders.
Next, white-space: nowrap prevents the browser
from breaking the line into two. Without this line, the h1
would just get taller to accommodate the text at the defined width.
Finally, text-overflow: ellipsis appends three dots
to the end of any chopped-off text to indicate to the user that she is
not seeing the entire string.
4. Automatic Scroll-to-Top
Let’s say you have a page that is longer than the viewable area on
the phone. The user visits the page, scrolls down to the bottom, and
clicks on a link to an even longer page. In this case, the new page will
show up “prescrolled” instead of at the top as you’d expect.
Technically, this makes sense because we are
not actually leaving the current (scrolled) page, but it’s certainly a
confusing situation for the user. To rectify the situation, we can add
a scrollTo() command to the loadPage() function (Example 5).
Whenever a user clicks a link, the page will
first jump to the top. This has the added benefit of ensuring the
loading graphic is visible if the user clicks a link at the bottom of a
long page.
Example 5. It’s a good idea to scroll back to the top when a user
navigates to a new page
function loadPage(url) { $('body').append('<div id="progress">Loading...</div>'); scrollTo(0,0); if (url == undefined) { $('#container').load('index.html #header ul', hijackLinks); } else { $('#container').load(url + ' #content', hijackLinks); } }
|
5. Hijacking Local Links Only
Like most sites, ours has links to external pages (i.e., pages
hosted on other domains). We shouldn’t hijack these external links,
because it wouldn’t make sense to inject their HTML into our
Android-specific layout. As shown in Example 3-9, we can add a conditional that
checks the URL for the existence of our domain name. If it’s found, the
link is hijacked and the content is loaded into the current page (i.e.,
Ajax is in effect). If not, the browser will navigate to the URL
normally.
Warning:
You must
change jonathanstark.com to the appropriate domain or hostname for your website, or
the links to pages on your website will no longer be
hijacked.
Example 6. You can allow external pages to load normally by checking the
domain name of the URL
function hijackLinks() { $('#container a').click(function(e){ var url = e.target.href; if (url.match(/jonathanstark.com/)) { e.preventDefault(); loadPage(url); } }); var title = $('h2').html() || 'Hello!'; $('h1').html(title); $('h2').remove(); $('#progress').remove(); }
|
Tip:
The url.match function uses a
language, regular expressions, that is often embedded within other
programming languages such as JavaScript, PHP, and Perl. Although this
regular expression is simple, more complex expressions can be a bit
intimidating, but are well worth becoming familiar with. My favorite
regex page is located at http://www.regular-expressions.info/javascriptexample.html.
6. Roll Your Own Back Button
The elephant in the room at this point is that the user has no
way to navigate back to previous pages (remember that we’ve hijacked all
the links, so the browser page history won’t work). Let’s address that
by adding a Back button to the top left corner of the screen. First,
we’ll update the JavaScript, and then we’ll do the CSS.
Adding a standard toolbar Back button to the
app means keeping track of the user’s click history. To do this, we’ll
have to:
store the URL of the previous page so we know where to go back
to, and
store the title of the previous page so we know what label to
put on the Back button
Adding this feature touches on most of the
JavaScript we’ve written so far in this chapter, so I’ll go over the
entire new version of android.js line by line (see
Example 7). The result will look
like Figure 6.
Example 7. Expanding the existing JavaScript example to include support
for a Back button
var hist = []; var startUrl = 'index.html'; $(document).ready(function(){ loadPage(startUrl); }); function loadPage(url) { $('body').append('<div id="progress">Loading...</div>'); scrollTo(0,0); if (url == startUrl) { var element = ' #header ul'; } else { var element = ' #content'; } $('#container').load(url + element, function(){ var title = $('h2').html() || 'Hello!'; $('h1').html(title); $('h2').remove(); $('.leftButton').remove(); hist.unshift({'url':url, 'title':title}); if (hist.length > 1) { $('#header').append('<div class="leftButton">'+hist[1].title+'</div>'); $('#header .leftButton').click(function(){ var thisPage = hist.shift(); var previousPage = hist.shift(); loadPage(previousPage.url); }); } $('#container a').click(function(e){ var url = e.target.href; if (url.match(/jonathanstark.com/)) { e.preventDefault(); loadPage(url); } }); $('#progress').remove(); }); }
|
Tip:
Please visit http://www.hunlock.com/blogs/Mastering_Javascript_Arrays
for a full listing of JavaScript array functions with descriptions and
examples.
Now that we have our Back button, all that
remains is to purty it up with some CSS (see Example 8). We’ll start off by styling the text
with font-weight, text-align,
line-height, color, and
text-shadow. We’ll continue by placing the div
precisely where we want it on the page with position,
top, and left. Then, we’ll make sure that long
text on the button label will truncate with an ellipsis using max-width, white-space,
overflow, and text-overflow. Finally, we’ll
apply a graphic with border-width and -webkit-border-image. Unlike the earlier
border image example, this image has a different width for the left and
right borders because the image is made asymmetrical by the arrowhead on
the left side.
Note:
Don’t forget that you’ll need an image for
this button. You’ll need to save it as
back_button.png in the
images folder underneath the folder that holds
your HTML file.
Example 8. Add the following to android.css to
beautify the Back button with a border image
#header div.leftButton { font-weight: bold; text-align: center; line-height: 28px; color: white; text-shadow: 0px -1px 1px rgba(0,0,0,0.6); position: absolute; top: 7px; left: 6px; max-width: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-width: 0 8px 0 14px; -webkit-border-image: url(images/back_button.png) 0 8 0 14; }
|
By default, Android displays an orange
highlight to clickable objects that have been tapped (Figure 8). This may appear only briefly, but
removing it is easy and makes the app look much better. Fortunately,
Android supports a CSS property called -webkit-tap-highlight-color, which allows
you to suppress this behavior. We can do this here by setting the tap
highlight to a fully transparent color (see Example 9).
Example 9. Add the following to android.css to remove
the default tap highlight effect
#header div.leftButton { font-weight: bold; text-align: center; line-height: 28px; color: white; text-shadow: 0px -1px 1px rgba(0,0,0,0.6); position: absolute; top: 7px; left: 6px; max-width: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border-width: 0 8px 0 14px; -webkit-border-image: url(images/back_button.png) 0 8 0 14; -webkit-tap-highlight-color: rgba(0,0,0,0); }
|
In the case of the Back button, there could
be at least a second or two of delay before the content from the
previous page appears. To avoid frustration, we can configure the button
to look clicked the instant it’s tapped. In a desktop browser, this is a
simple process: you just add a declaration to your CSS using
the :active pseudoclass to specify an
alternate style for the object that the user clicked. I don’t know if
it’s a bug or a feature, but this approach does not work on Android; the
:active style is ignored.
I toyed around with combinations of
:active and :hover, which brought me some success
with non-Ajax apps. However, with an Ajax app like the one we are using
here, the :hover style is sticky (i.e., the button
appears to remain “clicked” even after the finger is removed).
Fortunately, the fix is pretty simple—use
jQuery to add the class clicked to the button
when the user taps it. I’ve opted to apply a darker version of the
button image to the button in the example (see Figure 8 and Example 10). You’ll need to make sure
you have a button image called
back_button_clicked.png in the
images subfolder.
Example 10. Add the following to android.css to make
the Back button looked clicked when the user taps it
#header div.leftButton.clicked { -webkit-border-image: url(images/back_button_clicked.png) 0 8 0 14; }
|
Note:
Since we’re using an image for the clicked
style, it would be smart to preload the image. Otherwise, the
unclicked button graphic will disappear the first time it’s tapped
while the clicked graphic downloads. I’ll cover image preloading in
the next chapter.
With the CSS in place, we can now update the
portion of the android.js that assigns the click
handler to the Back button. First, we add a variable, e, to
the anonymous function to capture the incoming click event. Then, we
wrap the event target in a jQuery selector and call jQuery’s
addClass() function to assign the clicked CSS class to the
button:
$('#header .leftButton').click(function(e){
$(e.target).addClass('clicked');
var thisPage = hist.shift();
var previousPage = hist.shift();
loadPage(lastUrl.url);
});
Note:
A special note to any CSS gurus in the
crowd: the CSS Sprite technique—popularized by A List Apart—is
not an option in this case because it requires setting offsets for the
image. The -webkit-border-image property does not support
image offsets.