SlideShare uma empresa Scribd logo
1 de 136
Baixar para ler offline
PhoneGap by Dissection
My first PhoneGap 3.x app
Daniel Rhodes
This book is for sale at http://leanpub.com/phonegapbydissection
This version was published on 2015-02-26
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools
and many iterations to get reader feedback, pivot until you have the right book and build
traction once you do.
©2015 Daniel Rhodes
Dedicated to all the hard-working girls and boys in the free and open source software
communities.
Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Conventions used in the text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. What you’ll need . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
3. What is PhoneGap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
4. Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.1 The cool new way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
4.2 The fiddly older way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
5. Quick run-through of the default app . . . . . . . . . . . . . . . . . . . . . . . . . . 9
6. First things first: The layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
7. First things first: The tabbing mechanism . . . . . . . . . . . . . . . . . . . . . . . . 27
8. The Search tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
8.2 Creating the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
8.3 Querying the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
8.4 Results scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
8.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
9. The Discover tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
9.2 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
10.The Write tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
10.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
10.2 Filling the screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
10.3 Displaying a random character . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
10.4 Finger doodling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
10.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
11.Splash screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
12.Launcher icon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
13.Submitting to Google Play . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
CONTENTS
14.That’s all folks! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
1. Introduction
This book is going to teach you how to get started with mobile app development using the
PhoneGap platform. We’ll essentially rebuild, from scratch, a basic yet fully-functional app that
really exists! It’s called Japxlate and can be found here in the Google Play Store. The app is a
Japanese dictionary that you can search - even if offline. Not to worry though, we won’t get
bogged down in the nitty gritty of Japanese linguistics. We’ll focus on setting up, building and
finally deploying the app. You’ll laugh, you’ll cry, you’ll sick a little bit in the back of your throat,
but the journey will definitely be worth it…
This is version 1.0 of the book, first published February 2015 (v0.9 first published
January 2014)
Latest source code for the app is at https://github.com/danielrhodeswarp/japxlate-
android
This book was written using PhoneGap v3.1.0, but has been updated to cover anything
new or different in v3.3.0
1.1 Conventions used in the text
A command that you need to type on the Linux command line will look like:
you@yours$ somewhere]$ some linux command to type
Code (of any type - CSS, HTML or JavaScript) that you need to type in will look like:
//does the cursor have random fractals?
function checkRandomFractals()
{
return something.or.other;
}
HTML elements will be referred to like:
<elementname>
Code fragments, variable names, method names etc will look like:
Introduction 2
someMethod();
File names and folder names will look like:
/assets/www/some_file.html
A side note, something tangental to the main text, will look like:
..
I’m hungry but my teeth hurt.
New or updated information relevant for PhoneGap v3.3.0 will look like:
PhoneGap v3.3.0 uses the “Plugman” plugin manager.
2. What you’ll need
To keep things small and simple we’ll focus solely on developing on Linux for an app that we’ll
make for Android. Though one huge benefit of PhoneGap is that you can package the same(ish)
code into a working app for many different mobile platforms. We also won’t be using any third-
party JavaScript or CSS libraries, though these will be useful to you going forward with your
app development. What you’ll need:
• A Linux desktop box
• PhoneGap (which requires NodeJS) on the above box - at time of writing this tutorial I
was using version 3.1.0. Don’t worry, we’ll install this in the Getting started chapter
• As many Android devices as you can get your hands on! At least one
• Google’s “Android Developer Tools” bundle - or at least Eclipse with the Android plugins.
Again we’ll cover this in the Getting started chapter
• At least a lower-intermediate knowledge of HTML5, JavaScript and CSS
• To not be terrified of the Linux command line!
3. What is PhoneGap
PhoneGap is a way to make apps for mobile devices using standard website frontend technolo-
gies. Namely HTML5, JavaScript and CSS. PhoneGap is free and open source. PhoneGap apps
aren’t true or native apps, but rather they are apps that open up a “WebView” on you mobile
device - essentially a web browser in fullscreen mode without title bars or bezels - running your
frontend code. It’s not a million miles away from a desktop browser running in fullscreen mode
(usually accessed by pressing F11). Implemented well, this non-nativeness isn’t necessarily a bad
thing.
..
PhoneGap versus Cordova
You’ve probably come across the term “Cordova” in your research for PhoneGap. PhoneGap
and Cordova are very closely related, and so it’s worth explaining the difference. There’s a lot
of back-story here which I’ll skip, but in a nutshell:
PhoneGap is a software product by Adobe Systems Inc. It is a branded and maintained
distribution of:
Cordova, which is a free and open source project maintained by the Apache Software
Foundation (ASF).
At the time of writing, PhoneGap adds a cloud build service to basic Cordova. This changes
the command line for PhoneGap (versus Cordova) somewhat, though you should be able to -
in theory - follow this tutorial using plain vanilla Cordova instead of PhoneGap. I also noticed,
annoyingly, that a lot of PhoneGap documentation simply points to Cordova documentation
which can mean that the command line syntax is wrong.
4. Getting started
There are two routes we can go down to get started with PhoneGap development. Both routes
require the Android SDK to be installed so let’s do that first. The easiest way to install the Android
SDK is to install the Android Developer Tools (or ADT) bundle. This bundle installs the Android
SDK and Eclipse IDE configured for Android (native) development.
Right, let’s install the Android Developer Tools. The easy peasy way is to download and install
the “ADT Bundle for Linux” from http://developer.android.com/sdk/index.html which should be
worry free.
If you’re already using Eclipse IDE, you can simply download the Android Developer Tools
plugin for it at http://developer.android.com/tools/index.html
..
About IDEs
You aren’t forced to use Eclipse IDE for Android development, though it does make a lot of
things easier as it supports direct deploy to an actual Android device and it has a virtual device
manager for deploying to emulated Android devices.
Myself, I didn’t like the way that Eclipse was opening - and highlighting - the various
frontend source files for the app (though I don’t doubt that this is configurable in the options
somewhere!). There’s also the fact that it doesn’t speak PhoneGap. I found myself cutting the
code in NetBeans IDE and checking in with Eclipse every now and again to deploy to the actual
device (Ctrl-F11) or to check console.log() messages in LogCat.
Netbeans IDE v7.4 dropped just before I finished this tutorial and interestingly that seems to
have PhoneGap (well, Cordova) support built in! Definitely worth a look.
Bizarrely, I found that regardless of the IDE used, I often had to deploy to the device twice
in order to have it truly updated. This happened whenever a resource file was updated, ie.
JavaScript or HTML or CSS. I notice this doesn’t happen when Java sources are edited which
indicates some kind of caching issue. I still haven’t got to the bottom of this particular mystery.
4.1 The cool new way
OK, now we can install PhoneGap itself. For some strange reason that I can’t figure out (I’m
guessing it’s just for package management) it requires NodeJS so go to http://nodejs.org and
install it. Then, as we see at http://phonegap.com/install we simply do (on the command line):
you@yours$ somewhere]$ sudo npm install -g phonegap
Getting started 6
This installs the PhoneGap binaries and commands globally on our system. After that, let’s
actually create the PhoneGap project where we’ll put all of our lovely code for the app. There
are two slightly different syntaxes for this:
you@yours$ somewhere]$ phonegap create --name "Japxlate" --id "com.drappenheimer.japxla
te" japxlate
or
you@yours$ somewhere]$ phonegap create japxlate com.drappenheimer.japxlate "Japxlate"
This will create a PhoneGap project folder structure for building the same code to many
different device targets (Android or iOS etc). "Japxlate" is the name of our app (in quotes).
com.drappenheimer.japxlate is our app’s reverse domain name identifier. All Android apps
have a unique identifier like this. japxlate is our desired folder name for the project. We then
want to do:
you@yours$ somewhere]$ cd japxlate
you@yours$ japxlate]$ phonegap run android
Which will detect your Android SDK and try to run the app on the currently connected device
(or configured virtual machine). If no Android SDK is found or present, it will try to deploy the
app to your account on the PhoneGap remote cloud build environment - which is just out of
beta at time of writing. But you’ll more than likely need an extra bit of setup to get this run
android command to work. Specifically you’ll need to add a couple of folders from the Android
SDK install to your PATH. The gory details are at http://docs.phonegap.com/en/edge/guide_-
platforms_android_index.md.html#Android%20Platform%20Guide, but how I did it was to add
the following lines to my ∼/.bashrc file:
export ANDROID_SDK_HOME=/wherever/you/installed/it/adt-bundle-linux-x86_64-20130729/sdk
export PATH=${PATH}:${ANDROID_SDK_HOME}/platform-tools:${ANDROID_SDK_HOME}/tools
As well as this I personally needed the Java development libraries to be installed.
If the run android command still doesn’t work after all this configuration, double check your
Android SDK Manager which you can reach from the Eclipse IDE.
Note that this run command is a shortcut for the build followed by install commands. If you
don’t want to actually run your PhoneGap app from the command line, you need to at least build
it which is like this:
you@yours$ japxlate]$ phonegap build android
This will create a PROJECTROOT/platforms/android folder with skeleton source files for our app
in it. And importantly the project files for this to be pickupable as an Android project in Eclipse
IDE.
Getting started 7
..
How many mobile platforms does it take to
change a lightbulb?
You might be wondering now, if PhoneGap is supposed to be this amazing tool that lets us
write the same app code for multiple mobile platforms, why would we want to dive straight in
to the /platforms/android folder? How is that going to work on, say, iOS?
The answer is simple, PhoneGap is indeed a tool where the same app code can be compiled
for multiple mobile platforms, but - in a nutshell - we are cheating and taking a shortcut! This
tutorial is rather simplified and focuses solely on Android. This is why we dive right in at
/platforms/android.
If your app needs to work on multiple mobile platforms - as most apps do - then you should
really create your app’s code in PROJECTROOT/www, specifying any platform-specific customisa-
tions in PROJECTROOT/merges, then debug each time for your platforms with the build, install
and run commands. The excellent blog post at http://devgirl.org/2013/09/05/phonegap-3-0-
stuff-you-should-know/ explains this very well.
Like the run command, the build command will also fallback to the remote cloud build
environment. You can disable this fallback with the command phonegap local build android.
Right, so now you’ve at least built your app on the command line. You might even have run it
from the command line! Going forward with this tutorial, let’s plug the skeleton code we’ve just
built into our Eclipse IDE as an Android project. Follow these steps:
1. Click File ⇒ New ⇒ Project
2. Select Android ⇒ Android Project from Existing Code (note there’s also a sample native
project in there!)
3. Browse to PROJECTROOT/platforms/android folder (actually just PROJECTROOT seems to
also work)
4. Click OK
5. You’ll get an “Import Projects” dialogue now with the project details that you can confirm
/ change and then click Finish
..
Keeping your PhoneGap up-to-date
Installing PhoneGap via NodeJS has the nice advantage that you can keep your PhoneGap
version up-to-date by running this command:
you@yours$ somewhere]$ sudo npm update -g phonegap
Getting started 8
4.2 The fiddly older way
An older way of getting started (that PhoneGap up to v2.1.0 used) still works and can be useful
if you are struggling with the configuration steps details in the above section. You’ll still need
to have Eclipse with the ADT installed first, but you won’t have to fiddle around with installing
NodeJS or altering PATH environment variables.
Simply download - rather than install - the relevant “archive” version of PhoneGap from
http://phonegap.com/install, and then you can follow the steps from “Setup New Project” in
the PhoneGap documentation. Please note that these instructions are for older versions of
PhoneGap and Eclipse and so your mileage with the latest versions may vary.
This page on the Adobe website is also a useful reference.
Sorry but I can’t specify exactly how to do it this way as it is not the supported way any more.
It might stop working for future versions of PhoneGap. Though I could get it working - with a
few tweaks - with PhoneGap v3.1.0.
Advantages of this method: You don’t have to install PhoneGap or NodeJS or any dependencies.
Disadvantages of this method: You don’t get PhoneGap’s latest template for setting up an Android
app and you have to do it manually (ie. updating the manifest etc).
5. Quick run-through of the default
app
Our app starts life as the PhoneGap “Hello world” app (unless you went The fiddly older way in
which case it’s empty). This is a good starting point and has some things we can build on and
learn from. Of course we’ll need to ditch a lot of it as well!
Go ahead, hit CTRL-F11 in Eclipse to run the app on your virtual or actual device. We get a little
robot icon and a pulsing (via CSS3) “device is ready” message. Rotate your device, it redraws
itself accordingly and changes the layout slightly if needed. It also doesn’t present or allow any
kind of scrolling or pinching which is A Good Thing for most apps - including Japxlate.
Figure 1. The default PhoneGap app (landscape)
The files that we’ll be wanting to edit (CSS, HTML5, JavaScript) to make our own app can be
found in the assets/www folder of our Eclipse project.
Let’s take a look at the generated assets/www/index.html (Apache licence text removed for
brevity):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="format-detection" content="telephone=no" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale
=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device
-dpi" />
<link rel="stylesheet" type="text/css" href="css/index.css" />
<title>Hello World</title>
</head>
<body>
Quick run-through of the default app 10
<div class="app">
<h1>PhoneGap</h1>
<div id="deviceready" class="blink">
<p class="event listening">Connecting to Device</p>
<p class="event received">Device is Ready</p>
</div>
</div>
<script type="text/javascript" src="phonegap.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
</body>
</html>
PhoneGap v3.3.0 adds a comment talking about a workaround for iOS 7.
We’ve got the simplified “html” DOCTYPE for HTML5. We explicity set a charset of utf-8
Unicode which is clearly going to be very important for this app! We’ve got a lot of “viewport”
settings which are mostly self-explanatory, but essentially say “this app fills the device display,
defaults to 100% zoom and can not be zoomed in or out”. This is really going to help our PhoneGap
app look and feel more like a native app and not a web browser view.
We then link to some CSS which we’ll look at shortly. The <title> needs updating, but this
won’t normally be visible to the app user anyway. Especially as PhoneGap build puts a theme
setting of Theme.Black.NoTitleBar in AndroidManifest.xml.
Then the <body> starts and we have whatever markup the app needs. Just before the <body>
closes, we have links to some JavaScript (this is debated but considered to be something of a
performance improvement). phonegap.js (in assets/www) is the PhoneGap library and is how
we can access phone hardware (ie. camera) from JavaScript in our PhoneGap app. Commenting
out this file will enable you to somewhat preview the app just by opening the index.html file in
Chrome desktop browser. We’ll talk about this later.
js/index.js is JavaScript specifically for this app. We then call app.initialize(). The app
object is in index.js which we’ll look at after taking a quick peek at the key things in the CSS
file we mentioned a moment ago (Apache licence text removed for brevity):
Quick run-through of the default app 11
* {
-webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adj
ust last value opacity 0 to 1.0 */
}
body {
-webkit-touch-callout: none; /* prevent callout to copy image, etc w
hen tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to
fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change
'none' to 'text' */
background-color:#E4E4E4;
background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%);
background-image:-webkit-gradient(
linear,
left top,
left bottom,
color-stop(0, #A7A7A7),
color-stop(0.51, #E4E4E4)
);
background-attachment:fixed;
font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif;
font-size:12px;
height:100%;
margin:0px;
padding:0px;
text-transform:uppercase;
width:100%;
}
/* Portrait layout (default) */
.app {
background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */
position:absolute; /* position in the center of the screen */
left:50%;
top:50%;
height:50px; /* text area height */
width:225px; /* text area width */
text-align:center;
padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped
with text) */
margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text ar
ea height */
/* offset horizontal: half of text area width */
}
Quick run-through of the default app 12
/* Landscape layout (with min-width) */
@media screen and (min-aspect-ratio: 1/1) and (min-width:400px) {
.app {
background-position:left center;
padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = ima
ge height */
margin:-90px 0px 0px -198px; /* offset vertical: half of image height */
/* offset horizontal: half of image width and tex
t area width */
}
}
.
.
The clause for * simply removes, from any element that we might make tappable, the default
sickly orange highlight that Android WebView gives to links and buttons and things.
The body clause starts by disabling some default Android WebView interations. This makes our
PhoneGap app feel a bit more nativey.
Then we set a grey gradient as the background.
Then we set the font type and size (12px). Height and width are both set to 100% which makes
our <body> fill the size of the WebView screen. We specify no margin (which is gap space outside
the <body>) and no padding (which is gap space inside the <body>).
In .app - our top level div in the markup - we set the layout of our app specific things. Portrait
orientation is assumed - a safe assumption for most phone apps. I won’t bore you with this too
much (but if you are baffled then please see a CSS refresher) other than to say it pulls some
strings with absolute positioning and negative margins to centre a background image and some
text.
Then we have another .app block wrapped in what’s called a media query
(http://cssmediaqueries.com/what-are-css-media-queries.html is a useful introduction) which
triggers when the phone is rotated into landscape view. It moves the background image to the
left of the text and also moves the text such that things are still centred.
Right, let’s get back to that js/index.js file that we’ve almost forgotten about! (Apache licence
text removed for brevity):
var app = {
// Application Constructor
initialize: function() {
this.bindEvents();
},
// Bind Event Listeners
//
// Bind any events that are required on startup. Common events are:
// 'load', 'deviceready', 'offline', and 'online'.
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
Quick run-through of the default app 13
},
// deviceready Event Handler
//
// The scope of 'this' is the event. In order to call the 'receivedEvent'
// function, we must explicity call 'app.receivedEvent(...);'
onDeviceReady: function() {
app.receivedEvent('deviceready');
},
// Update DOM on a Received Event
receivedEvent: function(id) {
var parentElement = document.getElementById(id);
var listeningElement = parentElement.querySelector('.listening');
var receivedElement = parentElement.querySelector('.received');
listeningElement.setAttribute('style', 'display:none;');
receivedElement.setAttribute('style', 'display:block;');
console.log('Received Event: ' + id);
}
};
All we have is one object called app which represents - wait for it! - our PhoneGap app.
initialize() is the constructor. We call this directly from index.html if you remember.
initialize() simply calls app.bindEvents() which in turn uses a DOM standard way of adding
an event listener. The event we listen for here is ‘deviceready’ which is fired from the PhoneGap
library when our Android device is, well, ready. We specify that this event is to be handled by
app.onDeviceReady() which simply calls app.receivedEvent('deviceready').
app.receivedEvent('deviceready') simply hides the “connecting” message and displays the
“ready” message (which are displayed and hidden, respectively, via the default index.css).
someElement.querySelector() is very interesting here and we’ll look at that later.
console.log(someMessage) is worth talking about now because we are going to be hammering it
during development! Basically this logs something to the browser’s console without disturbing
the user. When running your app via Eclipse’s F11, console.log() messages that fire on the
device will show up in your Eclipse’s “LogCat” thus:
Quick run-through of the default app 14
Figure 2. console.log() messages as appearing in Eclipse’s LogCat
Or, if debugging in Chrome desktop, you can see it by pressing F12 on the page in question then
clicking the console tab:
Figure 3. console.log() messages as appearing in Chrome desktop’s debugger
console.log() (and there are actually some other methods) is a general JavaScript development
technique that isn’t specific to mobile development. It works on all major browsers (though IE
needs help!).
6. First things first: The layout
Japxlate is going to have a single screen or “intent”. It won’t jump out to, for example, your
phone’s camera intent or “share to” list. The single screen is going to have three tab options -
Search, Discover and Write. We want the tab navigation and current tab content to all fit on the
device display without scrolling. OK, the PhoneGap Hello World app we just looked at is a good
start, but let’s see what tweaks we can do.
The Japxlate app is a spinoff of the @japxlate Twitter channel, so let’s look at that to get some
design ideas:
Figure 4. The @japxlate Twitter channel
OK, so we’ve got a greyish background. The logo is a red ‘J’ on a white background. The red
is our signature red and is actually #990000. The red ‘J’ on a white background is going to be a
good launcher icon for our app which we’ll talk about in a later chapter.
Right, so we need three tabs and we have some colour ideas. Here’s a quick wireframe:
First things first: The layout 16
Figure 5. Quick wireframe of the Japxlate app layout
Let’s put our tabs at the top so they’re out of the way of our device’s core Android buttons (back,
home, menu / special). Let’s have a little footer and see if we need that. The footer and header
have grey backgrounds. The tab content area is bog-standard black text on a white background.
When a tab is tapped, the header and footer will stay the same (though possibly with some kind
of current tab highlight) but the content area will load the appropriate content for that tab.
HTML5 gives us <header> and <footer> elements, so let’s try those. Change the <body> in
index.html to look like:
<body>
<header>
header
</header>
<div class="japxlate_app"> <!--note we've changed the class name-->
content area
</div>
<footer>
footer
</footer>
<!--<script type="text/javascript" src="phonegap.js"></script>-->
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
</body>
Fire this up on your device (or desktop Chrome) and it looks like this:
First things first: The layout 17
Figure 6. Unstyled <header> and <footer>
Not quite what we had in mind! The <header> and <footer> are both 100% wide which is great,
but we need to give them positions and heights (with tab content taking up the remaining space
inbetween). Also let’s get rid of the PhoneGap background gradient and put our own background
colours in. Also let’s take out the forced uppercase. Change the body clause in index.css to look
like this:
body {
-webkit-touch-callout: none; /* prevent callout to copy image, etc w
hen tap to hold */
-webkit-text-size-adjust: none; /* prevent webkit from resizing text to
fit */
-webkit-user-select: none; /* prevent copy paste, to allow, change
'none' to 'text' */
font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif;
font-size:12px;
height:100%;
margin:0px;
padding:0px;
width:100%;
}
Then add a clause for header like this:
First things first: The layout 18
header {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:40px;
line-height:40px; /*height of a *text* line*/
}
Then add a clause for footer like this:
footer {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:20px;
line-height:20px;
}
Running this looks like:
Figure 7. <footer> is too high
Hmm, the footer isn’t at the bottom! Let’s position it absolutely and make it flush with the bottom
of its parent (the document body). Add to the footer rule so that it looks like:
First things first: The layout 19
footer {
background-color:#555; /*medium grey*/
color:#ccc; /*slightly greyish white*/
height:20px;
line-height:20px;
position:absolute;
bottom:0;
width:100%; /*no default width for position:absolute*/
}
Running this looks like:
Figure 8. <footer> flush with bottom of document body
Great! Now let’s put our three tabs into the header. We’ll do it as an unordered list of links. Make
<header> of index.html look like this:
<header>
<ul id="tab-bar">
<li >
<a href="#search">Search</a>
</li>
<li >
<a href="#discover">Discover</a>
</li>
<li>
<a href="#write">Write</a>
</li>
</ul>
</header>
Running this looks like:
First things first: The layout 20
Figure 9. First attempt at tabs
Clearly a disaster! We need some styling to line up the list items horizontally in the header. Add
the following three clauses to the CSS file:
/*entire tab row*/
#tab-bar {
/*clear any inside and outside gap space*/
margin:0;
padding:0;
}
/*each tab*/
#tab-bar li {
display: inline; /*prevent each item from newlining*/
float:left; /*stack left*/
width: 33.3333%; /*have a third of total tab-bar space*/
}
/*tappable link in each tab*/
#tab-bar li a {
color: #ccc;
display: block; /*make "width-having"*/
font-weight: bold;
overflow: hidden; /*so long link text words get cropped*/
text-align: center;
text-decoration: none; /*remove default link underline*/
}
Running this looks like:
First things first: The layout 21
Figure 10. Tabs line up horizontally
Looking good! But the tabs need a few more things to look more useful. Namely, horizontal
dividers, icons and some kind of current tab highlight. For the horizontal dividers, let’s try giving
the second and third tabs a left border. CSS version 2 (the latest version being 3) has a nifty
selector where we can say “element type Y only where it follows an element type X”. With this
we can target any tab after the first one and apply a left border. Add the following clause to the
CSS:
/*a border-left for the middle and rightmost tab*/
#tab-bar li + li
{
border-left:1px solid #aaa; /*light grey*/
}
Running this looks like:
Figure 11. <header> too wide for document body
First things first: The layout 22
Ouch, that’s not a good look. What’s happened here is that the border has added 1px to the total
width of the second and third tabs. These tabs are now wider than a 3rd of the <header> row
and so the last tab gets bumped onto the next line. This is A Very Annoying Thing. One cheesy
little workaround for this is to use a simple background image to simulate the border. Make a 1
pixel wide by 16 pixel tall image in GIMP (or what-have-you) and floodfill with #aaaaaa which
is a very light grey. Export to a PNG image in assests/www/img called aaaaaa_16_v.png. Then
change the previously added CSS clause to look like this:
/*simulate a border-left for the middle and rightmost tab*/
#tab-bar li + li
{
background-image:url(../img/aaaaaa_16_v.png);
background-repeat:repeat-y;
background-position:left;
}
Running this looks like:
Figure 12. <header> fits nicely
Pretty good! OK, we’ll do the icons next. We want each tab to have a little icon on it. There are
millions of icon sets floating around these days. They tend to be one of three types:
• Always free
• Free only for personal use (else you should pay)
• Always paid-for
There’s also new school flat icons versus traditional deep icons. Design memes come and go but
we’ll go with something a little flat. We’ll use these rather nice ones which are royalty-free, free
for personal and commercial use:
http://www.graphicsfuel.com/2013/04/20-flat-icons-psd
Note that these icons are in PNG format which is a raster format. Raster icons are easy to use,
but can only be shrunk or enlarged by extracting or guessing information (respectively). This
means they only really look good at their native size which means that, depending on the pixel
First things first: The layout 23
density of the device display, they might be too tiny and hard to make out or really massive and
Legoish. But we’ll use them for simplicity.
One alternative would be to use a vector format - such as SVG - for the icons which stores the
image such that it can be scaled up or down without losing information. Another new trend is to
have the browser load something called an icon font. This is like a normal font but where each
character is an icon (remember Wingdings?!). This has the advantage that the icons are sizeable
just like any other text. Also they can be bolded or italicised. But they can only be of one colour.
Go ahead and put all of the PNG icons in assets/www/img (though we won’t use all of them).
Let’s reference some of these icons in our tab markup, change <header> in index.html to look
like this:
<header>
<ul id="tab-bar">
<li>
<a href="#search"><img src="img/search.png"> Search</a>
</li>
<li>
<a href="#discover"><img src="img/chat-bubble.png"> Discover</a>
</li>
<li>
<a href="#write"><img src="img/file.png"> Write</a>
</li>
</ul>
</header>
Note the space after the image and before the link text. Running this gives:
Figure 13. Icons we sourced are way too big
Woah, those icons are pretty big eh? The icons are a mix of square, tall or wide, but they all have
a biggest side of about 128 pixels. That’s clearly way too big for us here. Let’s use GIMP to resize
search.png, chat-bubble.png and file.png to have a biggest side of 16px - the same as our app
font size (in index.css) [NOTETOSELF double check this]. So go ahead and make those changes
and overwrite the original icon files. While you’re at it, do the same for paste.png because we’ll
be using that later on. (Feel free to trash the other icon files from assets/www/img as we won’t
First things first: The layout 24
be needing them in this little app.) Those scalable icon formats are looking real attractive now
huh?
After changing the icon sizes, it looks like this:
Figure 14. Icons at correct size
Not bad at all. But hmmmm, don’t you think the icons look a little out of whack? Like they’re
slightly higher than the line of text? We can remedy this by adding to the CSS:
a img {
vertical-align:middle; /*make more sensible relative to text baseline*/
}
(Yes, we could do these icons as CSS background images but what the heck.) That’s better. We
restrict this only to images in <a>’s so we don’t screw up any other images we might have in the
markup.
All we need now is a highlight for the currently selected tab, and while we’re at it we should
choose our default tab that we want to be displayed first on app load. Let’s plump for the Search
tab. Add a class name of “current” to the Search tab thus:
<ul id="tab-bar">
<li class="current">
<a href="#search"><img src="img/search.png"> Search</a>
</li>
.
.
</ul>
Then, in the CSS, modify #tab-bar li{} and add #tab-bar li.current{} thus:
First things first: The layout 25
/*each tab*/
#tab-bar li {
display: inline;
float:left;
width: 33.3333%;
border-bottom:3px solid #555; /*same bg as header*/
}
/*current tab*/
#tab-bar li.current {
border-bottom:3px solid #990000; /*signature red*/
}
We simply add a bottom border, in our signature red, to any tab bar list item that has a class of
“current”. We also add a border of the same size but using the header’s background colour to non
current tabs. This keeps everything looking flush horizontally. Later on (soon actually!) we will
use JavaScript to detect tap events on the tabs and change the current tab. Running what you
have so far looks like:
Figure 15. Current tab highlight
Pretty good! Only two little things are bugging us now. The content area text starts a little too
close to the tab bar, and, thinking about it this app doesn’t really need a footer at all! Change the
HTML footer to simply look like this:
<footer></footer>
Then add .japxlate_app{} to the CSS and also change the height of footer{} thus:
.japxlate_app {
padding-top:1em; /*move content away from tab bar*/
}
footer {
background-color:#555;
color:#ccc;
height:2px; /*down to 2px from 20px*/
line-height:20px; /*no longer meaningful...*/
First things first: The layout 26
position:absolute;
bottom:0;
width:100%;
}
Running this looks like:
Figure 16. Final app layout
Which we’ll stick with for the rest of the tutorial - and app! We have a 2px footer which is a bit
gimmicky, but will help us a bit with scroll debugging a bit later on. The tab content text is now
one newline(ish) down from the tab bar.
..
To fullscreen or not?
You might have noticed by now that the default PhoneGap app, and our own app’s layout that
we’ve just finished, fill the entire screen of the device. Even the Android status bar (which
shows the time, battery charge and signal strength etc) is obliterated.
Game apps tend to fill the entire screen, but almost every utility app out there leaves the
status bar. The good news is that we can get the status bar back quite easily by opening
PROJECTROOT/platforms/android/res/xml/config.xml and changing:
<preference name="fullscreen" value="true" />
to
<preference name="fullscreen" value="false" />
and then re-running the app.
You can choose which style you like and the rest of this tutorial is valid either way. Note that
figures showing device screenshots won’t have the status bar.
7. First things first: The tabbing
mechanism
The layout is in the bag now, but we need a mechanism to markup the content for our three
different tabs and a way for taps on the tabs to trigger the display of the relevant content.
We can markup the content for all three tabs in the HTML file and simply have Discover and
Write hidden (Search is our default remember) with CSS when the app first starts. Let’s do this
first before we look at any JavaScript. Edit <div class="japxlate_app"> in index.html so that
it’s contents are like this:
<div class="japxlate_app">
<div id="tab-content">
<div id="search" class="current">
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
search tab content. search tab content. search tab content.
</div>
<div id="discover">
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
discover tab content. discover tab content. discover tab content.
</div>
<div id="write">
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
write tab content.write tab content. write tab content.
</div>
</div>
</div>
Then let’s default to hidden, but with class="current" being visible, for these <div>s in
#tab-content. Add the following two clauses to index.css:
First things first: The tabbing mechanism 28
#tab-content > div.current {
display:block;
}
#tab-content > div {
display:none;
}
Hmm, well running this looks like:
Figure 17. Tab content spills over the footer
Search tab is indeed the only visible tab, but if there is a lot of content then it overflows and goes
past the footer! This will cause our PhoneGap app to be swipe scrollable which is a bad thing!
To fix this, let’s see what the .japxlate_app master container <div> is doing in relation to the
footer when it has both little and lots of content. For that let’s add this cheeky little debug to the
.japxlate_app{} CSS:
.japxlate_app {
padding-top:1em;
border:1px solid green; /*debug*/
}
This puts a thin green border around the entire div. This is a useful debugging tool but note that
it will add two pixels to the width and two pixels to the height of the div it is applied to. This
may make scrollbars appear where usually you wouldn’t have scrollbars.
Running with both large and small amounts of content looks like this:
First things first: The tabbing mechanism 29
Figure 18. Size of .japxlate_app div with large (left) and small (right) content amounts
So it looks like our master container div doesn’t have a fixed height and is as tall as it needs to be
for its content. We want it to be exactly tall enough to fit perfectly under the header and above
the footer. Then, if content is lots and it overspills, it will clip above the footer and won’t screw
up our app’s look and feel. We may then choose to handle content scrolling manually.
Our .japxlate_app master container div has the same parent as the header and footer (ie.
<body>) so we should be able to position it absolutely, tinker with CSS top and bottom properties
and “slot” it in between the header and footer. Let’s change the CSS for .japxlate_app to look
like this:
.japxlate_app {
padding-top:1em;
border:1px solid green;
overflow:auto; /*scrolling functionality *IF* we need it*/
position:absolute;
top:43px; /*flush with bottom of header*/
bottom:2px; /*flush with top of footer*/
width:100%;
}
Note that we’re keeping the debug green border for the moment. Running the app now looks
like this:
First things first: The tabbing mechanism 30
Figure 19. Improved .japxlate_app div with large (left) and small (right) content amounts
For the win! Notice how (on desktop Chrome only) we only get the scrollbar when we need it.
Notice also how it’s a scrollbar just for the content div and not a full scrollbar for the entire
document. This is great for our app because users won’t be able to whiz it around the screen like
a normal browser page. As we’ll see later though, we will annoyingly have to implement our
own scrolling for this content pane on the device. Go ahead and strip out that border:1px solid
green; statement for the .japxlate_app{} rule.
You must be exhausted with CSS things now (I know I am!), so let’s move on to the very last first
thing (say what?!) - which is the behaviour for the tab tapping which we’ll implement in good
ol’ JavaScript. We need to do two things here:
1. Detect a tap on a tab
2. Load / display content for that tab (hiding the previous tab’s content at the same time)
If you’ve been debugging the app in Chrome so far (I have!), here’s where we hit a tiny
stumbling block. If you remember our default index.js, all of the magic happens after we
catch the deviceready event. This is a PhoneGap event that desktop browsers won’t fire. An
advanced way to get around this would be to look at something like Stopgap (though, at the
time of writing, this is looking a bit tumbleweedy) or, more straightforwardly, some hacks
like at http://stackoverflow.com/questions/6687099/how-to-fire-deviceready-event-in-chrome-
browser-trying-to-debug-phonegap-projec.
What we want to do, for desktop browsers, is to not load phonegap.js. Then, instead of waiting
for the deviceready event to execute our x_y_z(), we simply call x_y_z() as soon as the browser
DOM is ready. Let’s use the solution by Chemik at the aforementioned StackOverflow page to
only load phonegap.js on condition of being on a mobile device. We can do this in index.html
thus:
First things first: The tabbing mechanism 31
.
.
<footer></footer>
<!--load phonegap.js only if on mobile device-->
<script type="text/javascript">
if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|IEMobile)/)) {
var line = '<script type="text/javascript" src="phonegap.js"' + '></'+'script>';
document.writeln(line);
}
</script>
.
.
Note that we break up the ending </script> in our string so that it isn’t picked up by the
(WebView) browser - or our IDE - as an actual ending script tag! This code will now only load
phonegap.js for mobile devices. You can test this by - carefully! - inserting a cheeky alert('I am
phonegap.js'); right at the top of phonegap.js. Don’t forget to remove this alert when you’ve
finished testing!
So now we only have phonegap.js loaded on an actual mobile device. This gives us a little tool to
help with the deviceready event problem. Edit bindEvents() and receivedEvent() in index.js
to look like this:
.
.
// Bind Event Listeners
//
// Bind any events that are required on startup. Common events are:
// 'load', 'deviceready', 'offline', and 'online'.
bindEvents: function() {
if (window.cordova) { //actual app
document.addEventListener('deviceready', this.onDeviceReady, false);
} else { //debugging in desktop browser
this.onDeviceReady();
}
},
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
},
.
.
If phonegap.js is loaded, it will define the window.cordova object which we can test for before
setting up our event listener. If phonegap.js is not loaded, we simply call what the listener calls
anyway. Running this in both desktop Chrome and your device should produce the eventual
console.log() message (you’ll see this via Eclipse’s LogCat if running on your device).
First things first: The tabbing mechanism 32
..
All about alerts (and PhoneGap API plugins)
Since we’re talking about debugging and JavaScript alert()s and things, let’s talk about how
we can use PhoneGap to produce more native-like alerts. JavaScript alerts will definitely give
your app that non-nativey, browser app feel. In fact, using alert() even on desktop sites is
considered a bit naff these days!
Conveniently, PhoneGap exposes a Notification API for “Visual, audible, and tactile device no-
tifications.” The documentation at http://docs.phonegap.com/en/3.1.0/cordova_notification_-
notification.md.html says we can use it like this:
First things first: The tabbing mechanism 33
..
navigator.notification.alert(message, alertCallback, [title], [buttonName]);
So let’s try that. Stick navigator.notification.alert('Some alert message', null); in the
receivedEvent() function that we were just tinkering with. Running this (which obviously
won’t work in desktop Chrome) gives a spurious error in LogCat:
Figure 20. Error when attempting navigator.notification.alert()
What’s going on? Well, it turns out that “As of version 3.0, Cordova implements device-level
APIs as plugins”. We have to install whichever APIs we want in our project. This removes
bloat as, previously, all APIs came pre-installed in every PhoneGap project. I actually found
this to be a bit mysterious and poorly documented (I found myself mashing up a mix of info
from Cordova docs and PhoneGap docs). But here’s how to add a particular plugin to your
PhoneGap project. Go to anywhere in your project folder structure on the command line and:
First things first: The tabbing mechanism 34
..
you@yours$ japxlate]$ phonegap local plugin add https://git-wip-us.apache.org/repos/asf
/cordova-plugin-dialogs.git
From PhoneGap v3.3.0 you can simply type phonegap local plugin add
org.apache.cordova.dialogs
Which should echo:
First things first: The tabbing mechanism 35
..
[phonegap] adding the plugin: https://git-wip-us.apache.org/repos/asf/cordova-plugin-di
alogs.git
[phonegap] successfully added the plugin
(Note that you won’t need to run this command, and you won’t get the above error, if you’ve
gone down The fiddly older way as that bundles all plugins into your project).
You’ll get the relevant URL from the docs for whichever plugin at the “API Reference” section at
http://docs.phonegap.com/en/3.1.0/ (PhoneGap has a good list of core and 3rd party plugins at
https://build.phonegap.com/plugins but the installation instructions for each one are seemingly
out-of-date and mention tinkering with XML config files which we don’t need to do after
running the above command.) The above command has downloaded the source for the plugin
and put it in /assets/www/plugins (in this case in org.apache.cordova.dialogs)
but diff on v3.3.0 etc.
It has also added references to the plugin in /assets/www/cordova_plugins.js - a file which
has been there from the start but just as a placeholder stub. The phonegap.js that we include
in our index.html actually also includes cordova_plugins.js so after running the above
command, we have all we need to start using navigator.notification.alert()! Try it again!
It works!:
First things first: The tabbing mechanism 36
..
Figure 21. Default navigator.notification.alert()
Great. But hmmm, it looks just the same as a normal JavaScript alert()! Currently it does
yes, but the advantage is that we can customise the title and button text. We can also specify a
callback function to trigger when the button is tapped. Try:
First things first: The tabbing mechanism 37
..
navigator.notification.alert('Some alert message', null, 'The title', 'Oki doki');
Running this looks like:
Figure 22. Customised navigator.notification.alert()
For the win! If you want to go forward with these customised alerts, keep in mind that they
won’t work on desktop Chrome so you may need to write a little wrapper function to still be
able to debug on desktop Chrome. The Japxlate app won’t be alerting anything to the user
on purpose - perhaps just some important error messages. Therefore we’ll go forward in this
tutorial with plain vanilla JavaScript alert()s. But I wanted to show you the general plugin
mechanism on what is no doubt one of the easier to use plugins. In fact, I’m not done yet!:
First things first: The tabbing mechanism 38
..
you@yours$ japxlate]$ phonegap local plugin list
[phonegap] org.apache.cordova.dialogs
This command lists all plugins installed in the current project.
you@yours$ japxlate]$ phonegap local plugin remove org.apache.cordova.dialogs
[phonegap] removing the plugin: org.apache.cordova.dialogs
[phonegap] successfully removed the plugin
This command removes the specified plugin from the current project. You specify the plugin
by its reverse-DNS identifier. You can find these out by issuing the above “list” command.
There are plugins to access the mobile device’s camera, accelerometer, phone contacts and
many more. Using these plugins is how we make a full fat mobile app and not just a simple
website-in-a-box.
PhoneGap v3.3.0 also has “Plugman” which is another way of working with plugins.
Plugman lets you add or remove plugins for one specific platform, whereas the above
method will add or remove plugins globally to any and all platforms used in the project.
Please see http://docs.phonegap.com/en/3.3.0/plugin_ref_plugman.md.html.
We’ve just been able to simulate our deviceready event on desktop Chrome for debugging and
we are ready to get our tab taps working. receivedEvent() in index.js is where the magic
happens because by the time we reach there, the device is ready (and the browser DOM is ready
as we’ve put JavaScript includes at the bottom of our HTML). But let’s not go down the route of
stuffing all of our JavaScript in index.js. Let’s go modular - right from the start. Create a new
JavaScript file called:
japxlate.js
in /assets/www/js
and include it from index.html thus:
<script type="text/javascript" src="js/japxlate.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
app.initialize();
</script>
Put a function called configureTabs() in the newly created japxlate.js thus:
First things first: The tabbing mechanism 39
//tab clickability
function configureTabs()
{
var tabs = document.querySelectorAll("#tab-bar li a");
for(var loop = 0; loop < tabs.length; loop++)
{
var tab = tabs.item(loop);
tab.addEventListener('click', function(event){alert(event + ' on ' + this);}, f
alse);
}
}
Then modify index.js to call this new function in receivedEvent() thus:
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
configureTabs();
},
Running this, and clicking on one of the tabs results in:
First things first: The tabbing mechanism 40
Figure 23. Debug alert() after clicking Discover tab
We are nearly there! We are detecting tab taps nicely! First let me explain some key points of the
configureTabs() function so far.
document.querySelectorAll("#tab-bar li a");
This is a great new piece of modern JavaScript that returns to us an array of DOM elements (a
“NodeList”) that match our CSS style selector. (The related querySelector() returns the first
matching element.) This is something that has found its way into W3C standard DOMJavaScript
based on something that jQuery has popularised (but not invented - Behaviour.js was one of the
first to do this).
Here we run querySelectorAll() on the document object so we are going to get all matches
contained in <body>. Usefully, it can also be run on an Element object - for example a certain table
or form - or a DocumentFragment element to only return matching elements in that particular
container element. #tab-bar li a is a CSS style query for “an <a> in a <li> in any element with
id ‘tab-bar’”.
We loop over all matching <a> elements and set a click handler using the DOM standard
addEventListener() (as formally described at
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration). Our event han-
dler in this case is a simple anonymous function giving a debug alert. In event handler functions,
First things first: The tabbing mechanism 41
an Event object is passed as a parameter and contains information about the particular event that
triggered the handler - screen x and y coordinates for mouse events and which key was pressed
for keyboard events and so on. In event handler functions, this refers to the element on which
the event happened.
Let’s replace the dummy click handler with something that we’ll actually want to use. But first,
remember that in the click handler function we only have the event object and the <a> object (as
this)? We’ll also need to know which content <div> relates to which <a>, then we can switch
the content accordingly. Modify the header of index.html to look like this:
<header>
<ul id="tab-bar">
<li class="current">
<a href="#search" data-div-id="search"><img src="img/search.png"> Search</a>
</li>
<li>
<a href="#discover" data-div-id="discover"><img src="img/chat-bubble.png"> 
Discover</a>
</li>
<li>
<a href="#write" data-div-id="write"><img src="img/file.png"> Write</a>
</li>
</ul>
</header>
HTML5 allows us to use custom or “data” attributes where we can add any attribute and value
we like to any particular element. The attribute names start with “data-“. Here we simply link
each <a> to its matching content <div> id. We’ll use this attribute (soon) in the click handler for
tabs.
OK, next strip out the dummy handler from addEventListener() and make it look like this:
tab.addEventListener('click', onclickForTab, false);
This will call the onclickForTab() function as a click handler. We define the onclickForTab()
function, in japxlate.js thus:
//set up and display a newly tapped tab
function onclickForTab(event)
{
//to prevent URL from changing and browse history building up
event.preventDefault();
//-------tab display logic---
var lastTab = document.querySelector('li.current a');
//NOP if clicking current tab again
if(lastTab == this)
First things first: The tabbing mechanism 42
{
return false;
}
lastTab.parentNode.className = ''; //undisplay
this.parentNode.className = 'current';
//---------------------------
//-----content div display logic---
var lastDiv = document.querySelector('div.current');
lastDiv.className = ''; //undisplay
var matchingDiv = this.getAttribute('data-div-id');
var thisDiv = document.getElementById(matchingDiv);
thisDiv.className = 'current';
//-----------
//get tab div id from tab link
var divId = this.getAttribute('data-div-id');
}
Let’s go through this code, which looks fiddly at first, but basically tinkers with CSS class names
such that things turn on and off as we want.
The first thing we do is the DOM standard preventDefault() which prevents the browser’s
default action for the event from triggering. The default browser action for clicking on a link is
to:
1. Change URL in address bar to that of link target
2. Add new URL to browsing history
3. Load new URL
As our links are simply triggers to load tabs and not proper links, we don’t want any of these
steps to happen. Step [2] is especially annoying. If we don’t call preventDefault() for our tab
taps, if we open our app and click on the tabs ten times, we will have to use the device’s BACK
button ten times to exit the app!
Next we use querySelector() to get the single current tab link. Because ‘this’ in our click handler
will be the clicked element, we can do a check to see if this is the same as the previous current
tab. And if so, do a “no operation” (NOP). We then manipulate classnames to activate only the
clicked tab.
Similarly, we use querySelector() to get the currently active content <div>. We activate the
content <div> for the clicked tab by retreiving data-div-id from the clicked <a> and using that
to get the correct div.
First things first: The tabbing mechanism 43
Anyway, this all works!
Figure 24. Initial configureTabs() is working well
Thinking deeper and keeping an open mind, there’s more to our tabs than just displaying the
relevant content. A given tab might have to do some one-off initialising of a resource - perhaps
a database. Or some per-load checking of, eg, network availability on the device. We also might
like to add new tabs in future as users request more features. We might simply just want to
change the default tab based on user complaints!
We can cover all of these bases with a few simple steps. First, alter the bottom of onclickForTab()
to look like:
.
.
//get tab div id from tab link
var divId = this.getAttribute('data-div-id');
onclickForNamedTab(divId);
}
onclickForTab() is a generic handler for any tab tap, but we are adding onclickForNamedTab()
to handle tab specific initialisation. Put onclickForNamedTab() in japxlate.js and it looks like
this:
First things first: The tabbing mechanism 44
//Do the one-off loading and everytime setup for whichever tab
function onclickForNamedTab(divId)
{
if(divId == 'discover')
{
onclickForTab_Discover();
}
else if(divId == 'search')
{
onclickForTab_Search();
}
else if(divId == 'write')
{
onclickForTab_Write();
}
}
We simply switch on the tab content <div> id, calling the appropriate onclickForTab_theTab().
Yes, you’ve guessed it, if you want to add more tabs to the app, you will have to update this
switch case (and add the corresponding onclickForTab_theNewTab()). This function is a simple
dispatcher to other functions that are going to do the actual one-off and per-load initialisations
for tabs.
For a “one-off” initialisation, we are going to have to somehow record which tabs have been
opened so far. We’ll do this using a global variable. Eek! Global variables are not current best
practice for JavaScript, but we’ll do it to keep this small and simple app, er, small and simple. Put
this at the top of japxlate.js:
//Has the first load of each tab happened yet?
var global_pagesLoaded = {discover:false, search:false, write:false};
We can then check - and set - these values in our onclickForTab_theTabName() functions that
our onclickForNamedTab() dispatcher calls. Let’s get started with the first of these functions for
our Discover tab. Put this in japxlate.js:
//One-off loading and each time setup for discover tab
function onclickForTab_Discover()
{
//console.log('click on discover tab');
if(!global_pagesLoaded.discover)
{
firstLoadForTab_Discover();
}
//each time setup to go here
}
First things first: The tabbing mechanism 45
We simply check if global_pagesLoaded.discover is false and if so call firstLoadForTab_-
Discover(). We also have a space here for any “each time” setup of the Discover tab. Go ahead
and create functions, using this one as a template, for the Search and Write tabs (do a copy paste
and then change ‘Discover’ to ‘Search’ and ‘discover’ to ‘search’ and etc). We’ll modify these
functions later if we need to.
OK, we still need firstLoadForTab_Discover() which will perform one-off initialisation for the
Discover tab. Do it like this, again in japxlate.js:
//One-off loading for discover tab
function firstLoadForTab_Discover()
{
//console.log('first load for discover tab');
global_pagesLoaded.discover = true;
//one-off setup to go here
}
All we do is set global_pagesLoaded.discover to true so that this function does not get
called again from onclickForTab_Discover() when the tab is tapped a subsequent time. At
the moment this is just a placeholder for whatever we might need down the line. Like we just
did for onclickForTab_*(), replicate this function for the Search and Write tabs.
If we temporarily uncomment the console.log() calls, running this - and clicking tabs randomly
- shows that we do indeed have a first load that fires only once and a click that fires each time.
Figure 25. One-off tab loading is confirmed
Done and dusted. Money in the bank. Move along, nothing to see here… right? Well there’s
just one thing missing. If you’ve really really been paying attention and thinking one or two
steps ahead perhaps, you may have noted that our setups (one-off and each time) for the default
Search tab are only fired if we click off that tab and then back on it. Clearly this is not useful
and whichever tab is set to be the default needs to have its setups run right off the bat. Let’s
solve this problem by, on deviceready, calling a little function to retreive the current tab
and calling our already existing onclickForNamedTab() dispatcher for that tab. Add a call to
initialiseDefaultTab() at the bottom of receivedEvent() in index.js so that it now looks
like this:
First things first: The tabbing mechanism 46
// Update DOM on a Received Event
receivedEvent: function(id) {
console.log('Received Event: ' + id);
configureTabs();
//load and show whatever we've set the initial tab to be
initialiseDefaultTab();
}
Then define initialiseDefaultTab() in japxlate.js thus:
//Load and show our default initial tab
function initialiseDefaultTab()
{
var defaultTab = document.querySelector('div.current');
var divId = defaultTab.id;
onclickForNamedTab(divId);
}
We use querySelector() to get whichever content <div> has been set as current in the HTML
markup. We could in theory select the tab that has been marked as current but, as that will be
in sync with the content div anyway, it is academic.
Congratulations, you have just built a working infrastructure for the Japxlate app! This is a good
starting point for any simple PhoneGap app.
8. The Search tab
8.1 Layout and interface
The Search tab - the first tab that the user will see when launching our app - is going to be
a search form for the user to search our Japanese dictionary. It will also display any and all
matching results in a scrollable area.
We’ll have a rule that the user’s search query can be in Japanese as well as English. Not only will
this increase the usefulness of our app, it will also enable a future “reversing” of the app to be
localised for Japanese speakers wanting to learn English vocabulary. Let’s have another rule that
they can type the Japanese or English query into the form in the same input box and without
having to fiddle with radio buttons or other such inputs (which are a bit old hat for search forms
anyway but especially cumbersome on mobile devices). With these rules and functionalities in
mind, a wireframe of the Search tab might look like:
Figure 26. Quick wireframe of the Search tab layout
OK, let’s markup - and then style - the search form and the results space for dictionary queries.
Mosey on down to http://www.ajaxload.info and make a “loading” spinner image (gif) for the
Search tab. I made mine use the Japxlate signature red (#990000) and a transparent background.
Download it and put it in /assets/www/img as spinner.gif.
Let’s markup the form and results space - in index.html - like this:
The Search tab 48
<div id="search" class="current">
<button type="button" id="search-button" style="float:right; width:45%; margin-righ
t:1%;">
<img src="img/search.png">
Search
<img id="button-spinner" src="img/spinner.gif" style="visibility:hidden;">
</button>
<input type="text" id="search-query" placeholder="Japanese or English" size="40"
style="width:45%; margin-left:1%;">
<br>
<span id="loading-text">
[Loading core dictionary. This takes a while the first time.
<img src="img/spinner.gif">]
</span>
<div id="results-wrapper">
<div id="search-results">
You can search by kanji, hiragana, katakana, English or romaji!
</div>
</div>
</div>
We float our search button right (which means that in the markup it has to come before things on
the same line that would be visually to the left of it) but make it 1% (of total width) away from the
edge for nice appearance. We reuse search.png as a button icon. We also include the spinner.gif
that we just created but default it to visibility:hidden. Why not just display:none? Because
with visibility:hidden, it is hidden but still takes up space in the layout flow. This means the
layout won’t “jump” when we make it appear. We’ll switch this image’s visibility on and off
programmatically.
Then we’ve got our text input which uses the new HTML5 placeholder attribute to present a
hint or instruction to the user about what kind of entry it expects. The text input is also 45% wide
with an edge spacing of 1%.
Why not just make both 50%? Because then they will touch in the middle which will end in tears
with big fingers on a small display!
We then have a “this will take a while” message and spinner that we will remove after one-off
setup is complete.
Finally we have a container for our search results - <div id=”search-results”> - which displays a
default search hint. We also have a wrapper for the search results container - <div id=”results-
wrapper”> - which is going to be the scroll viewport for search results. These two divs need the
following styles in index.css:
The Search tab 49
#results-wrapper {
position:static;
width:100%;
margin-top:1em; /*space one <br>(ish) from bottom of search form*/
overflow:hidden;
}
#search-results {
position:relative; /*we position this relative to its *normal* position*/
top:0; /*but set the normal top position anyway. We will*/
width:100%; /*change this top value to affect a scroll*/
}
The keypoint here is position:relative; on the search-results div which means that we will be
able to position it (ie. scroll it) relative to an unmoving parent - the results-wrapper div. Running
this looks like:
Figure 27. Initial appearance of the Search tab
Not bad. Just two grumbles here.
1. The height of the text area is lacking and also it’s shorter than the button. Let’s even these
two out. (Actually on my device the text input and the button don’t seem to be on the
same baseline!)
The Search tab 50
2. Icon for search is screwy again - let’s fix that like we fixed the tab icons.
In index.css, change the existing:
a img
{
vertical-align:middle; /*make more sensible relative to text baseline*/
}
to:
a img, button img
{
vertical-align:middle; /*make more sensible relative to text baseline*/
}
Which covers (2). To fix (1), add this to index.css:
input[type="text"], button {
height:30px;
margin:0;
}
Running looks like this:
The Search tab 51
Figure 28. Improved appearance of the Search tab
Better!
8.2 Creating the database
Now, let’s also have a rule to the effect of dictionary searches working even when the mobile
device is offline. That is to say the app must use some kind of local storage on the device itself or
the WebView browser. Well, it turns out that the Android WebView supports something called
Web SQL which is a small, local implementation of an SQL database (specifically SQLite) in the
browser. We can load our Japanese dictionary into a client-side database and, based on the user’s
search term, query it in whichever way we need to pull out matches.
..
Important note about Web SQL
Web SQL is an abandoned specification (see http://www.w3.org/TR/webdatabase/) that W3C
no longer maintain, and I do not recommend that you use it going forward in your owns apps!
W3C’s beef was that it was only being implemented using SQLite - obviously they aren’t in
the business of standardising a piece of vendor lock in! For similar reasons Mozilla (ie. Firefox
browser) have chosen not to implement it right from the start. I do kind of agree that bringing
a heavy server-side thing to the client is a bit of an odd move. In fact, traditional SQL on the
The Search tab 52
..
back-end is somewhat in crisis itself these days in the world of NoSQL datastores. Though it is
very useful for mobile apps that might not be online and need to work with some data.
Why are we using it for this tutorial?
Somewhat for historical reasons but also because I know it will be perfect for fuzzy text
searching. I know from experience that it will “just work”. When using PhoneGap we are lucky
too because “Cordova provides access to both interfaces (Web SQL and something else called
Web Storage) for the minority of devices that don’t already support them. Otherwise the built-
in implementations apply.”
What would be some alternatives?
Ignoring PhoneGap and the world of mobile apps, Indexed DB (a W3C standard at
http://www.w3.org/TR/IndexedDB/) looks to be picking up steam. Though caniuse.com tells
me that support is currently less than that of Web SQL. Also it hasn’t made its way into
PhoneGap at the time of writing. Indexed DB mirrors the more modern style of NoSQL
databases closely.
I hope that future versions of the app (and this tutorial) can use Indexed DB.
PhoneGap v3.3.0 now supports Indexed DB, but only if the underlying WebView
supports it. At the time of writing this means only Windows Phone 8 and BlackBerry
10.
PhoneGap’s (well actually Cordova’s) Web SQL docs are at
http://docs.phonegap.com/en/3.1.0/cordova_storage_storage.md.html As you can see, it’s a fairly
small implementation of an SQL database. But writing for it in JavaScript with callbacks was a
novelty for this grizzled MySQL hacker!
OK, let’s crack on now with Web SQL initialisation for the first load of the Search tab. Stick this
cheeky call - to a function we’re about to create - at the bottom of firstLoadForTab_Search()
in japxlate.js:
tryPopulateDB();
Let’s create this function, and other functions to do with general Web SQL setup, in a new file
in /assets/www/js called websql_core.js. Create this file, and the first function we’ll put in it
is the tryPopulateDB() we’ve just referenced. It will look like this:
The Search tab 53
//Open / create the "Japxlate" Web SQL database and - if it's not already
//present - create and populate the "edict" table
function tryPopulateDB()
{
//version 1.0, 4 megabytes
var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024);
db.transaction(checkDB); //only populate edict table if it not already exist
}
PRO TIP: The Cordova docs on Web SQL are going to be very useful to reference
when following this chapter. They are at http://docs.phonegap.com/en/3.1.0/cordova_-
storage_storage.md.html.
The same page for PhoneGap v3.3.0 removes the Web SQL reference, which to be
honest had at least one mistake in it, and instead points you to have a look at
http://www.html5rocks.com/en/features/storage.
We open a Web SQL database called Japxlate, at version 1.0, with a display name of “Japxlate DB”
and a size of 4 megabytes. I know from tinkering with the dictionary database for the @japxlate
Twitter channel that the core dictionary definitions will fit in 4 megabytes with a bit to spare.
Then we call transaction() on the returned database to run the query or queries in the
checkDB() function that we’re about to implement.
Now’s a good time to talk about the schema we’ll use for the dictionary table. We’ll call the table
“edict” as that’s the name of the Japanese dictionary that powers it
(at http://www.csse.monash.edu.au/∼jwb/wwwjdicinf.html#dicfil_tag) and the fields will be:
edict(id unique, kanji, kana, definition)
“id” will be an integer and a unique key to each record. “kanji” will hold the Chinese characters
that the word is written in. “kana” will hold the Japanese phonetic script that the word is written
in. Finally “definition” will hold one or more English language definitions for the word, separated
by ‘/’.
Our checkDB() function needs to know if the edict table exists and is full. If not, create it and fill
it.
The checkDB() function will receive a SQLTransaction object as a parameter from db.transaction().
Again in websql_core.js, make checkDB() look like this:
The Search tab 54
//Check if "edict" table exists and has records
function checkDB(tx)
{
//console.log('checkDB()');
tx.executeSql('SELECT COUNT(id) AS count FROM edict', [], successCheckDB, errorChec
kDB);
}
We call executeSql() on the received SQLTransaction object which needs at least an SQL query
as its first argument (and parameter values as the 2nd parameter if the query in the first argument
uses parameter binding), but can optionally take both a success and failure callback as 3rd and
4th parameter respectively. Here we run a very simple query to get the count of rows - by id
- in the edict table. This query will throw an error if the edict table does not exist (but not if
it exists and is empty which is a condition we will knowingly ignore for this simple app). We
don’t use parameter binding in this query so we provide an empty array as the 2nd parameter
simply because we need to “get” to the 3rd and 4th parameters. We specify an error and a success
callback. Should the query fail we can assume that the table does not exist and therefore needs
to be created and populated. Let’s look at the success callback first as it’s simpler and only has
to clear the “database loading” message:
//Callback for if checkDB() succeeds - ie. "edict" table present and full
//SO clear the "database loading" message
function successCheckDB(tx, results)
{
//console.log('edict already loaded');
document.getElementById('loading-text').innerHTML = '';
}
Pretty easy and not worth explaining other than to point out that the callback function receives
an SQLTransaction and an SQLResultSet object respectively.
Let’s get started on the error callback:
//Callback for if checkDB() fails - ie. no "edict" table
//SO create it and fill it
function errorCheckDB(transaction, error)
{
console.log('edict table not exist - will create and fill');
//here we need to do something to fill the table
}
This code so far will run without errors (but don’t forget include websql_core.js from
index.html (above the japxlate.js include)) but won’t do anything useful. It will get to the
“edict table not exist - will create and fill” log message and then stop. In the error callback, we
need to run another transaction on the Japxlate database which will load all the dictionary data
we need. Change errorCheckDB() to look like this:
The Search tab 55
//Callback for if checkDB() fails - ie. no "edict" table
//SO create it and fill it
function errorCheckDB(transaction, error)
{
console.log('edict table not exist - will create and fill');
//version 1.0, 4 megabytes
var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024);
db.transaction(populateDB, errorWebSQL, successPopulate);
}
We open the same Japxlate database and try to run the populateDB() queries on it. We have new
success and error callbacks. populateDB() looks like this:
//Create and fill the "edict" table
function populateDB(tx)
{
console.log('creating and filling edict table');
//DROP if present (ie. because it's present but empty)
tx.executeSql('DROP TABLE IF EXISTS edict');
//create
tx.executeSql('CREATE TABLE IF NOT EXISTS edict(id unique, kanji, kana, definition)
');
websqlEdictInserts(tx); //see websql_edict_inserts.js
}
We create the table according to our schema - DROPing it first just in case and so the
CREATE doesn’t fail. Finally we call websqlEdictInserts() which is a function we’ll put
in another JavaScript file. The websqlEdictInserts() function accepts an SQLTransaction
object and essentially runs a huge list of INSERT queries on it to populate our table. This
function isn’t very do-at-homeable because it’s basically a dump of the most common words
from the @japxlate Twitter feed’s database. If you are following this tutorial step by step,
please get the file /js/websql_edict_inserts.js from the app’s GitHub repository and stick
it in your /assets/www/js folder. To explain it a little bit more, here’s an excerpt from
/js/websql_edict_inserts.js:
The Search tab 56
function websqlEdictInserts(tx)
{
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(5,",,
/curry/rice and curry/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(21,,,
/to blow (one's nose)/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(119,,
,/1000 yen/)');
tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(138,,
,/ten percent/)');
.
.
}
Note that the ID numbers aren’t in sequence because these words are the most common 20,000
or so words from @japxlate’s Edict dictionary which has nearly 200,000 entries!
OK, that’s populateDB() in the bag. But don’t forget errorCheckDB()’s custom error and success
callbacks. Let’s do the error callback first:
//Generic SQLError handler (for both db.transaction() and tx.executeSQL())
function errorWebSQL(transactionOrError, errorOrNull)
{
var error = null;
if(typeof transactionOrError == 'SQLTransaction') { //from tx.executeSQL()
error = errorOrNull;
} else { //from db.transaction()
error = transactionOrError;
}
console.log(error); //error is now an SQLError object
alert(Error processing SQL:  + error.code);
}
Ouch! This looks a bit over-complicated. What’s going on? Well, I didn’t realise at first,
and I only discovered it on a hunch, but we can reference error callbacks from both the
database.transaction() and transaction.executeSQL() methods (as we are already doing) but in
each case they will receive different parameters! The PhoneGap / Cordova docs for the Web
SQL API - at the time of writing - don’t seem to realise this and actually are therefore incorrect.
The PhoneGap v3.3.0 docs remove the entire Web SQL reference section.
This is something of a generic error callback and so we pull some strings to handle both cases.
Error callbacks as called from database.transaction() will receive (SQLError), and error callbacks
called from transaction.executeSQL() will receive (SQLTransaction, SQLError).
The Search tab 57
We simply alert out the code property of the received SQLError object. This is going to be our
recyclable Web SQL error handler going forward with the app.
The success callback for errorCheckDB() is going to do the same as the success callback for
checkDB() (which is successCheckDB()):
//Callback for if errorCheckDB() succeeds - ie. edict table populated OK
function successPopulate()
{
console.log('finished loading edict');
document.getElementById('loading-text').innerHTML = '';
}
Include websql_edict_inserts.js from index.html (above the include for websql_core.js)
and we are ready to go for a spin!
On first run, the “database loading” message and spinner take a few seconds to disappear, and
the log messages indicate database loading success. It looks like this:
Figure 29. First run of app with Web SQL database loading
Go ahead and run the app again after exiting it, the 2nd time around feels kind of faster right?
Let’s check the logs:
The Search tab 58
Figure 30. Second, faster run of app with Web SQL database loading
Woah! That’s right, Web SQL databases that you’ve created persist over multiple sessions of the
app (or browser). Pretty hot and tasty! This is a great reason why Web SQL, as abandoned and
awkward as it is, is really useful for mobile WebView apps as it can be used for saving things
offline.
8.3 Querying the database
Right, so that’s the database created, the table created, and the table filled. Phew!
We’re coming to the meat and bones of it now which is getting results from the database based
on the user’s search query. This will involve a bit of work on the frontend interface and a lot
of work on the backend. As we are kind of frazzled with Web SQL things right now, let’s get to
work on the frontend interface first.
Let’s make a new JavaScript file in /assets/www/js called search_interface.js to hold
anything to do with the frontend look and feel of searching. Right, one of the main things we’ll
want to do is to put search results from the database into the container div in our markup. Let’s
add a function to do this:
The Search tab 59
//Put the matching search results (which could be zero matches) on the page
function putResultsOnPage(results)
{
//get search results div
var theDiv = document.getElementById('search-results');
//clear current content
theDiv.innerHTML = '';
//might be no matches
if(results.rows.length === 0)
{
theDiv.innerHTML = 'No matches found in the common words dictionary.
Tweet @japxlate yourAdvancedWord for advanced word definitions.';
buttonSpinnerVisible(false); //stop the loading spinner
return;
}
//some results so loop through and print
for(var loop = 0; loop  results.rows.length; loop++)
{
var item = results.rows.item(loop);
var var theRomaji = item.kana; //TODO
var formattedDefinition = format_slashes(item.definition);
var defText = item.kanji + ' / ' + item.kana + ' (' + theRomaji + ') / ' + form
attedDefinition;
defText = defText.replace(new RegExp(global_searchTerm, 'ig'), 'span style=co
lor:#990000;$/span');
var defLine = 'img src=img/j.png style=vertical-align:middle; ' + defText
+ 'hr';
//var defLine = 'p class=def-line ' + defText + '/p'; //had CSS styling i
ssues (mostly text overflow)
theDiv.innerHTML += defLine;
}
buttonSpinnerVisible(false); //stop the loading spinner
}
We’ll expect to be passed an SQLResultSet object which will come from a successful query
on our Web SQL database. First we reset the current (ie. old) results by setting the container
div’s innerHTML property to empty. We then cover a scenario of no matches by printing a
“no matches” message (with a plug for the @japxlate Twitter bot!). Note that you can split up
very long quoted strings in JavaScript by ending lines with a ‘’. We then stop the “searching”
spinner by calling the buttonSpinnerVisible() function with a parameter of false. We’ll write
The Search tab 60
this function shortly and it’s basically a way to switch the “searching” spinner on and off. We
then return.
..
document.getElementById('some-id') versus
document.querySelector('#some-id')
You may be wondering why, for single elements, I am using document.getElementById('some-id')
and not the new fangled document.querySelector('#some-id'). Well it’s true that these will
both return the same element, and it’s true that getElementById() is a much older piece
of XML DOM, but the issue - at the time of writing - is one of performance (and perhaps
getElementById() is a teeny tiny bit more readable). After some benchmarking experiments
in desktop Chrome (using the mega useful console.time() and console.timeEnd() as at
https://developers.google.com/chrome-developer-tools/docs/console-api#consoletimelabel) I saw
that, for single elements, getElementById() was much faster than querySelector(). Out of
curiosity I also tested jQuery’s $('#some-id) (which returns a jQuery-specific list of nodes)
and found this to be much slower than the browser’s native querySelector(). Of note is that
the new jQuery v2.0 was much faster than v1.x for the same selector (though still slower than
querySelector()).
Now, if we’re still in the function we’ll have some results. We loop over and retrieve the results
using the SQLResultSet object’s rows.length property and rows.item(itemIndex) method.
What we do in the loop looks fiddly, but all we are doing is replicating the style of definition
lines that @japxlate uses. If you remember the snippet of websql_edict_inserts.js that we
looked at earlier, the format of the “definition” field in the database is “/definition one/definition
two/definition three/”. We want to space these multiple definitions out a bit more and remove
the lead and tail slashes; for that we’ll use a helper function called format_slashes() which also
goes in this file:
//Clean up the EDICT definition line that we get from our Web SQL DB
//For example, /one/two/three/ -- one; two; three
function format_slashes(slashesString)
{
//remove leading and trailing '/' characters
var string = slashesString.replace(/^//, ''); //leading
var string = string.replace(//$/, ''); //trailing
//change remaining '/' characters to a semicolon with space
return string.replace(///g, '; ');
}
We use JavaScript’s core replace() method to change the slashes based on regex matching. We
replace single lead and tail slashes with an empty string. We replace globally (the ‘g’ modifier
after the regex) all remaining slashes with a semicolon followed by space. We return the modified
string.
The Search tab 61
OK, let’s come back to explaining putResultsOnPage(). We create in defText a nicely formatted
definition line. We use String.replace() on this definition line to highlight the user’s search
term in our trademark red. For this we use global_searchTerm which we’ll define a bit later on.
buttonSpinnerVisible() is a simple CSS style toggler that also goes in search_interface.js
and looks like this:
//Toggle for search button's loading spinner
function buttonSpinnerVisible(visible)
{
var spinner = document.getElementById('button-spinner');
if(visible)
{
spinner.style.visibility = 'visible';
}
else
{
spinner.style.visibility = 'hidden';
}
}
Remember in websql_core.js we did this a couple of times:
document.getElementById('loading-text').innerHTML = '';
As this is manipulating the search interface, let’s refactor this as a function in search_-
interface.js. Let’s call it clearLoadingMessage():
function clearLoadingMessage()
{
document.getElementById('loading-text').innerHTML = '';
}
Then replace the two document.getElementById('loading-text').innerHTML = ''; lines in
websql_core.js with calls to clearLoadingMessage();.
OK, in search_interface.js we now have all of the functions that other functions might call
to update the interface for database searching, but we are missing something here. The user! We
need to catch tap events on the Search button and then use their entered query to search the
database and return results. Let’s start where the user starts - the Search button. Let’s add a click
handler. Add a call to:
configureSearchButton();
at the bottom of receivedEvent() in good old index.js.
We define configureSearchButton() in search_interface.js:
The Search tab 62
//search button clickability
function configureSearchButton()
{
document.getElementById('search-button').addEventListener('click', onclickForSearch
Button, false);
}
We define onclickForSearchButton() as the click handler for the search button.
onclickForSearchButton(), also in search_interface.js, is like this:
//Perform a dictionary search for entered query
function onclickForSearchButton(event)
{
var q = document.getElementById('search-query').value;
//some kanji searches are going to be legitimately only one char
if(q.length  1)
{
return;
}
buttonSpinnerVisible(true);
var matches = doEdictQueryOn(q);
}
We get the user’s entered search query and - on the condition that it’s at least one character long
- we pass it to doEdictQueryOn() after displaying the “searching” spinner. doEdictQueryOn() is a
function that we haven’t written yet that will need a whole ‘nother JavaScript file. We’ve already
got quite a few JavaScript files, but this is keeping it nice and modular. Create websql_query.js
in /assets/www/js and add doEdictQueryOn() thus:
//function to query the database based on whatever query string
function doEdictQueryOn(newQ)
{
//TODO
}
What? It’s empty! Yes, we’re going to take a breather now and plan what we’re going to do next.
A keyboard break if you like. Remember back in the Layout and interface section of this chapter
when we laid down some rules about our app? It’s time to recap those now as it will affect how
we implement dictionary searching. We said we’ll stick to a rule “that the user’s search query
can be in Japanese as well as English”. Obviously we’ll then go down different search query
routes depending on the entered language. So we need a way to detect if the query is Japanese
or English.
The Search tab 63
..
Why two search querying routes?
We could get away with not detecting the input query’s language by having this kind of logic:
“Assume the query is English, do a search, if no results then assume it’s Japanese
and search again”
Which has two problems. We have to make an assumption about how our app is being mostly
used. (Admittedly we could change the assumption if we find out it’s wrong.) Another problem
is performance - we may be searching unnecessarily.
A very simple, and linguistically incorrect!, way to do this is to see if we have multibyte
characters in our query string or not. We’ve set our HTML page to be UTF-8. UTF-8 is interesting
because it’s a flavour of Unicode that’s backwards compatible with good ol’ ASCII. ASCII can
be utf-8, but so can Japanese! But ASCII won’t set the right bits in each byte to be considered
a multibyte stream. Something we can use to our advantage is that for ASCII, the length of a
string in bytes will also be the length of that string in characters. For multibyte utf-8 strings, this
will not be the case and the byte length will be greater than the character length.
Let’s implement an is_mb() (“mb” meaning “multibyte”) check using this knowledge. Keeping
things modular, and realising that we are going to need functions soon for Japanese language
handling, make a new file in /assets/www/js called linguistics.js. Add is_mb() thus:
//Does the given utf8 string have multibyte characters or not?
function is_mb(utf8String)
{
return utf8String.length != mb_bytelen(utf8String);
}
Here we compare a string’s length in characters (using the length property - JavaScript operates
internally with utf-16 unicode) with its length in bytes. mb_bytelen() is the key function
here that we need to write. It will give us the byte length for a utf8 string. Put it also in
linguistics.js:
//Get length in BYTES of a utf8 string
function mb_bytelen(utf8String)
{
//Matches only the 10.. bytes that are non-initial characters
//in a multi-byte sequence.
var m = encodeURIComponent(utf8String).match(/%[89ABab]/g);
return utf8String.length + (m ? m.length : 0);
}
In utf-8, everything is a sequence of bytes. For an ASCII character, one byte is the full sequence
- that byte is the character. But it allows for multibyte characters by the initial byte in
The Search tab 64
that character’s sequence of bytes setting a special bit. This special bit tells the browser (or
programming language or text editor etc) that “there’s more to come!” and the browser adds
the remaining bytes in the sequence to get the full value for that character. The remaning bytes
also set a special bit so the browser knows when that particular character has all of its bytes read.
For the gory details please see http://en.wikipedia.org/wiki/UTF-8.
So what we are doing in mb_bytelen() is adding the length of the string in characters to the
count of non-initial character sequence bytes. This will give us the total byte length for any utf8
string - containing multibyte characters or not!
OK, is_mb() is one important tool for our database querying logic in the bag. Using it, let’s think
more about our query logic with some pseudocode:
if(is_mb(searchTerm)) {
//searchTerm is Japanese (or at least multibyte)
//
//[1] exact kanji match
//[2] exact kana match
} else {
//searchTerm is English or, as last resort, romaji
//
//[1] exact definition match
//[2] partial definition match
//[3] exact romaji match (on kana field)
}
So, if we detect multibyte characters in the search term, we assume it is Japanese and try to
match it exactly against, first, the kanji field of the words in our database. Then, if that produces
no results, we try to match it exactly against the kana field. This priority order is realistic because
kanji (Chinese idiogrammic characters) are the “correct” way to write a Japanese word. The kana
is just the way to pronounce those Chinese characters. Though note that some words are kana
only and don’t hava a kanji.
We assume that the search term is in English if it contains no multibyte characters. Then we
focus on the definition field of our database. Remember that definition entries look like this:
/uncertain/vague/ambiguous/
Multiple definitions are separated by slashes. So our most relevant results (query [1] of the
English route) would be to find the search term exactly as one of these definitions. A search
for “vague” would match the above definition, for example. If that produces no results, we query
for partial matches of the search term in these definitions. For example a search for “director”
will match a definition of /company director/board member/. Finally, if we still have no
results, we can take a gamble and assume that the user has entered a term in romaji (which
is Japanese written in abc like “sayonara” or “moshimoshi”). For this we’ll have to convert the
search term into phonetic kana and query for a matching kana field. So this one needs a bit more
work programmatically.
Note that if we go down the Japanese route, and get no results at the end, we don’t then proceed
down the English route (and vice-versa).
The Search tab 65
Let’s implement the Japanese route first as the queries are easier. We’ll go back to working in
websql_search.js. Remember from writing websql_core.js how Web SQL works with callback
chains? Well, with this in mind (and don’t get me wrong, there are better and cleverer ways to
do this) we’re going to stick a couple of global variables at the top of websql_search.js so that
all callback functions can access them:
//User's search term as a global variable (so we can access it from all the different c
allbacks). Hmmm...
var global_searchTerm = null;
//Maximum number of search results to return for any query
var global_maxResultsCount = 40;
Now make doEdictQueryOn() look like this:
function doEdictQueryOn(newQ)
{
//set global_searchTerm
global_searchTerm = newQ;
//version 1.0, 4 megabytes
var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024);
if(is_mb(global_searchTerm)) //Japanese (or at least multibyte)
{
//console.log('doing as japanese - kanji');
db.transaction(queryDB_ja, errorWebSQL);
}
else //ie. English (or - as last resort - romaji)
{
console.log('doing as english - exact');
}
}
We simply save the search term into global_searchTerm, open the database and attempt the
queryDB_ja() query function (using our generic Web SQL error handler). We’ll come back to
the else section for English later, but in the meantime let’s make queryDB_ja() which is like
this:
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection
PhoneGap by Dissection

Mais conteúdo relacionado

Semelhante a PhoneGap by Dissection

How To Do Everything With JavaScript
How To Do Everything With JavaScriptHow To Do Everything With JavaScript
How To Do Everything With JavaScriptAzharul Haque Shohan
 
Finding a useful outlet for my many Adventures in go
Finding a useful outlet for my many Adventures in goFinding a useful outlet for my many Adventures in go
Finding a useful outlet for my many Adventures in goEleanor McHugh
 
Power shell for newbies getting started powershell 4
Power shell for newbies getting started powershell 4Power shell for newbies getting started powershell 4
Power shell for newbies getting started powershell 4Zafar Ali Khan
 
Beginning android application development wei meng lee
Beginning android application development wei meng leeBeginning android application development wei meng lee
Beginning android application development wei meng leeHiệp Lê Quang
 
Pc maintenance-handbook
Pc maintenance-handbookPc maintenance-handbook
Pc maintenance-handbookKrimo Kitouchi
 
FYPJ - Cerebral Android App Development (Report)
FYPJ - Cerebral Android App Development (Report)FYPJ - Cerebral Android App Development (Report)
FYPJ - Cerebral Android App Development (Report)Nehemiah Tan
 
The java interview questions ebook - confused coders
The java interview questions ebook -  confused codersThe java interview questions ebook -  confused coders
The java interview questions ebook - confused codersYash Sharma
 
Programming.clojure
Programming.clojureProgramming.clojure
Programming.clojureKwanzoo Dev
 
Android_Programming_Cookbook.pdf
Android_Programming_Cookbook.pdfAndroid_Programming_Cookbook.pdf
Android_Programming_Cookbook.pdfBasemMohammad2
 
Ipad Usability 2nd Edition
Ipad Usability 2nd EditionIpad Usability 2nd Edition
Ipad Usability 2nd Editionjamiewaltz
 

Semelhante a PhoneGap by Dissection (20)

Systems se
Systems seSystems se
Systems se
 
How To Do Everything With JavaScript
How To Do Everything With JavaScriptHow To Do Everything With JavaScript
How To Do Everything With JavaScript
 
Linux install
Linux installLinux install
Linux install
 
Finding a useful outlet for my many Adventures in go
Finding a useful outlet for my many Adventures in goFinding a useful outlet for my many Adventures in go
Finding a useful outlet for my many Adventures in go
 
Own cloudusermanual
Own cloudusermanualOwn cloudusermanual
Own cloudusermanual
 
Tutor Py
Tutor PyTutor Py
Tutor Py
 
Powershell
Powershell Powershell
Powershell
 
Power shell for newbies getting started powershell 4
Power shell for newbies getting started powershell 4Power shell for newbies getting started powershell 4
Power shell for newbies getting started powershell 4
 
Learning selenium sample
Learning selenium sampleLearning selenium sample
Learning selenium sample
 
Beginning android application development wei meng lee
Beginning android application development wei meng leeBeginning android application development wei meng lee
Beginning android application development wei meng lee
 
Developers survival-guide
Developers survival-guideDevelopers survival-guide
Developers survival-guide
 
Cimlvojt 2013bach (1)
Cimlvojt 2013bach (1)Cimlvojt 2013bach (1)
Cimlvojt 2013bach (1)
 
LaTeX InDesign CC
LaTeX InDesign CCLaTeX InDesign CC
LaTeX InDesign CC
 
Pc maintenance-handbook
Pc maintenance-handbookPc maintenance-handbook
Pc maintenance-handbook
 
FYPJ - Cerebral Android App Development (Report)
FYPJ - Cerebral Android App Development (Report)FYPJ - Cerebral Android App Development (Report)
FYPJ - Cerebral Android App Development (Report)
 
The java interview questions ebook - confused coders
The java interview questions ebook -  confused codersThe java interview questions ebook -  confused coders
The java interview questions ebook - confused coders
 
Programming.clojure
Programming.clojureProgramming.clojure
Programming.clojure
 
Python
PythonPython
Python
 
Android_Programming_Cookbook.pdf
Android_Programming_Cookbook.pdfAndroid_Programming_Cookbook.pdf
Android_Programming_Cookbook.pdf
 
Ipad Usability 2nd Edition
Ipad Usability 2nd EditionIpad Usability 2nd Edition
Ipad Usability 2nd Edition
 

Mais de Daniel_Rhodes

Hyperlocalisation or "localising everything"
Hyperlocalisation or "localising everything"Hyperlocalisation or "localising everything"
Hyperlocalisation or "localising everything"Daniel_Rhodes
 
PHP floating point precision
PHP floating point precisionPHP floating point precision
PHP floating point precisionDaniel_Rhodes
 
Creating a constructive comment culture
Creating a constructive comment cultureCreating a constructive comment culture
Creating a constructive comment cultureDaniel_Rhodes
 
"Internationalisation with PHP and Intl" source code
"Internationalisation with PHP and Intl" source code"Internationalisation with PHP and Intl" source code
"Internationalisation with PHP and Intl" source codeDaniel_Rhodes
 
Internationalisation with PHP and Intl
Internationalisation with PHP and IntlInternationalisation with PHP and Intl
Internationalisation with PHP and IntlDaniel_Rhodes
 
Character sets and iconv
Character sets and iconvCharacter sets and iconv
Character sets and iconvDaniel_Rhodes
 
"Character sets and iconv" PHP source code
"Character sets and iconv" PHP source code"Character sets and iconv" PHP source code
"Character sets and iconv" PHP source codeDaniel_Rhodes
 
Handling multibyte CSV files in PHP
Handling multibyte CSV files in PHPHandling multibyte CSV files in PHP
Handling multibyte CSV files in PHPDaniel_Rhodes
 
Multibyte string handling in PHP
Multibyte string handling in PHPMultibyte string handling in PHP
Multibyte string handling in PHPDaniel_Rhodes
 

Mais de Daniel_Rhodes (9)

Hyperlocalisation or "localising everything"
Hyperlocalisation or "localising everything"Hyperlocalisation or "localising everything"
Hyperlocalisation or "localising everything"
 
PHP floating point precision
PHP floating point precisionPHP floating point precision
PHP floating point precision
 
Creating a constructive comment culture
Creating a constructive comment cultureCreating a constructive comment culture
Creating a constructive comment culture
 
"Internationalisation with PHP and Intl" source code
"Internationalisation with PHP and Intl" source code"Internationalisation with PHP and Intl" source code
"Internationalisation with PHP and Intl" source code
 
Internationalisation with PHP and Intl
Internationalisation with PHP and IntlInternationalisation with PHP and Intl
Internationalisation with PHP and Intl
 
Character sets and iconv
Character sets and iconvCharacter sets and iconv
Character sets and iconv
 
"Character sets and iconv" PHP source code
"Character sets and iconv" PHP source code"Character sets and iconv" PHP source code
"Character sets and iconv" PHP source code
 
Handling multibyte CSV files in PHP
Handling multibyte CSV files in PHPHandling multibyte CSV files in PHP
Handling multibyte CSV files in PHP
 
Multibyte string handling in PHP
Multibyte string handling in PHPMultibyte string handling in PHP
Multibyte string handling in PHP
 

Último

SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AISyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AIABDERRAOUF MEHENNI
 
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerHow To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerThousandEyes
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comFatema Valibhai
 
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...MyIntelliSource, Inc.
 
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...MyIntelliSource, Inc.
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providermohitmore19
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️Delhi Call girls
 
Software Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsSoftware Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsArshad QA
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Modelsaagamshah0812
 
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...kellynguyen01
 
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfLearn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfkalichargn70th171
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Steffen Staab
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdfWave PLM
 
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...Health
 
A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxComplianceQuest1
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfkalichargn70th171
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsAndolasoft Inc
 
Diamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with PrecisionDiamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with PrecisionSolGuruz
 
Hand gesture recognition PROJECT PPT.pptx
Hand gesture recognition PROJECT PPT.pptxHand gesture recognition PROJECT PPT.pptx
Hand gesture recognition PROJECT PPT.pptxbodapatigopi8531
 

Último (20)

SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AISyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
 
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerHow To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.com
 
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
Steps To Getting Up And Running Quickly With MyTimeClock Employee Scheduling ...
 
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
Try MyIntelliAccount Cloud Accounting Software As A Service Solution Risk Fre...
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
Software Quality Assurance Interview Questions
Software Quality Assurance Interview QuestionsSoftware Quality Assurance Interview Questions
Software Quality Assurance Interview Questions
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
Short Story: Unveiling the Reasoning Abilities of Large Language Models by Ke...
 
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfLearn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf
 
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICECHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
CHEAP Call Girls in Pushp Vihar (-DELHI )🔝 9953056974🔝(=)/CALL GIRLS SERVICE
 
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
 
A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docx
 
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdfThe Ultimate Test Automation Guide_ Best Practices and Tips.pdf
The Ultimate Test Automation Guide_ Best Practices and Tips.pdf
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.js
 
Diamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with PrecisionDiamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with Precision
 
Hand gesture recognition PROJECT PPT.pptx
Hand gesture recognition PROJECT PPT.pptxHand gesture recognition PROJECT PPT.pptx
Hand gesture recognition PROJECT PPT.pptx
 

PhoneGap by Dissection

  • 1.
  • 2. PhoneGap by Dissection My first PhoneGap 3.x app Daniel Rhodes This book is for sale at http://leanpub.com/phonegapbydissection This version was published on 2015-02-26 This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and many iterations to get reader feedback, pivot until you have the right book and build traction once you do. ©2015 Daniel Rhodes
  • 3. Dedicated to all the hard-working girls and boys in the free and open source software communities.
  • 4. Contents 1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1 Conventions used in the text . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2. What you’ll need . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 3. What is PhoneGap . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 4. Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 4.1 The cool new way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 4.2 The fiddly older way . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 5. Quick run-through of the default app . . . . . . . . . . . . . . . . . . . . . . . . . . 9 6. First things first: The layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 7. First things first: The tabbing mechanism . . . . . . . . . . . . . . . . . . . . . . . . 27 8. The Search tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 8.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 8.2 Creating the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 8.3 Querying the database . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58 8.4 Results scrolling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78 8.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89 9. The Discover tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 9.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 9.2 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 10.The Write tab . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 10.1 Layout and interface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97 10.2 Filling the screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 10.3 Displaying a random character . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106 10.4 Finger doodling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 10.5 Extra credit challenges . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 11.Splash screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 12.Launcher icon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 13.Submitting to Google Play . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
  • 5. CONTENTS 14.That’s all folks! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131
  • 6. 1. Introduction This book is going to teach you how to get started with mobile app development using the PhoneGap platform. We’ll essentially rebuild, from scratch, a basic yet fully-functional app that really exists! It’s called Japxlate and can be found here in the Google Play Store. The app is a Japanese dictionary that you can search - even if offline. Not to worry though, we won’t get bogged down in the nitty gritty of Japanese linguistics. We’ll focus on setting up, building and finally deploying the app. You’ll laugh, you’ll cry, you’ll sick a little bit in the back of your throat, but the journey will definitely be worth it… This is version 1.0 of the book, first published February 2015 (v0.9 first published January 2014) Latest source code for the app is at https://github.com/danielrhodeswarp/japxlate- android This book was written using PhoneGap v3.1.0, but has been updated to cover anything new or different in v3.3.0 1.1 Conventions used in the text A command that you need to type on the Linux command line will look like: you@yours$ somewhere]$ some linux command to type Code (of any type - CSS, HTML or JavaScript) that you need to type in will look like: //does the cursor have random fractals? function checkRandomFractals() { return something.or.other; } HTML elements will be referred to like: <elementname> Code fragments, variable names, method names etc will look like:
  • 7. Introduction 2 someMethod(); File names and folder names will look like: /assets/www/some_file.html A side note, something tangental to the main text, will look like: .. I’m hungry but my teeth hurt. New or updated information relevant for PhoneGap v3.3.0 will look like: PhoneGap v3.3.0 uses the “Plugman” plugin manager.
  • 8. 2. What you’ll need To keep things small and simple we’ll focus solely on developing on Linux for an app that we’ll make for Android. Though one huge benefit of PhoneGap is that you can package the same(ish) code into a working app for many different mobile platforms. We also won’t be using any third- party JavaScript or CSS libraries, though these will be useful to you going forward with your app development. What you’ll need: • A Linux desktop box • PhoneGap (which requires NodeJS) on the above box - at time of writing this tutorial I was using version 3.1.0. Don’t worry, we’ll install this in the Getting started chapter • As many Android devices as you can get your hands on! At least one • Google’s “Android Developer Tools” bundle - or at least Eclipse with the Android plugins. Again we’ll cover this in the Getting started chapter • At least a lower-intermediate knowledge of HTML5, JavaScript and CSS • To not be terrified of the Linux command line!
  • 9. 3. What is PhoneGap PhoneGap is a way to make apps for mobile devices using standard website frontend technolo- gies. Namely HTML5, JavaScript and CSS. PhoneGap is free and open source. PhoneGap apps aren’t true or native apps, but rather they are apps that open up a “WebView” on you mobile device - essentially a web browser in fullscreen mode without title bars or bezels - running your frontend code. It’s not a million miles away from a desktop browser running in fullscreen mode (usually accessed by pressing F11). Implemented well, this non-nativeness isn’t necessarily a bad thing. .. PhoneGap versus Cordova You’ve probably come across the term “Cordova” in your research for PhoneGap. PhoneGap and Cordova are very closely related, and so it’s worth explaining the difference. There’s a lot of back-story here which I’ll skip, but in a nutshell: PhoneGap is a software product by Adobe Systems Inc. It is a branded and maintained distribution of: Cordova, which is a free and open source project maintained by the Apache Software Foundation (ASF). At the time of writing, PhoneGap adds a cloud build service to basic Cordova. This changes the command line for PhoneGap (versus Cordova) somewhat, though you should be able to - in theory - follow this tutorial using plain vanilla Cordova instead of PhoneGap. I also noticed, annoyingly, that a lot of PhoneGap documentation simply points to Cordova documentation which can mean that the command line syntax is wrong.
  • 10. 4. Getting started There are two routes we can go down to get started with PhoneGap development. Both routes require the Android SDK to be installed so let’s do that first. The easiest way to install the Android SDK is to install the Android Developer Tools (or ADT) bundle. This bundle installs the Android SDK and Eclipse IDE configured for Android (native) development. Right, let’s install the Android Developer Tools. The easy peasy way is to download and install the “ADT Bundle for Linux” from http://developer.android.com/sdk/index.html which should be worry free. If you’re already using Eclipse IDE, you can simply download the Android Developer Tools plugin for it at http://developer.android.com/tools/index.html .. About IDEs You aren’t forced to use Eclipse IDE for Android development, though it does make a lot of things easier as it supports direct deploy to an actual Android device and it has a virtual device manager for deploying to emulated Android devices. Myself, I didn’t like the way that Eclipse was opening - and highlighting - the various frontend source files for the app (though I don’t doubt that this is configurable in the options somewhere!). There’s also the fact that it doesn’t speak PhoneGap. I found myself cutting the code in NetBeans IDE and checking in with Eclipse every now and again to deploy to the actual device (Ctrl-F11) or to check console.log() messages in LogCat. Netbeans IDE v7.4 dropped just before I finished this tutorial and interestingly that seems to have PhoneGap (well, Cordova) support built in! Definitely worth a look. Bizarrely, I found that regardless of the IDE used, I often had to deploy to the device twice in order to have it truly updated. This happened whenever a resource file was updated, ie. JavaScript or HTML or CSS. I notice this doesn’t happen when Java sources are edited which indicates some kind of caching issue. I still haven’t got to the bottom of this particular mystery. 4.1 The cool new way OK, now we can install PhoneGap itself. For some strange reason that I can’t figure out (I’m guessing it’s just for package management) it requires NodeJS so go to http://nodejs.org and install it. Then, as we see at http://phonegap.com/install we simply do (on the command line): you@yours$ somewhere]$ sudo npm install -g phonegap
  • 11. Getting started 6 This installs the PhoneGap binaries and commands globally on our system. After that, let’s actually create the PhoneGap project where we’ll put all of our lovely code for the app. There are two slightly different syntaxes for this: you@yours$ somewhere]$ phonegap create --name "Japxlate" --id "com.drappenheimer.japxla te" japxlate or you@yours$ somewhere]$ phonegap create japxlate com.drappenheimer.japxlate "Japxlate" This will create a PhoneGap project folder structure for building the same code to many different device targets (Android or iOS etc). "Japxlate" is the name of our app (in quotes). com.drappenheimer.japxlate is our app’s reverse domain name identifier. All Android apps have a unique identifier like this. japxlate is our desired folder name for the project. We then want to do: you@yours$ somewhere]$ cd japxlate you@yours$ japxlate]$ phonegap run android Which will detect your Android SDK and try to run the app on the currently connected device (or configured virtual machine). If no Android SDK is found or present, it will try to deploy the app to your account on the PhoneGap remote cloud build environment - which is just out of beta at time of writing. But you’ll more than likely need an extra bit of setup to get this run android command to work. Specifically you’ll need to add a couple of folders from the Android SDK install to your PATH. The gory details are at http://docs.phonegap.com/en/edge/guide_- platforms_android_index.md.html#Android%20Platform%20Guide, but how I did it was to add the following lines to my ∼/.bashrc file: export ANDROID_SDK_HOME=/wherever/you/installed/it/adt-bundle-linux-x86_64-20130729/sdk export PATH=${PATH}:${ANDROID_SDK_HOME}/platform-tools:${ANDROID_SDK_HOME}/tools As well as this I personally needed the Java development libraries to be installed. If the run android command still doesn’t work after all this configuration, double check your Android SDK Manager which you can reach from the Eclipse IDE. Note that this run command is a shortcut for the build followed by install commands. If you don’t want to actually run your PhoneGap app from the command line, you need to at least build it which is like this: you@yours$ japxlate]$ phonegap build android This will create a PROJECTROOT/platforms/android folder with skeleton source files for our app in it. And importantly the project files for this to be pickupable as an Android project in Eclipse IDE.
  • 12. Getting started 7 .. How many mobile platforms does it take to change a lightbulb? You might be wondering now, if PhoneGap is supposed to be this amazing tool that lets us write the same app code for multiple mobile platforms, why would we want to dive straight in to the /platforms/android folder? How is that going to work on, say, iOS? The answer is simple, PhoneGap is indeed a tool where the same app code can be compiled for multiple mobile platforms, but - in a nutshell - we are cheating and taking a shortcut! This tutorial is rather simplified and focuses solely on Android. This is why we dive right in at /platforms/android. If your app needs to work on multiple mobile platforms - as most apps do - then you should really create your app’s code in PROJECTROOT/www, specifying any platform-specific customisa- tions in PROJECTROOT/merges, then debug each time for your platforms with the build, install and run commands. The excellent blog post at http://devgirl.org/2013/09/05/phonegap-3-0- stuff-you-should-know/ explains this very well. Like the run command, the build command will also fallback to the remote cloud build environment. You can disable this fallback with the command phonegap local build android. Right, so now you’ve at least built your app on the command line. You might even have run it from the command line! Going forward with this tutorial, let’s plug the skeleton code we’ve just built into our Eclipse IDE as an Android project. Follow these steps: 1. Click File ⇒ New ⇒ Project 2. Select Android ⇒ Android Project from Existing Code (note there’s also a sample native project in there!) 3. Browse to PROJECTROOT/platforms/android folder (actually just PROJECTROOT seems to also work) 4. Click OK 5. You’ll get an “Import Projects” dialogue now with the project details that you can confirm / change and then click Finish .. Keeping your PhoneGap up-to-date Installing PhoneGap via NodeJS has the nice advantage that you can keep your PhoneGap version up-to-date by running this command: you@yours$ somewhere]$ sudo npm update -g phonegap
  • 13. Getting started 8 4.2 The fiddly older way An older way of getting started (that PhoneGap up to v2.1.0 used) still works and can be useful if you are struggling with the configuration steps details in the above section. You’ll still need to have Eclipse with the ADT installed first, but you won’t have to fiddle around with installing NodeJS or altering PATH environment variables. Simply download - rather than install - the relevant “archive” version of PhoneGap from http://phonegap.com/install, and then you can follow the steps from “Setup New Project” in the PhoneGap documentation. Please note that these instructions are for older versions of PhoneGap and Eclipse and so your mileage with the latest versions may vary. This page on the Adobe website is also a useful reference. Sorry but I can’t specify exactly how to do it this way as it is not the supported way any more. It might stop working for future versions of PhoneGap. Though I could get it working - with a few tweaks - with PhoneGap v3.1.0. Advantages of this method: You don’t have to install PhoneGap or NodeJS or any dependencies. Disadvantages of this method: You don’t get PhoneGap’s latest template for setting up an Android app and you have to do it manually (ie. updating the manifest etc).
  • 14. 5. Quick run-through of the default app Our app starts life as the PhoneGap “Hello world” app (unless you went The fiddly older way in which case it’s empty). This is a good starting point and has some things we can build on and learn from. Of course we’ll need to ditch a lot of it as well! Go ahead, hit CTRL-F11 in Eclipse to run the app on your virtual or actual device. We get a little robot icon and a pulsing (via CSS3) “device is ready” message. Rotate your device, it redraws itself accordingly and changes the layout slightly if needed. It also doesn’t present or allow any kind of scrolling or pinching which is A Good Thing for most apps - including Japxlate. Figure 1. The default PhoneGap app (landscape) The files that we’ll be wanting to edit (CSS, HTML5, JavaScript) to make our own app can be found in the assets/www folder of our Eclipse project. Let’s take a look at the generated assets/www/index.html (Apache licence text removed for brevity): <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="format-detection" content="telephone=no" /> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale =1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device -dpi" /> <link rel="stylesheet" type="text/css" href="css/index.css" /> <title>Hello World</title> </head> <body>
  • 15. Quick run-through of the default app 10 <div class="app"> <h1>PhoneGap</h1> <div id="deviceready" class="blink"> <p class="event listening">Connecting to Device</p> <p class="event received">Device is Ready</p> </div> </div> <script type="text/javascript" src="phonegap.js"></script> <script type="text/javascript" src="js/index.js"></script> <script type="text/javascript"> app.initialize(); </script> </body> </html> PhoneGap v3.3.0 adds a comment talking about a workaround for iOS 7. We’ve got the simplified “html” DOCTYPE for HTML5. We explicity set a charset of utf-8 Unicode which is clearly going to be very important for this app! We’ve got a lot of “viewport” settings which are mostly self-explanatory, but essentially say “this app fills the device display, defaults to 100% zoom and can not be zoomed in or out”. This is really going to help our PhoneGap app look and feel more like a native app and not a web browser view. We then link to some CSS which we’ll look at shortly. The <title> needs updating, but this won’t normally be visible to the app user anyway. Especially as PhoneGap build puts a theme setting of Theme.Black.NoTitleBar in AndroidManifest.xml. Then the <body> starts and we have whatever markup the app needs. Just before the <body> closes, we have links to some JavaScript (this is debated but considered to be something of a performance improvement). phonegap.js (in assets/www) is the PhoneGap library and is how we can access phone hardware (ie. camera) from JavaScript in our PhoneGap app. Commenting out this file will enable you to somewhat preview the app just by opening the index.html file in Chrome desktop browser. We’ll talk about this later. js/index.js is JavaScript specifically for this app. We then call app.initialize(). The app object is in index.js which we’ll look at after taking a quick peek at the key things in the CSS file we mentioned a moment ago (Apache licence text removed for brevity):
  • 16. Quick run-through of the default app 11 * { -webkit-tap-highlight-color: rgba(0,0,0,0); /* make transparent link selection, adj ust last value opacity 0 to 1.0 */ } body { -webkit-touch-callout: none; /* prevent callout to copy image, etc w hen tap to hold */ -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */ -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ background-color:#E4E4E4; background-image:linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-webkit-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-ms-linear-gradient(top, #A7A7A7 0%, #E4E4E4 51%); background-image:-webkit-gradient( linear, left top, left bottom, color-stop(0, #A7A7A7), color-stop(0.51, #E4E4E4) ); background-attachment:fixed; font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif; font-size:12px; height:100%; margin:0px; padding:0px; text-transform:uppercase; width:100%; } /* Portrait layout (default) */ .app { background:url(../img/logo.png) no-repeat center top; /* 170px x 200px */ position:absolute; /* position in the center of the screen */ left:50%; top:50%; height:50px; /* text area height */ width:225px; /* text area width */ text-align:center; padding:180px 0px 0px 0px; /* image height is 200px (bottom 20px are overlapped with text) */ margin:-115px 0px 0px -112px; /* offset vertical: half of image height and text ar ea height */ /* offset horizontal: half of text area width */ }
  • 17. Quick run-through of the default app 12 /* Landscape layout (with min-width) */ @media screen and (min-aspect-ratio: 1/1) and (min-width:400px) { .app { background-position:left center; padding:75px 0px 75px 170px; /* padding-top + padding-bottom + text area = ima ge height */ margin:-90px 0px 0px -198px; /* offset vertical: half of image height */ /* offset horizontal: half of image width and tex t area width */ } } . . The clause for * simply removes, from any element that we might make tappable, the default sickly orange highlight that Android WebView gives to links and buttons and things. The body clause starts by disabling some default Android WebView interations. This makes our PhoneGap app feel a bit more nativey. Then we set a grey gradient as the background. Then we set the font type and size (12px). Height and width are both set to 100% which makes our <body> fill the size of the WebView screen. We specify no margin (which is gap space outside the <body>) and no padding (which is gap space inside the <body>). In .app - our top level div in the markup - we set the layout of our app specific things. Portrait orientation is assumed - a safe assumption for most phone apps. I won’t bore you with this too much (but if you are baffled then please see a CSS refresher) other than to say it pulls some strings with absolute positioning and negative margins to centre a background image and some text. Then we have another .app block wrapped in what’s called a media query (http://cssmediaqueries.com/what-are-css-media-queries.html is a useful introduction) which triggers when the phone is rotated into landscape view. It moves the background image to the left of the text and also moves the text such that things are still centred. Right, let’s get back to that js/index.js file that we’ve almost forgotten about! (Apache licence text removed for brevity): var app = { // Application Constructor initialize: function() { this.bindEvents(); }, // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() { document.addEventListener('deviceready', this.onDeviceReady, false);
  • 18. Quick run-through of the default app 13 }, // deviceready Event Handler // // The scope of 'this' is the event. In order to call the 'receivedEvent' // function, we must explicity call 'app.receivedEvent(...);' onDeviceReady: function() { app.receivedEvent('deviceready'); }, // Update DOM on a Received Event receivedEvent: function(id) { var parentElement = document.getElementById(id); var listeningElement = parentElement.querySelector('.listening'); var receivedElement = parentElement.querySelector('.received'); listeningElement.setAttribute('style', 'display:none;'); receivedElement.setAttribute('style', 'display:block;'); console.log('Received Event: ' + id); } }; All we have is one object called app which represents - wait for it! - our PhoneGap app. initialize() is the constructor. We call this directly from index.html if you remember. initialize() simply calls app.bindEvents() which in turn uses a DOM standard way of adding an event listener. The event we listen for here is ‘deviceready’ which is fired from the PhoneGap library when our Android device is, well, ready. We specify that this event is to be handled by app.onDeviceReady() which simply calls app.receivedEvent('deviceready'). app.receivedEvent('deviceready') simply hides the “connecting” message and displays the “ready” message (which are displayed and hidden, respectively, via the default index.css). someElement.querySelector() is very interesting here and we’ll look at that later. console.log(someMessage) is worth talking about now because we are going to be hammering it during development! Basically this logs something to the browser’s console without disturbing the user. When running your app via Eclipse’s F11, console.log() messages that fire on the device will show up in your Eclipse’s “LogCat” thus:
  • 19. Quick run-through of the default app 14 Figure 2. console.log() messages as appearing in Eclipse’s LogCat Or, if debugging in Chrome desktop, you can see it by pressing F12 on the page in question then clicking the console tab: Figure 3. console.log() messages as appearing in Chrome desktop’s debugger console.log() (and there are actually some other methods) is a general JavaScript development technique that isn’t specific to mobile development. It works on all major browsers (though IE needs help!).
  • 20. 6. First things first: The layout Japxlate is going to have a single screen or “intent”. It won’t jump out to, for example, your phone’s camera intent or “share to” list. The single screen is going to have three tab options - Search, Discover and Write. We want the tab navigation and current tab content to all fit on the device display without scrolling. OK, the PhoneGap Hello World app we just looked at is a good start, but let’s see what tweaks we can do. The Japxlate app is a spinoff of the @japxlate Twitter channel, so let’s look at that to get some design ideas: Figure 4. The @japxlate Twitter channel OK, so we’ve got a greyish background. The logo is a red ‘J’ on a white background. The red is our signature red and is actually #990000. The red ‘J’ on a white background is going to be a good launcher icon for our app which we’ll talk about in a later chapter. Right, so we need three tabs and we have some colour ideas. Here’s a quick wireframe:
  • 21. First things first: The layout 16 Figure 5. Quick wireframe of the Japxlate app layout Let’s put our tabs at the top so they’re out of the way of our device’s core Android buttons (back, home, menu / special). Let’s have a little footer and see if we need that. The footer and header have grey backgrounds. The tab content area is bog-standard black text on a white background. When a tab is tapped, the header and footer will stay the same (though possibly with some kind of current tab highlight) but the content area will load the appropriate content for that tab. HTML5 gives us <header> and <footer> elements, so let’s try those. Change the <body> in index.html to look like: <body> <header> header </header> <div class="japxlate_app"> <!--note we've changed the class name--> content area </div> <footer> footer </footer> <!--<script type="text/javascript" src="phonegap.js"></script>--> <script type="text/javascript" src="js/index.js"></script> <script type="text/javascript"> app.initialize(); </script> </body> Fire this up on your device (or desktop Chrome) and it looks like this:
  • 22. First things first: The layout 17 Figure 6. Unstyled <header> and <footer> Not quite what we had in mind! The <header> and <footer> are both 100% wide which is great, but we need to give them positions and heights (with tab content taking up the remaining space inbetween). Also let’s get rid of the PhoneGap background gradient and put our own background colours in. Also let’s take out the forced uppercase. Change the body clause in index.css to look like this: body { -webkit-touch-callout: none; /* prevent callout to copy image, etc w hen tap to hold */ -webkit-text-size-adjust: none; /* prevent webkit from resizing text to fit */ -webkit-user-select: none; /* prevent copy paste, to allow, change 'none' to 'text' */ font-family:'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif; font-size:12px; height:100%; margin:0px; padding:0px; width:100%; } Then add a clause for header like this:
  • 23. First things first: The layout 18 header { background-color:#555; /*medium grey*/ color:#ccc; /*slightly greyish white*/ height:40px; line-height:40px; /*height of a *text* line*/ } Then add a clause for footer like this: footer { background-color:#555; /*medium grey*/ color:#ccc; /*slightly greyish white*/ height:20px; line-height:20px; } Running this looks like: Figure 7. <footer> is too high Hmm, the footer isn’t at the bottom! Let’s position it absolutely and make it flush with the bottom of its parent (the document body). Add to the footer rule so that it looks like:
  • 24. First things first: The layout 19 footer { background-color:#555; /*medium grey*/ color:#ccc; /*slightly greyish white*/ height:20px; line-height:20px; position:absolute; bottom:0; width:100%; /*no default width for position:absolute*/ } Running this looks like: Figure 8. <footer> flush with bottom of document body Great! Now let’s put our three tabs into the header. We’ll do it as an unordered list of links. Make <header> of index.html look like this: <header> <ul id="tab-bar"> <li > <a href="#search">Search</a> </li> <li > <a href="#discover">Discover</a> </li> <li> <a href="#write">Write</a> </li> </ul> </header> Running this looks like:
  • 25. First things first: The layout 20 Figure 9. First attempt at tabs Clearly a disaster! We need some styling to line up the list items horizontally in the header. Add the following three clauses to the CSS file: /*entire tab row*/ #tab-bar { /*clear any inside and outside gap space*/ margin:0; padding:0; } /*each tab*/ #tab-bar li { display: inline; /*prevent each item from newlining*/ float:left; /*stack left*/ width: 33.3333%; /*have a third of total tab-bar space*/ } /*tappable link in each tab*/ #tab-bar li a { color: #ccc; display: block; /*make "width-having"*/ font-weight: bold; overflow: hidden; /*so long link text words get cropped*/ text-align: center; text-decoration: none; /*remove default link underline*/ } Running this looks like:
  • 26. First things first: The layout 21 Figure 10. Tabs line up horizontally Looking good! But the tabs need a few more things to look more useful. Namely, horizontal dividers, icons and some kind of current tab highlight. For the horizontal dividers, let’s try giving the second and third tabs a left border. CSS version 2 (the latest version being 3) has a nifty selector where we can say “element type Y only where it follows an element type X”. With this we can target any tab after the first one and apply a left border. Add the following clause to the CSS: /*a border-left for the middle and rightmost tab*/ #tab-bar li + li { border-left:1px solid #aaa; /*light grey*/ } Running this looks like: Figure 11. <header> too wide for document body
  • 27. First things first: The layout 22 Ouch, that’s not a good look. What’s happened here is that the border has added 1px to the total width of the second and third tabs. These tabs are now wider than a 3rd of the <header> row and so the last tab gets bumped onto the next line. This is A Very Annoying Thing. One cheesy little workaround for this is to use a simple background image to simulate the border. Make a 1 pixel wide by 16 pixel tall image in GIMP (or what-have-you) and floodfill with #aaaaaa which is a very light grey. Export to a PNG image in assests/www/img called aaaaaa_16_v.png. Then change the previously added CSS clause to look like this: /*simulate a border-left for the middle and rightmost tab*/ #tab-bar li + li { background-image:url(../img/aaaaaa_16_v.png); background-repeat:repeat-y; background-position:left; } Running this looks like: Figure 12. <header> fits nicely Pretty good! OK, we’ll do the icons next. We want each tab to have a little icon on it. There are millions of icon sets floating around these days. They tend to be one of three types: • Always free • Free only for personal use (else you should pay) • Always paid-for There’s also new school flat icons versus traditional deep icons. Design memes come and go but we’ll go with something a little flat. We’ll use these rather nice ones which are royalty-free, free for personal and commercial use: http://www.graphicsfuel.com/2013/04/20-flat-icons-psd Note that these icons are in PNG format which is a raster format. Raster icons are easy to use, but can only be shrunk or enlarged by extracting or guessing information (respectively). This means they only really look good at their native size which means that, depending on the pixel
  • 28. First things first: The layout 23 density of the device display, they might be too tiny and hard to make out or really massive and Legoish. But we’ll use them for simplicity. One alternative would be to use a vector format - such as SVG - for the icons which stores the image such that it can be scaled up or down without losing information. Another new trend is to have the browser load something called an icon font. This is like a normal font but where each character is an icon (remember Wingdings?!). This has the advantage that the icons are sizeable just like any other text. Also they can be bolded or italicised. But they can only be of one colour. Go ahead and put all of the PNG icons in assets/www/img (though we won’t use all of them). Let’s reference some of these icons in our tab markup, change <header> in index.html to look like this: <header> <ul id="tab-bar"> <li> <a href="#search"><img src="img/search.png"> Search</a> </li> <li> <a href="#discover"><img src="img/chat-bubble.png"> Discover</a> </li> <li> <a href="#write"><img src="img/file.png"> Write</a> </li> </ul> </header> Note the space after the image and before the link text. Running this gives: Figure 13. Icons we sourced are way too big Woah, those icons are pretty big eh? The icons are a mix of square, tall or wide, but they all have a biggest side of about 128 pixels. That’s clearly way too big for us here. Let’s use GIMP to resize search.png, chat-bubble.png and file.png to have a biggest side of 16px - the same as our app font size (in index.css) [NOTETOSELF double check this]. So go ahead and make those changes and overwrite the original icon files. While you’re at it, do the same for paste.png because we’ll be using that later on. (Feel free to trash the other icon files from assets/www/img as we won’t
  • 29. First things first: The layout 24 be needing them in this little app.) Those scalable icon formats are looking real attractive now huh? After changing the icon sizes, it looks like this: Figure 14. Icons at correct size Not bad at all. But hmmmm, don’t you think the icons look a little out of whack? Like they’re slightly higher than the line of text? We can remedy this by adding to the CSS: a img { vertical-align:middle; /*make more sensible relative to text baseline*/ } (Yes, we could do these icons as CSS background images but what the heck.) That’s better. We restrict this only to images in <a>’s so we don’t screw up any other images we might have in the markup. All we need now is a highlight for the currently selected tab, and while we’re at it we should choose our default tab that we want to be displayed first on app load. Let’s plump for the Search tab. Add a class name of “current” to the Search tab thus: <ul id="tab-bar"> <li class="current"> <a href="#search"><img src="img/search.png"> Search</a> </li> . . </ul> Then, in the CSS, modify #tab-bar li{} and add #tab-bar li.current{} thus:
  • 30. First things first: The layout 25 /*each tab*/ #tab-bar li { display: inline; float:left; width: 33.3333%; border-bottom:3px solid #555; /*same bg as header*/ } /*current tab*/ #tab-bar li.current { border-bottom:3px solid #990000; /*signature red*/ } We simply add a bottom border, in our signature red, to any tab bar list item that has a class of “current”. We also add a border of the same size but using the header’s background colour to non current tabs. This keeps everything looking flush horizontally. Later on (soon actually!) we will use JavaScript to detect tap events on the tabs and change the current tab. Running what you have so far looks like: Figure 15. Current tab highlight Pretty good! Only two little things are bugging us now. The content area text starts a little too close to the tab bar, and, thinking about it this app doesn’t really need a footer at all! Change the HTML footer to simply look like this: <footer></footer> Then add .japxlate_app{} to the CSS and also change the height of footer{} thus: .japxlate_app { padding-top:1em; /*move content away from tab bar*/ } footer { background-color:#555; color:#ccc; height:2px; /*down to 2px from 20px*/ line-height:20px; /*no longer meaningful...*/
  • 31. First things first: The layout 26 position:absolute; bottom:0; width:100%; } Running this looks like: Figure 16. Final app layout Which we’ll stick with for the rest of the tutorial - and app! We have a 2px footer which is a bit gimmicky, but will help us a bit with scroll debugging a bit later on. The tab content text is now one newline(ish) down from the tab bar. .. To fullscreen or not? You might have noticed by now that the default PhoneGap app, and our own app’s layout that we’ve just finished, fill the entire screen of the device. Even the Android status bar (which shows the time, battery charge and signal strength etc) is obliterated. Game apps tend to fill the entire screen, but almost every utility app out there leaves the status bar. The good news is that we can get the status bar back quite easily by opening PROJECTROOT/platforms/android/res/xml/config.xml and changing: <preference name="fullscreen" value="true" /> to <preference name="fullscreen" value="false" /> and then re-running the app. You can choose which style you like and the rest of this tutorial is valid either way. Note that figures showing device screenshots won’t have the status bar.
  • 32. 7. First things first: The tabbing mechanism The layout is in the bag now, but we need a mechanism to markup the content for our three different tabs and a way for taps on the tabs to trigger the display of the relevant content. We can markup the content for all three tabs in the HTML file and simply have Discover and Write hidden (Search is our default remember) with CSS when the app first starts. Let’s do this first before we look at any JavaScript. Edit <div class="japxlate_app"> in index.html so that it’s contents are like this: <div class="japxlate_app"> <div id="tab-content"> <div id="search" class="current"> search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. search tab content. </div> <div id="discover"> discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. discover tab content. </div> <div id="write"> write tab content.write tab content. write tab content. write tab content.write tab content. write tab content. write tab content.write tab content. write tab content. write tab content.write tab content. write tab content. </div> </div> </div> Then let’s default to hidden, but with class="current" being visible, for these <div>s in #tab-content. Add the following two clauses to index.css:
  • 33. First things first: The tabbing mechanism 28 #tab-content > div.current { display:block; } #tab-content > div { display:none; } Hmm, well running this looks like: Figure 17. Tab content spills over the footer Search tab is indeed the only visible tab, but if there is a lot of content then it overflows and goes past the footer! This will cause our PhoneGap app to be swipe scrollable which is a bad thing! To fix this, let’s see what the .japxlate_app master container <div> is doing in relation to the footer when it has both little and lots of content. For that let’s add this cheeky little debug to the .japxlate_app{} CSS: .japxlate_app { padding-top:1em; border:1px solid green; /*debug*/ } This puts a thin green border around the entire div. This is a useful debugging tool but note that it will add two pixels to the width and two pixels to the height of the div it is applied to. This may make scrollbars appear where usually you wouldn’t have scrollbars. Running with both large and small amounts of content looks like this:
  • 34. First things first: The tabbing mechanism 29 Figure 18. Size of .japxlate_app div with large (left) and small (right) content amounts So it looks like our master container div doesn’t have a fixed height and is as tall as it needs to be for its content. We want it to be exactly tall enough to fit perfectly under the header and above the footer. Then, if content is lots and it overspills, it will clip above the footer and won’t screw up our app’s look and feel. We may then choose to handle content scrolling manually. Our .japxlate_app master container div has the same parent as the header and footer (ie. <body>) so we should be able to position it absolutely, tinker with CSS top and bottom properties and “slot” it in between the header and footer. Let’s change the CSS for .japxlate_app to look like this: .japxlate_app { padding-top:1em; border:1px solid green; overflow:auto; /*scrolling functionality *IF* we need it*/ position:absolute; top:43px; /*flush with bottom of header*/ bottom:2px; /*flush with top of footer*/ width:100%; } Note that we’re keeping the debug green border for the moment. Running the app now looks like this:
  • 35. First things first: The tabbing mechanism 30 Figure 19. Improved .japxlate_app div with large (left) and small (right) content amounts For the win! Notice how (on desktop Chrome only) we only get the scrollbar when we need it. Notice also how it’s a scrollbar just for the content div and not a full scrollbar for the entire document. This is great for our app because users won’t be able to whiz it around the screen like a normal browser page. As we’ll see later though, we will annoyingly have to implement our own scrolling for this content pane on the device. Go ahead and strip out that border:1px solid green; statement for the .japxlate_app{} rule. You must be exhausted with CSS things now (I know I am!), so let’s move on to the very last first thing (say what?!) - which is the behaviour for the tab tapping which we’ll implement in good ol’ JavaScript. We need to do two things here: 1. Detect a tap on a tab 2. Load / display content for that tab (hiding the previous tab’s content at the same time) If you’ve been debugging the app in Chrome so far (I have!), here’s where we hit a tiny stumbling block. If you remember our default index.js, all of the magic happens after we catch the deviceready event. This is a PhoneGap event that desktop browsers won’t fire. An advanced way to get around this would be to look at something like Stopgap (though, at the time of writing, this is looking a bit tumbleweedy) or, more straightforwardly, some hacks like at http://stackoverflow.com/questions/6687099/how-to-fire-deviceready-event-in-chrome- browser-trying-to-debug-phonegap-projec. What we want to do, for desktop browsers, is to not load phonegap.js. Then, instead of waiting for the deviceready event to execute our x_y_z(), we simply call x_y_z() as soon as the browser DOM is ready. Let’s use the solution by Chemik at the aforementioned StackOverflow page to only load phonegap.js on condition of being on a mobile device. We can do this in index.html thus:
  • 36. First things first: The tabbing mechanism 31 . . <footer></footer> <!--load phonegap.js only if on mobile device--> <script type="text/javascript"> if (navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|IEMobile)/)) { var line = '<script type="text/javascript" src="phonegap.js"' + '></'+'script>'; document.writeln(line); } </script> . . Note that we break up the ending </script> in our string so that it isn’t picked up by the (WebView) browser - or our IDE - as an actual ending script tag! This code will now only load phonegap.js for mobile devices. You can test this by - carefully! - inserting a cheeky alert('I am phonegap.js'); right at the top of phonegap.js. Don’t forget to remove this alert when you’ve finished testing! So now we only have phonegap.js loaded on an actual mobile device. This gives us a little tool to help with the deviceready event problem. Edit bindEvents() and receivedEvent() in index.js to look like this: . . // Bind Event Listeners // // Bind any events that are required on startup. Common events are: // 'load', 'deviceready', 'offline', and 'online'. bindEvents: function() { if (window.cordova) { //actual app document.addEventListener('deviceready', this.onDeviceReady, false); } else { //debugging in desktop browser this.onDeviceReady(); } }, // Update DOM on a Received Event receivedEvent: function(id) { console.log('Received Event: ' + id); }, . . If phonegap.js is loaded, it will define the window.cordova object which we can test for before setting up our event listener. If phonegap.js is not loaded, we simply call what the listener calls anyway. Running this in both desktop Chrome and your device should produce the eventual console.log() message (you’ll see this via Eclipse’s LogCat if running on your device).
  • 37. First things first: The tabbing mechanism 32 .. All about alerts (and PhoneGap API plugins) Since we’re talking about debugging and JavaScript alert()s and things, let’s talk about how we can use PhoneGap to produce more native-like alerts. JavaScript alerts will definitely give your app that non-nativey, browser app feel. In fact, using alert() even on desktop sites is considered a bit naff these days! Conveniently, PhoneGap exposes a Notification API for “Visual, audible, and tactile device no- tifications.” The documentation at http://docs.phonegap.com/en/3.1.0/cordova_notification_- notification.md.html says we can use it like this:
  • 38. First things first: The tabbing mechanism 33 .. navigator.notification.alert(message, alertCallback, [title], [buttonName]); So let’s try that. Stick navigator.notification.alert('Some alert message', null); in the receivedEvent() function that we were just tinkering with. Running this (which obviously won’t work in desktop Chrome) gives a spurious error in LogCat: Figure 20. Error when attempting navigator.notification.alert() What’s going on? Well, it turns out that “As of version 3.0, Cordova implements device-level APIs as plugins”. We have to install whichever APIs we want in our project. This removes bloat as, previously, all APIs came pre-installed in every PhoneGap project. I actually found this to be a bit mysterious and poorly documented (I found myself mashing up a mix of info from Cordova docs and PhoneGap docs). But here’s how to add a particular plugin to your PhoneGap project. Go to anywhere in your project folder structure on the command line and:
  • 39. First things first: The tabbing mechanism 34 .. you@yours$ japxlate]$ phonegap local plugin add https://git-wip-us.apache.org/repos/asf /cordova-plugin-dialogs.git From PhoneGap v3.3.0 you can simply type phonegap local plugin add org.apache.cordova.dialogs Which should echo:
  • 40. First things first: The tabbing mechanism 35 .. [phonegap] adding the plugin: https://git-wip-us.apache.org/repos/asf/cordova-plugin-di alogs.git [phonegap] successfully added the plugin (Note that you won’t need to run this command, and you won’t get the above error, if you’ve gone down The fiddly older way as that bundles all plugins into your project). You’ll get the relevant URL from the docs for whichever plugin at the “API Reference” section at http://docs.phonegap.com/en/3.1.0/ (PhoneGap has a good list of core and 3rd party plugins at https://build.phonegap.com/plugins but the installation instructions for each one are seemingly out-of-date and mention tinkering with XML config files which we don’t need to do after running the above command.) The above command has downloaded the source for the plugin and put it in /assets/www/plugins (in this case in org.apache.cordova.dialogs) but diff on v3.3.0 etc. It has also added references to the plugin in /assets/www/cordova_plugins.js - a file which has been there from the start but just as a placeholder stub. The phonegap.js that we include in our index.html actually also includes cordova_plugins.js so after running the above command, we have all we need to start using navigator.notification.alert()! Try it again! It works!:
  • 41. First things first: The tabbing mechanism 36 .. Figure 21. Default navigator.notification.alert() Great. But hmmm, it looks just the same as a normal JavaScript alert()! Currently it does yes, but the advantage is that we can customise the title and button text. We can also specify a callback function to trigger when the button is tapped. Try:
  • 42. First things first: The tabbing mechanism 37 .. navigator.notification.alert('Some alert message', null, 'The title', 'Oki doki'); Running this looks like: Figure 22. Customised navigator.notification.alert() For the win! If you want to go forward with these customised alerts, keep in mind that they won’t work on desktop Chrome so you may need to write a little wrapper function to still be able to debug on desktop Chrome. The Japxlate app won’t be alerting anything to the user on purpose - perhaps just some important error messages. Therefore we’ll go forward in this tutorial with plain vanilla JavaScript alert()s. But I wanted to show you the general plugin mechanism on what is no doubt one of the easier to use plugins. In fact, I’m not done yet!:
  • 43. First things first: The tabbing mechanism 38 .. you@yours$ japxlate]$ phonegap local plugin list [phonegap] org.apache.cordova.dialogs This command lists all plugins installed in the current project. you@yours$ japxlate]$ phonegap local plugin remove org.apache.cordova.dialogs [phonegap] removing the plugin: org.apache.cordova.dialogs [phonegap] successfully removed the plugin This command removes the specified plugin from the current project. You specify the plugin by its reverse-DNS identifier. You can find these out by issuing the above “list” command. There are plugins to access the mobile device’s camera, accelerometer, phone contacts and many more. Using these plugins is how we make a full fat mobile app and not just a simple website-in-a-box. PhoneGap v3.3.0 also has “Plugman” which is another way of working with plugins. Plugman lets you add or remove plugins for one specific platform, whereas the above method will add or remove plugins globally to any and all platforms used in the project. Please see http://docs.phonegap.com/en/3.3.0/plugin_ref_plugman.md.html. We’ve just been able to simulate our deviceready event on desktop Chrome for debugging and we are ready to get our tab taps working. receivedEvent() in index.js is where the magic happens because by the time we reach there, the device is ready (and the browser DOM is ready as we’ve put JavaScript includes at the bottom of our HTML). But let’s not go down the route of stuffing all of our JavaScript in index.js. Let’s go modular - right from the start. Create a new JavaScript file called: japxlate.js in /assets/www/js and include it from index.html thus: <script type="text/javascript" src="js/japxlate.js"></script> <script type="text/javascript" src="js/index.js"></script> <script type="text/javascript"> app.initialize(); </script> Put a function called configureTabs() in the newly created japxlate.js thus:
  • 44. First things first: The tabbing mechanism 39 //tab clickability function configureTabs() { var tabs = document.querySelectorAll("#tab-bar li a"); for(var loop = 0; loop < tabs.length; loop++) { var tab = tabs.item(loop); tab.addEventListener('click', function(event){alert(event + ' on ' + this);}, f alse); } } Then modify index.js to call this new function in receivedEvent() thus: // Update DOM on a Received Event receivedEvent: function(id) { console.log('Received Event: ' + id); configureTabs(); }, Running this, and clicking on one of the tabs results in:
  • 45. First things first: The tabbing mechanism 40 Figure 23. Debug alert() after clicking Discover tab We are nearly there! We are detecting tab taps nicely! First let me explain some key points of the configureTabs() function so far. document.querySelectorAll("#tab-bar li a"); This is a great new piece of modern JavaScript that returns to us an array of DOM elements (a “NodeList”) that match our CSS style selector. (The related querySelector() returns the first matching element.) This is something that has found its way into W3C standard DOMJavaScript based on something that jQuery has popularised (but not invented - Behaviour.js was one of the first to do this). Here we run querySelectorAll() on the document object so we are going to get all matches contained in <body>. Usefully, it can also be run on an Element object - for example a certain table or form - or a DocumentFragment element to only return matching elements in that particular container element. #tab-bar li a is a CSS style query for “an <a> in a <li> in any element with id ‘tab-bar’”. We loop over all matching <a> elements and set a click handler using the DOM standard addEventListener() (as formally described at http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-registration). Our event han- dler in this case is a simple anonymous function giving a debug alert. In event handler functions,
  • 46. First things first: The tabbing mechanism 41 an Event object is passed as a parameter and contains information about the particular event that triggered the handler - screen x and y coordinates for mouse events and which key was pressed for keyboard events and so on. In event handler functions, this refers to the element on which the event happened. Let’s replace the dummy click handler with something that we’ll actually want to use. But first, remember that in the click handler function we only have the event object and the <a> object (as this)? We’ll also need to know which content <div> relates to which <a>, then we can switch the content accordingly. Modify the header of index.html to look like this: <header> <ul id="tab-bar"> <li class="current"> <a href="#search" data-div-id="search"><img src="img/search.png"> Search</a> </li> <li> <a href="#discover" data-div-id="discover"><img src="img/chat-bubble.png"> Discover</a> </li> <li> <a href="#write" data-div-id="write"><img src="img/file.png"> Write</a> </li> </ul> </header> HTML5 allows us to use custom or “data” attributes where we can add any attribute and value we like to any particular element. The attribute names start with “data-“. Here we simply link each <a> to its matching content <div> id. We’ll use this attribute (soon) in the click handler for tabs. OK, next strip out the dummy handler from addEventListener() and make it look like this: tab.addEventListener('click', onclickForTab, false); This will call the onclickForTab() function as a click handler. We define the onclickForTab() function, in japxlate.js thus: //set up and display a newly tapped tab function onclickForTab(event) { //to prevent URL from changing and browse history building up event.preventDefault(); //-------tab display logic--- var lastTab = document.querySelector('li.current a'); //NOP if clicking current tab again if(lastTab == this)
  • 47. First things first: The tabbing mechanism 42 { return false; } lastTab.parentNode.className = ''; //undisplay this.parentNode.className = 'current'; //--------------------------- //-----content div display logic--- var lastDiv = document.querySelector('div.current'); lastDiv.className = ''; //undisplay var matchingDiv = this.getAttribute('data-div-id'); var thisDiv = document.getElementById(matchingDiv); thisDiv.className = 'current'; //----------- //get tab div id from tab link var divId = this.getAttribute('data-div-id'); } Let’s go through this code, which looks fiddly at first, but basically tinkers with CSS class names such that things turn on and off as we want. The first thing we do is the DOM standard preventDefault() which prevents the browser’s default action for the event from triggering. The default browser action for clicking on a link is to: 1. Change URL in address bar to that of link target 2. Add new URL to browsing history 3. Load new URL As our links are simply triggers to load tabs and not proper links, we don’t want any of these steps to happen. Step [2] is especially annoying. If we don’t call preventDefault() for our tab taps, if we open our app and click on the tabs ten times, we will have to use the device’s BACK button ten times to exit the app! Next we use querySelector() to get the single current tab link. Because ‘this’ in our click handler will be the clicked element, we can do a check to see if this is the same as the previous current tab. And if so, do a “no operation” (NOP). We then manipulate classnames to activate only the clicked tab. Similarly, we use querySelector() to get the currently active content <div>. We activate the content <div> for the clicked tab by retreiving data-div-id from the clicked <a> and using that to get the correct div.
  • 48. First things first: The tabbing mechanism 43 Anyway, this all works! Figure 24. Initial configureTabs() is working well Thinking deeper and keeping an open mind, there’s more to our tabs than just displaying the relevant content. A given tab might have to do some one-off initialising of a resource - perhaps a database. Or some per-load checking of, eg, network availability on the device. We also might like to add new tabs in future as users request more features. We might simply just want to change the default tab based on user complaints! We can cover all of these bases with a few simple steps. First, alter the bottom of onclickForTab() to look like: . . //get tab div id from tab link var divId = this.getAttribute('data-div-id'); onclickForNamedTab(divId); } onclickForTab() is a generic handler for any tab tap, but we are adding onclickForNamedTab() to handle tab specific initialisation. Put onclickForNamedTab() in japxlate.js and it looks like this:
  • 49. First things first: The tabbing mechanism 44 //Do the one-off loading and everytime setup for whichever tab function onclickForNamedTab(divId) { if(divId == 'discover') { onclickForTab_Discover(); } else if(divId == 'search') { onclickForTab_Search(); } else if(divId == 'write') { onclickForTab_Write(); } } We simply switch on the tab content <div> id, calling the appropriate onclickForTab_theTab(). Yes, you’ve guessed it, if you want to add more tabs to the app, you will have to update this switch case (and add the corresponding onclickForTab_theNewTab()). This function is a simple dispatcher to other functions that are going to do the actual one-off and per-load initialisations for tabs. For a “one-off” initialisation, we are going to have to somehow record which tabs have been opened so far. We’ll do this using a global variable. Eek! Global variables are not current best practice for JavaScript, but we’ll do it to keep this small and simple app, er, small and simple. Put this at the top of japxlate.js: //Has the first load of each tab happened yet? var global_pagesLoaded = {discover:false, search:false, write:false}; We can then check - and set - these values in our onclickForTab_theTabName() functions that our onclickForNamedTab() dispatcher calls. Let’s get started with the first of these functions for our Discover tab. Put this in japxlate.js: //One-off loading and each time setup for discover tab function onclickForTab_Discover() { //console.log('click on discover tab'); if(!global_pagesLoaded.discover) { firstLoadForTab_Discover(); } //each time setup to go here }
  • 50. First things first: The tabbing mechanism 45 We simply check if global_pagesLoaded.discover is false and if so call firstLoadForTab_- Discover(). We also have a space here for any “each time” setup of the Discover tab. Go ahead and create functions, using this one as a template, for the Search and Write tabs (do a copy paste and then change ‘Discover’ to ‘Search’ and ‘discover’ to ‘search’ and etc). We’ll modify these functions later if we need to. OK, we still need firstLoadForTab_Discover() which will perform one-off initialisation for the Discover tab. Do it like this, again in japxlate.js: //One-off loading for discover tab function firstLoadForTab_Discover() { //console.log('first load for discover tab'); global_pagesLoaded.discover = true; //one-off setup to go here } All we do is set global_pagesLoaded.discover to true so that this function does not get called again from onclickForTab_Discover() when the tab is tapped a subsequent time. At the moment this is just a placeholder for whatever we might need down the line. Like we just did for onclickForTab_*(), replicate this function for the Search and Write tabs. If we temporarily uncomment the console.log() calls, running this - and clicking tabs randomly - shows that we do indeed have a first load that fires only once and a click that fires each time. Figure 25. One-off tab loading is confirmed Done and dusted. Money in the bank. Move along, nothing to see here… right? Well there’s just one thing missing. If you’ve really really been paying attention and thinking one or two steps ahead perhaps, you may have noted that our setups (one-off and each time) for the default Search tab are only fired if we click off that tab and then back on it. Clearly this is not useful and whichever tab is set to be the default needs to have its setups run right off the bat. Let’s solve this problem by, on deviceready, calling a little function to retreive the current tab and calling our already existing onclickForNamedTab() dispatcher for that tab. Add a call to initialiseDefaultTab() at the bottom of receivedEvent() in index.js so that it now looks like this:
  • 51. First things first: The tabbing mechanism 46 // Update DOM on a Received Event receivedEvent: function(id) { console.log('Received Event: ' + id); configureTabs(); //load and show whatever we've set the initial tab to be initialiseDefaultTab(); } Then define initialiseDefaultTab() in japxlate.js thus: //Load and show our default initial tab function initialiseDefaultTab() { var defaultTab = document.querySelector('div.current'); var divId = defaultTab.id; onclickForNamedTab(divId); } We use querySelector() to get whichever content <div> has been set as current in the HTML markup. We could in theory select the tab that has been marked as current but, as that will be in sync with the content div anyway, it is academic. Congratulations, you have just built a working infrastructure for the Japxlate app! This is a good starting point for any simple PhoneGap app.
  • 52. 8. The Search tab 8.1 Layout and interface The Search tab - the first tab that the user will see when launching our app - is going to be a search form for the user to search our Japanese dictionary. It will also display any and all matching results in a scrollable area. We’ll have a rule that the user’s search query can be in Japanese as well as English. Not only will this increase the usefulness of our app, it will also enable a future “reversing” of the app to be localised for Japanese speakers wanting to learn English vocabulary. Let’s have another rule that they can type the Japanese or English query into the form in the same input box and without having to fiddle with radio buttons or other such inputs (which are a bit old hat for search forms anyway but especially cumbersome on mobile devices). With these rules and functionalities in mind, a wireframe of the Search tab might look like: Figure 26. Quick wireframe of the Search tab layout OK, let’s markup - and then style - the search form and the results space for dictionary queries. Mosey on down to http://www.ajaxload.info and make a “loading” spinner image (gif) for the Search tab. I made mine use the Japxlate signature red (#990000) and a transparent background. Download it and put it in /assets/www/img as spinner.gif. Let’s markup the form and results space - in index.html - like this:
  • 53. The Search tab 48 <div id="search" class="current"> <button type="button" id="search-button" style="float:right; width:45%; margin-righ t:1%;"> <img src="img/search.png"> Search <img id="button-spinner" src="img/spinner.gif" style="visibility:hidden;"> </button> <input type="text" id="search-query" placeholder="Japanese or English" size="40" style="width:45%; margin-left:1%;"> <br> <span id="loading-text"> [Loading core dictionary. This takes a while the first time. <img src="img/spinner.gif">] </span> <div id="results-wrapper"> <div id="search-results"> You can search by kanji, hiragana, katakana, English or romaji! </div> </div> </div> We float our search button right (which means that in the markup it has to come before things on the same line that would be visually to the left of it) but make it 1% (of total width) away from the edge for nice appearance. We reuse search.png as a button icon. We also include the spinner.gif that we just created but default it to visibility:hidden. Why not just display:none? Because with visibility:hidden, it is hidden but still takes up space in the layout flow. This means the layout won’t “jump” when we make it appear. We’ll switch this image’s visibility on and off programmatically. Then we’ve got our text input which uses the new HTML5 placeholder attribute to present a hint or instruction to the user about what kind of entry it expects. The text input is also 45% wide with an edge spacing of 1%. Why not just make both 50%? Because then they will touch in the middle which will end in tears with big fingers on a small display! We then have a “this will take a while” message and spinner that we will remove after one-off setup is complete. Finally we have a container for our search results - <div id=”search-results”> - which displays a default search hint. We also have a wrapper for the search results container - <div id=”results- wrapper”> - which is going to be the scroll viewport for search results. These two divs need the following styles in index.css:
  • 54. The Search tab 49 #results-wrapper { position:static; width:100%; margin-top:1em; /*space one <br>(ish) from bottom of search form*/ overflow:hidden; } #search-results { position:relative; /*we position this relative to its *normal* position*/ top:0; /*but set the normal top position anyway. We will*/ width:100%; /*change this top value to affect a scroll*/ } The keypoint here is position:relative; on the search-results div which means that we will be able to position it (ie. scroll it) relative to an unmoving parent - the results-wrapper div. Running this looks like: Figure 27. Initial appearance of the Search tab Not bad. Just two grumbles here. 1. The height of the text area is lacking and also it’s shorter than the button. Let’s even these two out. (Actually on my device the text input and the button don’t seem to be on the same baseline!)
  • 55. The Search tab 50 2. Icon for search is screwy again - let’s fix that like we fixed the tab icons. In index.css, change the existing: a img { vertical-align:middle; /*make more sensible relative to text baseline*/ } to: a img, button img { vertical-align:middle; /*make more sensible relative to text baseline*/ } Which covers (2). To fix (1), add this to index.css: input[type="text"], button { height:30px; margin:0; } Running looks like this:
  • 56. The Search tab 51 Figure 28. Improved appearance of the Search tab Better! 8.2 Creating the database Now, let’s also have a rule to the effect of dictionary searches working even when the mobile device is offline. That is to say the app must use some kind of local storage on the device itself or the WebView browser. Well, it turns out that the Android WebView supports something called Web SQL which is a small, local implementation of an SQL database (specifically SQLite) in the browser. We can load our Japanese dictionary into a client-side database and, based on the user’s search term, query it in whichever way we need to pull out matches. .. Important note about Web SQL Web SQL is an abandoned specification (see http://www.w3.org/TR/webdatabase/) that W3C no longer maintain, and I do not recommend that you use it going forward in your owns apps! W3C’s beef was that it was only being implemented using SQLite - obviously they aren’t in the business of standardising a piece of vendor lock in! For similar reasons Mozilla (ie. Firefox browser) have chosen not to implement it right from the start. I do kind of agree that bringing a heavy server-side thing to the client is a bit of an odd move. In fact, traditional SQL on the
  • 57. The Search tab 52 .. back-end is somewhat in crisis itself these days in the world of NoSQL datastores. Though it is very useful for mobile apps that might not be online and need to work with some data. Why are we using it for this tutorial? Somewhat for historical reasons but also because I know it will be perfect for fuzzy text searching. I know from experience that it will “just work”. When using PhoneGap we are lucky too because “Cordova provides access to both interfaces (Web SQL and something else called Web Storage) for the minority of devices that don’t already support them. Otherwise the built- in implementations apply.” What would be some alternatives? Ignoring PhoneGap and the world of mobile apps, Indexed DB (a W3C standard at http://www.w3.org/TR/IndexedDB/) looks to be picking up steam. Though caniuse.com tells me that support is currently less than that of Web SQL. Also it hasn’t made its way into PhoneGap at the time of writing. Indexed DB mirrors the more modern style of NoSQL databases closely. I hope that future versions of the app (and this tutorial) can use Indexed DB. PhoneGap v3.3.0 now supports Indexed DB, but only if the underlying WebView supports it. At the time of writing this means only Windows Phone 8 and BlackBerry 10. PhoneGap’s (well actually Cordova’s) Web SQL docs are at http://docs.phonegap.com/en/3.1.0/cordova_storage_storage.md.html As you can see, it’s a fairly small implementation of an SQL database. But writing for it in JavaScript with callbacks was a novelty for this grizzled MySQL hacker! OK, let’s crack on now with Web SQL initialisation for the first load of the Search tab. Stick this cheeky call - to a function we’re about to create - at the bottom of firstLoadForTab_Search() in japxlate.js: tryPopulateDB(); Let’s create this function, and other functions to do with general Web SQL setup, in a new file in /assets/www/js called websql_core.js. Create this file, and the first function we’ll put in it is the tryPopulateDB() we’ve just referenced. It will look like this:
  • 58. The Search tab 53 //Open / create the "Japxlate" Web SQL database and - if it's not already //present - create and populate the "edict" table function tryPopulateDB() { //version 1.0, 4 megabytes var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024); db.transaction(checkDB); //only populate edict table if it not already exist } PRO TIP: The Cordova docs on Web SQL are going to be very useful to reference when following this chapter. They are at http://docs.phonegap.com/en/3.1.0/cordova_- storage_storage.md.html. The same page for PhoneGap v3.3.0 removes the Web SQL reference, which to be honest had at least one mistake in it, and instead points you to have a look at http://www.html5rocks.com/en/features/storage. We open a Web SQL database called Japxlate, at version 1.0, with a display name of “Japxlate DB” and a size of 4 megabytes. I know from tinkering with the dictionary database for the @japxlate Twitter channel that the core dictionary definitions will fit in 4 megabytes with a bit to spare. Then we call transaction() on the returned database to run the query or queries in the checkDB() function that we’re about to implement. Now’s a good time to talk about the schema we’ll use for the dictionary table. We’ll call the table “edict” as that’s the name of the Japanese dictionary that powers it (at http://www.csse.monash.edu.au/∼jwb/wwwjdicinf.html#dicfil_tag) and the fields will be: edict(id unique, kanji, kana, definition) “id” will be an integer and a unique key to each record. “kanji” will hold the Chinese characters that the word is written in. “kana” will hold the Japanese phonetic script that the word is written in. Finally “definition” will hold one or more English language definitions for the word, separated by ‘/’. Our checkDB() function needs to know if the edict table exists and is full. If not, create it and fill it. The checkDB() function will receive a SQLTransaction object as a parameter from db.transaction(). Again in websql_core.js, make checkDB() look like this:
  • 59. The Search tab 54 //Check if "edict" table exists and has records function checkDB(tx) { //console.log('checkDB()'); tx.executeSql('SELECT COUNT(id) AS count FROM edict', [], successCheckDB, errorChec kDB); } We call executeSql() on the received SQLTransaction object which needs at least an SQL query as its first argument (and parameter values as the 2nd parameter if the query in the first argument uses parameter binding), but can optionally take both a success and failure callback as 3rd and 4th parameter respectively. Here we run a very simple query to get the count of rows - by id - in the edict table. This query will throw an error if the edict table does not exist (but not if it exists and is empty which is a condition we will knowingly ignore for this simple app). We don’t use parameter binding in this query so we provide an empty array as the 2nd parameter simply because we need to “get” to the 3rd and 4th parameters. We specify an error and a success callback. Should the query fail we can assume that the table does not exist and therefore needs to be created and populated. Let’s look at the success callback first as it’s simpler and only has to clear the “database loading” message: //Callback for if checkDB() succeeds - ie. "edict" table present and full //SO clear the "database loading" message function successCheckDB(tx, results) { //console.log('edict already loaded'); document.getElementById('loading-text').innerHTML = ''; } Pretty easy and not worth explaining other than to point out that the callback function receives an SQLTransaction and an SQLResultSet object respectively. Let’s get started on the error callback: //Callback for if checkDB() fails - ie. no "edict" table //SO create it and fill it function errorCheckDB(transaction, error) { console.log('edict table not exist - will create and fill'); //here we need to do something to fill the table } This code so far will run without errors (but don’t forget include websql_core.js from index.html (above the japxlate.js include)) but won’t do anything useful. It will get to the “edict table not exist - will create and fill” log message and then stop. In the error callback, we need to run another transaction on the Japxlate database which will load all the dictionary data we need. Change errorCheckDB() to look like this:
  • 60. The Search tab 55 //Callback for if checkDB() fails - ie. no "edict" table //SO create it and fill it function errorCheckDB(transaction, error) { console.log('edict table not exist - will create and fill'); //version 1.0, 4 megabytes var db = window.openDatabase("Japxlate", "1.0", "Japxlate DB", 4 * 1024 * 1024); db.transaction(populateDB, errorWebSQL, successPopulate); } We open the same Japxlate database and try to run the populateDB() queries on it. We have new success and error callbacks. populateDB() looks like this: //Create and fill the "edict" table function populateDB(tx) { console.log('creating and filling edict table'); //DROP if present (ie. because it's present but empty) tx.executeSql('DROP TABLE IF EXISTS edict'); //create tx.executeSql('CREATE TABLE IF NOT EXISTS edict(id unique, kanji, kana, definition) '); websqlEdictInserts(tx); //see websql_edict_inserts.js } We create the table according to our schema - DROPing it first just in case and so the CREATE doesn’t fail. Finally we call websqlEdictInserts() which is a function we’ll put in another JavaScript file. The websqlEdictInserts() function accepts an SQLTransaction object and essentially runs a huge list of INSERT queries on it to populate our table. This function isn’t very do-at-homeable because it’s basically a dump of the most common words from the @japxlate Twitter feed’s database. If you are following this tutorial step by step, please get the file /js/websql_edict_inserts.js from the app’s GitHub repository and stick it in your /assets/www/js folder. To explain it a little bit more, here’s an excerpt from /js/websql_edict_inserts.js:
  • 61. The Search tab 56 function websqlEdictInserts(tx) { tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(5,",, /curry/rice and curry/)'); tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(21,,, /to blow (one's nose)/)'); tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(119,, ,/1000 yen/)'); tx.executeSql('INSERT INTO edict(id, kanji, kana, definition) VALUES(138,, ,/ten percent/)'); . . } Note that the ID numbers aren’t in sequence because these words are the most common 20,000 or so words from @japxlate’s Edict dictionary which has nearly 200,000 entries! OK, that’s populateDB() in the bag. But don’t forget errorCheckDB()’s custom error and success callbacks. Let’s do the error callback first: //Generic SQLError handler (for both db.transaction() and tx.executeSQL()) function errorWebSQL(transactionOrError, errorOrNull) { var error = null; if(typeof transactionOrError == 'SQLTransaction') { //from tx.executeSQL() error = errorOrNull; } else { //from db.transaction() error = transactionOrError; } console.log(error); //error is now an SQLError object alert(Error processing SQL: + error.code); } Ouch! This looks a bit over-complicated. What’s going on? Well, I didn’t realise at first, and I only discovered it on a hunch, but we can reference error callbacks from both the database.transaction() and transaction.executeSQL() methods (as we are already doing) but in each case they will receive different parameters! The PhoneGap / Cordova docs for the Web SQL API - at the time of writing - don’t seem to realise this and actually are therefore incorrect. The PhoneGap v3.3.0 docs remove the entire Web SQL reference section. This is something of a generic error callback and so we pull some strings to handle both cases. Error callbacks as called from database.transaction() will receive (SQLError), and error callbacks called from transaction.executeSQL() will receive (SQLTransaction, SQLError).
  • 62. The Search tab 57 We simply alert out the code property of the received SQLError object. This is going to be our recyclable Web SQL error handler going forward with the app. The success callback for errorCheckDB() is going to do the same as the success callback for checkDB() (which is successCheckDB()): //Callback for if errorCheckDB() succeeds - ie. edict table populated OK function successPopulate() { console.log('finished loading edict'); document.getElementById('loading-text').innerHTML = ''; } Include websql_edict_inserts.js from index.html (above the include for websql_core.js) and we are ready to go for a spin! On first run, the “database loading” message and spinner take a few seconds to disappear, and the log messages indicate database loading success. It looks like this: Figure 29. First run of app with Web SQL database loading Go ahead and run the app again after exiting it, the 2nd time around feels kind of faster right? Let’s check the logs:
  • 63. The Search tab 58 Figure 30. Second, faster run of app with Web SQL database loading Woah! That’s right, Web SQL databases that you’ve created persist over multiple sessions of the app (or browser). Pretty hot and tasty! This is a great reason why Web SQL, as abandoned and awkward as it is, is really useful for mobile WebView apps as it can be used for saving things offline. 8.3 Querying the database Right, so that’s the database created, the table created, and the table filled. Phew! We’re coming to the meat and bones of it now which is getting results from the database based on the user’s search query. This will involve a bit of work on the frontend interface and a lot of work on the backend. As we are kind of frazzled with Web SQL things right now, let’s get to work on the frontend interface first. Let’s make a new JavaScript file in /assets/www/js called search_interface.js to hold anything to do with the frontend look and feel of searching. Right, one of the main things we’ll want to do is to put search results from the database into the container div in our markup. Let’s add a function to do this:
  • 64. The Search tab 59 //Put the matching search results (which could be zero matches) on the page function putResultsOnPage(results) { //get search results div var theDiv = document.getElementById('search-results'); //clear current content theDiv.innerHTML = ''; //might be no matches if(results.rows.length === 0) { theDiv.innerHTML = 'No matches found in the common words dictionary. Tweet @japxlate yourAdvancedWord for advanced word definitions.'; buttonSpinnerVisible(false); //stop the loading spinner return; } //some results so loop through and print for(var loop = 0; loop results.rows.length; loop++) { var item = results.rows.item(loop); var var theRomaji = item.kana; //TODO var formattedDefinition = format_slashes(item.definition); var defText = item.kanji + ' / ' + item.kana + ' (' + theRomaji + ') / ' + form attedDefinition; defText = defText.replace(new RegExp(global_searchTerm, 'ig'), 'span style=co lor:#990000;$/span'); var defLine = 'img src=img/j.png style=vertical-align:middle; ' + defText + 'hr'; //var defLine = 'p class=def-line ' + defText + '/p'; //had CSS styling i ssues (mostly text overflow) theDiv.innerHTML += defLine; } buttonSpinnerVisible(false); //stop the loading spinner } We’ll expect to be passed an SQLResultSet object which will come from a successful query on our Web SQL database. First we reset the current (ie. old) results by setting the container div’s innerHTML property to empty. We then cover a scenario of no matches by printing a “no matches” message (with a plug for the @japxlate Twitter bot!). Note that you can split up very long quoted strings in JavaScript by ending lines with a ‘’. We then stop the “searching” spinner by calling the buttonSpinnerVisible() function with a parameter of false. We’ll write
  • 65. The Search tab 60 this function shortly and it’s basically a way to switch the “searching” spinner on and off. We then return. .. document.getElementById('some-id') versus document.querySelector('#some-id') You may be wondering why, for single elements, I am using document.getElementById('some-id') and not the new fangled document.querySelector('#some-id'). Well it’s true that these will both return the same element, and it’s true that getElementById() is a much older piece of XML DOM, but the issue - at the time of writing - is one of performance (and perhaps getElementById() is a teeny tiny bit more readable). After some benchmarking experiments in desktop Chrome (using the mega useful console.time() and console.timeEnd() as at https://developers.google.com/chrome-developer-tools/docs/console-api#consoletimelabel) I saw that, for single elements, getElementById() was much faster than querySelector(). Out of curiosity I also tested jQuery’s $('#some-id) (which returns a jQuery-specific list of nodes) and found this to be much slower than the browser’s native querySelector(). Of note is that the new jQuery v2.0 was much faster than v1.x for the same selector (though still slower than querySelector()). Now, if we’re still in the function we’ll have some results. We loop over and retrieve the results using the SQLResultSet object’s rows.length property and rows.item(itemIndex) method. What we do in the loop looks fiddly, but all we are doing is replicating the style of definition lines that @japxlate uses. If you remember the snippet of websql_edict_inserts.js that we looked at earlier, the format of the “definition” field in the database is “/definition one/definition two/definition three/”. We want to space these multiple definitions out a bit more and remove the lead and tail slashes; for that we’ll use a helper function called format_slashes() which also goes in this file: //Clean up the EDICT definition line that we get from our Web SQL DB //For example, /one/two/three/ -- one; two; three function format_slashes(slashesString) { //remove leading and trailing '/' characters var string = slashesString.replace(/^//, ''); //leading var string = string.replace(//$/, ''); //trailing //change remaining '/' characters to a semicolon with space return string.replace(///g, '; '); } We use JavaScript’s core replace() method to change the slashes based on regex matching. We replace single lead and tail slashes with an empty string. We replace globally (the ‘g’ modifier after the regex) all remaining slashes with a semicolon followed by space. We return the modified string.
  • 66. The Search tab 61 OK, let’s come back to explaining putResultsOnPage(). We create in defText a nicely formatted definition line. We use String.replace() on this definition line to highlight the user’s search term in our trademark red. For this we use global_searchTerm which we’ll define a bit later on. buttonSpinnerVisible() is a simple CSS style toggler that also goes in search_interface.js and looks like this: //Toggle for search button's loading spinner function buttonSpinnerVisible(visible) { var spinner = document.getElementById('button-spinner'); if(visible) { spinner.style.visibility = 'visible'; } else { spinner.style.visibility = 'hidden'; } } Remember in websql_core.js we did this a couple of times: document.getElementById('loading-text').innerHTML = ''; As this is manipulating the search interface, let’s refactor this as a function in search_- interface.js. Let’s call it clearLoadingMessage(): function clearLoadingMessage() { document.getElementById('loading-text').innerHTML = ''; } Then replace the two document.getElementById('loading-text').innerHTML = ''; lines in websql_core.js with calls to clearLoadingMessage();. OK, in search_interface.js we now have all of the functions that other functions might call to update the interface for database searching, but we are missing something here. The user! We need to catch tap events on the Search button and then use their entered query to search the database and return results. Let’s start where the user starts - the Search button. Let’s add a click handler. Add a call to: configureSearchButton(); at the bottom of receivedEvent() in good old index.js. We define configureSearchButton() in search_interface.js:
  • 67. The Search tab 62 //search button clickability function configureSearchButton() { document.getElementById('search-button').addEventListener('click', onclickForSearch Button, false); } We define onclickForSearchButton() as the click handler for the search button. onclickForSearchButton(), also in search_interface.js, is like this: //Perform a dictionary search for entered query function onclickForSearchButton(event) { var q = document.getElementById('search-query').value; //some kanji searches are going to be legitimately only one char if(q.length 1) { return; } buttonSpinnerVisible(true); var matches = doEdictQueryOn(q); } We get the user’s entered search query and - on the condition that it’s at least one character long - we pass it to doEdictQueryOn() after displaying the “searching” spinner. doEdictQueryOn() is a function that we haven’t written yet that will need a whole ‘nother JavaScript file. We’ve already got quite a few JavaScript files, but this is keeping it nice and modular. Create websql_query.js in /assets/www/js and add doEdictQueryOn() thus: //function to query the database based on whatever query string function doEdictQueryOn(newQ) { //TODO } What? It’s empty! Yes, we’re going to take a breather now and plan what we’re going to do next. A keyboard break if you like. Remember back in the Layout and interface section of this chapter when we laid down some rules about our app? It’s time to recap those now as it will affect how we implement dictionary searching. We said we’ll stick to a rule “that the user’s search query can be in Japanese as well as English”. Obviously we’ll then go down different search query routes depending on the entered language. So we need a way to detect if the query is Japanese or English.
  • 68. The Search tab 63 .. Why two search querying routes? We could get away with not detecting the input query’s language by having this kind of logic: “Assume the query is English, do a search, if no results then assume it’s Japanese and search again” Which has two problems. We have to make an assumption about how our app is being mostly used. (Admittedly we could change the assumption if we find out it’s wrong.) Another problem is performance - we may be searching unnecessarily. A very simple, and linguistically incorrect!, way to do this is to see if we have multibyte characters in our query string or not. We’ve set our HTML page to be UTF-8. UTF-8 is interesting because it’s a flavour of Unicode that’s backwards compatible with good ol’ ASCII. ASCII can be utf-8, but so can Japanese! But ASCII won’t set the right bits in each byte to be considered a multibyte stream. Something we can use to our advantage is that for ASCII, the length of a string in bytes will also be the length of that string in characters. For multibyte utf-8 strings, this will not be the case and the byte length will be greater than the character length. Let’s implement an is_mb() (“mb” meaning “multibyte”) check using this knowledge. Keeping things modular, and realising that we are going to need functions soon for Japanese language handling, make a new file in /assets/www/js called linguistics.js. Add is_mb() thus: //Does the given utf8 string have multibyte characters or not? function is_mb(utf8String) { return utf8String.length != mb_bytelen(utf8String); } Here we compare a string’s length in characters (using the length property - JavaScript operates internally with utf-16 unicode) with its length in bytes. mb_bytelen() is the key function here that we need to write. It will give us the byte length for a utf8 string. Put it also in linguistics.js: //Get length in BYTES of a utf8 string function mb_bytelen(utf8String) { //Matches only the 10.. bytes that are non-initial characters //in a multi-byte sequence. var m = encodeURIComponent(utf8String).match(/%[89ABab]/g); return utf8String.length + (m ? m.length : 0); } In utf-8, everything is a sequence of bytes. For an ASCII character, one byte is the full sequence - that byte is the character. But it allows for multibyte characters by the initial byte in
  • 69. The Search tab 64 that character’s sequence of bytes setting a special bit. This special bit tells the browser (or programming language or text editor etc) that “there’s more to come!” and the browser adds the remaining bytes in the sequence to get the full value for that character. The remaning bytes also set a special bit so the browser knows when that particular character has all of its bytes read. For the gory details please see http://en.wikipedia.org/wiki/UTF-8. So what we are doing in mb_bytelen() is adding the length of the string in characters to the count of non-initial character sequence bytes. This will give us the total byte length for any utf8 string - containing multibyte characters or not! OK, is_mb() is one important tool for our database querying logic in the bag. Using it, let’s think more about our query logic with some pseudocode: if(is_mb(searchTerm)) { //searchTerm is Japanese (or at least multibyte) // //[1] exact kanji match //[2] exact kana match } else { //searchTerm is English or, as last resort, romaji // //[1] exact definition match //[2] partial definition match //[3] exact romaji match (on kana field) } So, if we detect multibyte characters in the search term, we assume it is Japanese and try to match it exactly against, first, the kanji field of the words in our database. Then, if that produces no results, we try to match it exactly against the kana field. This priority order is realistic because kanji (Chinese idiogrammic characters) are the “correct” way to write a Japanese word. The kana is just the way to pronounce those Chinese characters. Though note that some words are kana only and don’t hava a kanji. We assume that the search term is in English if it contains no multibyte characters. Then we focus on the definition field of our database. Remember that definition entries look like this: /uncertain/vague/ambiguous/ Multiple definitions are separated by slashes. So our most relevant results (query [1] of the English route) would be to find the search term exactly as one of these definitions. A search for “vague” would match the above definition, for example. If that produces no results, we query for partial matches of the search term in these definitions. For example a search for “director” will match a definition of /company director/board member/. Finally, if we still have no results, we can take a gamble and assume that the user has entered a term in romaji (which is Japanese written in abc like “sayonara” or “moshimoshi”). For this we’ll have to convert the search term into phonetic kana and query for a matching kana field. So this one needs a bit more work programmatically. Note that if we go down the Japanese route, and get no results at the end, we don’t then proceed down the English route (and vice-versa).
  • 70. The Search tab 65 Let’s implement the Japanese route first as the queries are easier. We’ll go back to working in websql_search.js. Remember from writing websql_core.js how Web SQL works with callback chains? Well, with this in mind (and don’t get me wrong, there are better and cleverer ways to do this) we’re going to stick a couple of global variables at the top of websql_search.js so that all callback functions can access them: //User's search term as a global variable (so we can access it from all the different c allbacks). Hmmm... var global_searchTerm = null; //Maximum number of search results to return for any query var global_maxResultsCount = 40; Now make doEdictQueryOn() look like this: function doEdictQueryOn(newQ) { //set global_searchTerm global_searchTerm = newQ; //version 1.0, 4 megabytes var db = window.openDatabase(Japxlate, 1.0, Japxlate DB, 4 * 1024 * 1024); if(is_mb(global_searchTerm)) //Japanese (or at least multibyte) { //console.log('doing as japanese - kanji'); db.transaction(queryDB_ja, errorWebSQL); } else //ie. English (or - as last resort - romaji) { console.log('doing as english - exact'); } } We simply save the search term into global_searchTerm, open the database and attempt the queryDB_ja() query function (using our generic Web SQL error handler). We’ll come back to the else section for English later, but in the meantime let’s make queryDB_ja() which is like this: