In this tutorial, you create a fully functional employee directory application with PhoneGap. You will learn:
- How to use different local data storage strategies.
- How to use several PhoneGap APIs such as Geolocation, Contacts, and Camera.
- How to handle specific mobile problems such as touch events, scrolling, styling, page transitions, etc.
- How to build an application using a single page architecture and HTML templates.
- How to build (compile and package) an application for 6 platforms using PhoneGap Build.
To complete this tutorial, all you need is a code editor, a modern browser, and a connection to the Internet. A working knowledge of HTML and JavaScript is assumed, but you don’t need to be a JavaScript guru.
Setting Up
- Download the assets for the workshop here.
- Unzip the file anywhere on your file system.
- If your code editor allows you to “open a directory”, open the phonegap-workshop-master directory.
- Follow the instructions below.
Part 1: Choosing a Local Storage Option
Step 1: Explore different persistence mechansisms
Open the following files in phonegap-workshop-master/js/storage, and explore the different persistence stores they define:
- memory-store.js (MemoryStore)
- ls-store.js (LocalStorageStore)
- websql-store.js (WebSqlStore)
Step 2: Test the application with different persistence mechanisms
To change the local persistence mechanism for the application:
- In index.html: add a script tag for the corresponding .js file: memory-store.js, ls-store.js, or websql-store.js.
- In js/main.js: Instantiate the specific store in the initialize() function of the app object: MemoryStore, LocalStorageStore, or WebSqlStore.
- To test the application, open index.html in your browser, or simply double-click index.html on your file system. Type a few characters in the search box to search employees by name. Clicking an employee link doesn’t produce any result at this time.
Part 2: Building with PhoneGap Build
- If you don’t already have one, create an account on http://build.phonegap.com.
- Click the “new app” button to create a new application on PhoneGap Build.
- Either point to a GitHub repository where you push your code for this workshop, or zip up your phonegap-workshop directory and upload it to PhoneGap Build.
- Click the Ready to build button.
The iOS button will immediately turn red because the iOS build requires that you upload your Apple Developer certificate and an application provisioning profile. You can find more information here if you haven’t already signed up for the Apple Developer Program. If you don’t have an iOS device, or if you are not ready to upload your developer certificate, you can skip step 5 and keep running the application in the browser or a non iOS device.
- To upload your Apple developer certificate and your application provisioning profile:
- Click the red iOS button.
- Select “add a key” in the “No key selected” dropdown.
- Provide a title for your developer certificate/provisioning profile combination (for example: EmployeeDirectory), select your developer certificate and provisioning profile, enter your developer certificate password, and click “submit key”.
- Go back to the list of apps. Click the iOS button for your application again. Select your newly added key in the iOS dropdown. The iOS build will start automatically.
- When the build process completes, use a QR Code reader app to install the Employee Directory application on your device.
To fine tune your build preferences:
- In the phonegap-workshop directory, create a file namedconfig.xml file defined as follows (make the necessary adjustments for id, author, etc.):
1234567891011121314151617181920212223242526
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
id
=
"org.coenraets.employeedirectory"
versionCode
=
"10"
version
=
"1.1.0"
>
<
name
>Employee Directory</
name
>
<
description
>
A simple employee directory application
</
description
>
Christophe Coenraets
</
author
>
</
widget
>
- If you used the GitHub approach, sync with GitHub and click the Update Code button in PhoneGap Build.
If you used the zip file approach, zip up your phonegap-workshop directory and upload the new version to PhoneGap Build
Part 3: Using Native Notification
A default webview alert gives away the fact that your application is not native. In this section, we set up the basic infrastructure to display native alerts when the application is running on a device, and fall back to default browser alerts when running in the browser.
- In index.html, add the following script tag (as the first script tag at the bottom of the body):
1
<
script
src
=
"phonegap.js"
></
script
>
This instructs PhoneGap Build to inject a platform specific version of phonegap.js at build time. In other words, phonegaps.js doesn’t need to be (and shouldn’t be) present in your project folder.
- In main.js, define a function named showAlert() inside the app object. If navigator.notification is available, use its alert() function. Otherwise, use the default browser alert() function.
1234567
showAlert:
function
(message, title) {
if
(navigator.notification) {
navigator.notification.alert(message,
null
, title,
'OK'
);
}
else
{
alert(title ? (title +
": "
+ message) : message);
}
},
- Test the notification logic by displaying a message when the application store has been initialized: Pass an anonymous callback function as an argument to the constructor of the persistence store (the store will call this function after it has successfully initialized). In the anonymous function, invoke the showAlert() function.
1234567
initialize:
function
() {
var
self =
this
;
this
.store =
new
MemoryStore(
function
() {
self.showAlert(
'Store Initialized'
,
'Info'
);
});
$(
'.search-key'
).on(
'keyup'
, $.proxy(
this
.findByName,
this
));
}
- Test the application: When you run the application in the browser, you should see a standard browser alert. When you run the application on your device, you should see a native alert.
Part 4: Setting Up a Single Page Application
A single page application is a web application that lives within a single HTML page. The “views” of the application are injected into- and removed from the DOM as needed as the user navigates through the app. A single page application architecture is particularly well suited for mobile apps:
- The absence of continual page refreshes provides a more fluid / closer to native experience.
- The UI is entirely created at the client-side with no dependency on a server to create the UI, making it an ideal architecture for applications that work offline.
In this section, we set up the basic infrastructure to turn Employee Directory into a single page application.
- In index.html: remove the HTML markup inside the body tag (with the exception of the script tags).
- In main.js, define a function named renderHomeView() inside the app object. Implement the function to programmatically add the Home View markup to the body element.
12345678910
renderHomeView:
function
() {
var
html =
"<div class='header'><h1>Home</h1></div>"
+
"<div class='search-view'>"
+
"<input class='search-key'/>"
+
"<ul class='employee-list'></ul>"
+
"</div>"
$(
'body'
).html(html);
$(
'.search-key'
).on(
'keyup'
, $.proxy(
this
.findByName,
this
));
},
- Modify the initialize() function of the app object. In the anonymous callback function of the store constructor, call the renderHomeView() function to programmatically display the Home View.
123456
initialize:
function
() {
var
self =
this
;
this
.store =
new
MemoryStore(
function
() {
self.renderHomeView();
});
}
Part 5: Using Handlebar Templates
Writing HTML fragments in JavaScript and programmatically inserting them into the DOM is tedious. It makes your application harder to write and harder to maintain. HTML templates address this issue by decoupling the UI definition (HTML markup) from your code. There are a number of great HTML template solutions: Mustache.js, Handlebar.js, and Underscore.js to name a few.
In this section, we create two templates to streamline the code of the Employee Directory application. We use Handlebar.js but the smae result can be achieved using the other HTML template solutions.
Modify index.html as follows:
- Add a script tag to include the handlebar.js library:
1
<
script
src
=
"lib/handlebars.js"
></
script
>
- Create an HTML template to render the Home View. Add this script tag as the first child of the body tag:
12345
<
script
id
=
"home-tpl"
type
=
"text/x-handlebars-template"
>
<
div
class
=
'header'
><
h1
>Home</
h1
></
div
>
<
div
class
=
'search-bar'
><
input
class
=
'search-key'
type
=
"text"
/></
div
>
<
ul
class
=
'employee-list'
></
ul
>
</
script
>
- Create an HTML template to render the employee list items. Add this script tag immediately after the previous one:
12345
<
script
id
=
"employee-li-tpl"
type
=
"text/x-handlebars-template"
>
{{#.}}
<
li
><
a
href
=
"#employees/{{this.id}}"
>{{this.firstName}} {{this.lastName}}<
br
/>{{this.title}}</
a
></
li
>
{{/.}}
</
script
>
Modify main.js as follows:
- In the initialize() function of the app object, add the code to compile the two templates defined above:
12
this
.homeTpl = Handlebars.compile($(
"#home-tpl"
).html());
this
.employeeLiTpl = Handlebars.compile($(
"#employee-li-tpl"
).html());
- Modify renderHomeView() to use the homeTpl template instead of the inline HTML:
1234
renderHomeView:
function
() {
$(
'body'
).html(
this
.homeTpl());
$(
'.search-key'
).on(
'keyup'
, $.proxy(
this
.findByName,
this
));
},
- Modify findByName() to use the employeeLiTpl template instead of the inline HTML:
123456
findByName:
function
() {
var
self =
this
;
this
.store.findByName($(
'.search-key'
).val(),
function
(employees) {
$(
'.employee-list'
).html(self.employeeLiTpl(employees));
});
},
- Test the application.
Part 6: Creating a View Class
It’s time to provide our application with some structure. If we keep adding all the core functions of the application to the app object, it will very quickly grow out of control. In this section we create a HomeView object that encapsulates the logic to create and render the Home view.
Step 1: Create the HomeView Class
- Create a file called HomeView.js in the js directory, and define a HomeView class implemented as follows:
1234
var
HomeView =
function
(store) {
}
- Add the two templates as static members of HomeView.
1234567
var
HomeView =
function
(store) {
}
HomeView.template = Handlebars.compile($(
"#home-tpl"
).html());
HomeView.liTemplate = Handlebars.compile($(
"#employee-li-tpl"
).html());
- Define an initialize() function inside the HomeView class. Define a div wrapper for the view. The div wrapper is used to attach the view-related events. Invoke the initialize() function inside the HomeView constructor function.
1234567891011121314
var
HomeView =
function
(store) {
this
.initialize =
function
() {
// Define a div wrapper for the view. The div wrapper is used to attach events.
this
.el = $(
'<div/>'
);
this
.el.on(
'keyup'
,
'.search-key'
,
this
.findByName);
};
this
.initialize();
}
HomeView.template = Handlebars.compile($(
"#home-tpl"
).html());
HomeView.liTemplate = Handlebars.compile($(
"#employee-li-tpl"
).html());
- Move the renderHomeView() function from the app object to the HomeView class. To keep the view reusable, attach the html to the div wrapper (this.el) instead of the document body. Because the function is now encapsulated in the HomeView class, you can also rename it from renderHomeView() to just render().
1234
this
.render =
function
() {
this
.el.html(HomeView.template());
return
this
;
};
- Move the findByName() function from the app object to the HomeView class.
12345
this
.findByName =
function
() {
store.findByName($(
'.search-key'
).val(),
function
(employees) {
$(
'.employee-list'
).html(HomeView.liTemplate(employees));
});
};
Step 2: Using the HomeView class
- In index.html, add a script tag to include HomeView.js (just before the script tag for main.js):
1
<
script
src
=
"js/HomeView.js"
></
script
>
- Remove the renderHomeView() function from the app object.
- Remove the findByName() function from the app object.
- Modify the initialize function() to display the Home View using the HomeView class:
123456
initialize:
function
() {
var
self =
this
;
this
.store =
new
MemoryStore(
function
() {
$(
'body'
).html(
new
HomeView(self.store).render().el);
});
}
Part 7: Adding Styles and Touch-Based Scrolling
Step 1: Style the Application
- Add the Source Sans Pro font definition to the head of index.html
1
<
script
src
=
"css/source-sans-pro.js"
></
script
>
Source Sans Pro is part of the free Adobe Edge Web Fonts.
- Add styles.css to the head of index.html
1
<
link
href
=
"css/styles.css"
rel
=
"stylesheet"
>
- In index.html, modify the home-tpl template: change the search-key input type from text to search.
- Test the application. Specifically, test the list behavior when the list is bigger than the browser window (or the screen)
Step 2: Native Scrolling Approach
- Modify the home-tpl template in index.html. Add a div wrapper with a scroll class around the ul element with a scroll:
12345
<
script
id
=
"home-tpl"
type
=
"text/x-handlebars-template"
>
<
div
class
=
'header'
><
h1
>Home</
h1
></
div
>
<
div
class
=
'search-bar'
><
input
class
=
'search-key'
type
=
"search"
/></
div
>
<
div
class
=
"scroll"
><
ul
class
=
'employee-list'
></
ul
></
div
>
</
script
>
- Add the following class definition to css/styles.css:
123456789
.
scroll
{
overflow
:
auto
;
-webkit-overflow-scrolling: touch;
position
:
absolute
;
top
:
84px
;
bottom
:
0px
;
left
:
0px
;
right
:
0px
;
}
Step 3: iScroll Approach
- Add a script tag to include the iscroll.js library:
1
<
script
src
=
"lib/iscroll.js"
></
script
>
- In HomeView.js, modify the findByName() function: Instantiate an iScroll object to scroll the list of employees returned. If the iScroll object already exists (), simply refresh it to adapt it to the new size of the list.
123456789101112
this
.findByName =
function
() {
store.findByName($(
'.search-key'
).val(),
function
(employees) {
$(
'.employee-list'
).html(HomeView.liTemplate(employees));
if
(self.iscroll) {
console.log(
'Refresh iScroll'
);
self.iscroll.refresh();
}
else
{
console.log(
'New iScroll'
);
self.iscroll =
new
iScroll($(
'.scroll'
, self.el)[0], {hScrollbar:
false
, vScrollbar:
false
});
}
});
};
Part 8: Highlighting Tapped or Clicked UI Elements
- In styles.css, add a tappable-active class definition for tapped or clicked list item links. The class simply highlights the item with a blue background:
1234
li>a.tappable-active {
color
:
#fff
;
background-color
:
#4286f5
;
}
- In main.js, define a registerEvents() function inside the app object. Add a the tappable_active class to the selected (tapped or clicked) list item:
123456789101112131415161718192021
registerEvents:
function
() {
var
self =
this
;
// Check of browser supports touch events...
if
(document.documentElement.hasOwnProperty(
'ontouchstart'
)) {
// ... if yes: register touch event listener to change the "selected" state of the item
$(
'body'
).on(
'touchstart'
,
'a'
,
function
(event) {
$(event.target).addClass(
'tappable-active'
);
});
$(
'body'
).on(
'touchend'
,
'a'
,
function
(event) {
$(event.target).removeClass(
'tappable-active'
);
});
}
else
{
// ... if not: register mouse events instead
$(
'body'
).on(
'mousedown'
,
'a'
,
function
(event) {
$(event.target).addClass(
'tappable-active'
);
});
$(
'body'
).on(
'mouseup'
,
'a'
,
function
(event) {
$(event.target).removeClass(
'tappable-active'
);
});
}
},
- Invoke the registerEvents() function from within the app object’s initialize() function.
- Test the application.
Part 9: View Routing
In this section, we add an employee details view. Since the application now has more than one view, we also add a simple view routing mechanism that uses the hash tag to determine whether to display the home view or the details view for a specific employee.
Step 1: Create the employee template
Open index.html and add a template to render a detailed employee view:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
< script id = "employee-tpl" type = "text/x-handlebars-template" > < div class = 'header' >< a href = '#' class = "button header-button header-button-left" >Back</ a >< h1 >Details</ h1 ></ div > < div class = 'details' > < img class = 'employee-image' src = 'img/{{firstName}}_{{lastName}}.jpg' /> < h1 >{{firstName}} {{lastName}}</ h1 > < h2 >{{title}}</ h2 > < span class = "location" ></ span > < ul > < li >< a href = "tel:{{officePhone}}" >Call Office< br />{{officePhone}}</ a ></ li > < li >< a href = "tel:{{cellPhone}}" >Call Cell< br />{{cellPhone}}</ a ></ li > < li >< a href = "sms:{{cellPhone}}" >SMS< br />{{cellPhone}}</ a ></ li > </ ul > </ div > </ script > |
Step 2: Create the EmployeeView class
- Create a file called EmployeeView.js in the js directory, and define an EmployeeView class implemented as follows:
1234
var
EmployeeView =
function
() {
}
- Add the template as a static member of EmployeeView.
123456
var
EmployeeView =
function
() {
}
EmployeeView.template = Handlebars.compile($(
"#employee-tpl"
).html());
- Define an initialize() function inside the HomeView class. Define a div wrapper for the view. The div wrapper is used to attach the view related events. Invoke the initialize() function inside the HomeView constructor function.
1234567891011
var EmployeeView = function(employee) {
this.initialize = function() {
this.el = $('<
div
/>');
};
this.initialize();
}
EmployeeView.template = Handlebars.compile($("#employee-tpl").html());
- Define a render() function implemented as follows:
1234
this
.render =
function
() {
this
.el.html(EmployeeView.template(employee));
return
this
;
};
- In index.html, add a script tag to include EmployeeView.js (just before the script tag for main.js):
1
<
script
src
=
"js/EmployeeView.js"
></
script
>
Step 3: Implement View Routing
- In the app’s initialize() function, define a regular expression that matches employee details urls.
1
this
.detailsURL = /^
#employees\/(\d{1,})/;
- In the app’s registerEvents() function, add an event listener to listen to URL hash tag changes:
1
$(window).on(
'hashchange'
, $.proxy(
this
.route,
this
));
- In the app object, define a route() function to route requests to the appropriate view:
- If there is no hash tag in the URL: display the HomeView
- If there is a has tag matching the pattern for an employee details URL: display an EmployeeView for the specified employee.
12345678910111213route:
function
() {
var
hash = window.location.hash;
if
(!hash) {
$(
'body'
).html(
new
HomeView(
this
.store).render().el);
return
;
}
var
match = hash.match(app.detailsURL);
if
(match) {
this
.store.findById(Number(match[1]),
function
(employee) {
$(
'body'
).html(
new
EmployeeView(employee).render().el);
});
}
}
- Modify the initialize() function to call the route() function:
12345678
initialize:
function
() {
var
self =
this
;
this
.detailsURL = /^
#employees\/(\d{1,})/;
this
.registerEvents();
this
.store =
new
MemoryStore(
function
() {
self.route();
});
}
- Test the application.
Part 10: Using the Location API
In this section, we add the ability to tag an employee with his/her location information. In this sample application, we display the raw information (longitude/latitude) in the employee view. In a real-life application, we would typically save the location in the database as part of the employee information and show it on a map.
- In index.html, add the following list item to the employee-tpl template:
1
<
li
><
a
href
=
"#"
class
=
"add-location-btn"
>Add Location</
a
></
li
>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Add Location list item:
1
this
.el.on(
'click'
,
'.add-location-btn'
,
this
.addLocation);
- In EmployeeView, define the addLocation event handler as follows:
123456789101112
this
.addLocation =
function
(event) {
event.preventDefault();
console.log(
'addLocation'
);
navigator.geolocation.getCurrentPosition(
function
(position) {
$(
'.location'
,
this
.el).html(position.coords.latitude +
','
+ position.coords.longitude);
},
function
() {
alert(
'Error getting location'
);
});
return
false
;
};
- Test the Application
Part 11: Using the Contacts API
In this section, we use the PhoneGap Contacts API to provide the user with the ability to add an employee to the device’s contact list.
- In index.html, add the following list item to the employee template:
1
<
li
><
a
href
=
"#"
class
=
"add-contact-btn"
>Add to Contacts</
a
></
li
>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Add to Contacts list item:
1
this
.el.on(
'click'
,
'.add-contact-btn'
,
this
.addToContacts);
- In EmployeeView, define the addToContacts event handler as follows:
12345678910111213141516
this
.addToContacts =
function
(event) {
event.preventDefault();
console.log(
'addToContacts'
);
if
(!navigator.contacts) {
app.showAlert(
"Contacts API not supported"
,
"Error"
);
return
;
}
var
contact = navigator.contacts.create();
contact.name = {givenName: employee.firstName, familyName: employee.lastName};
var
phoneNumbers = [];
phoneNumbers[0] =
new
ContactField(
'work'
, employee.officePhone,
false
);
phoneNumbers[1] =
new
ContactField(
'mobile'
, employee.cellPhone,
true
);
// preferred number
contact.phoneNumbers = phoneNumbers;
contact.save();
return
false
;
};
- Test the Application
Part 12: Using the Camera API
In this section, we use the PhoneGap Camera API to provide the user with the ability to take a picture of an employee, and use that picture as the employee’s picture in the application. We do not persist that picture in this sample application.
- In index.html, add the following list item to the employee template:
1
<
li
><
a
href
=
"#"
class
=
"change-pic-btn"
>Change Picture</
a
></
li
>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Change Picture list item:
1
this
.el.on(
'click'
,
'.change-pic-btn'
,
this
.changePicture);
- In EmployeeView, define the changePicture event handler as follows:
1234567891011121314151617181920212223
this
.changePicture =
function
(event) {
event.preventDefault();
if
(!navigator.camera) {
app.showAlert(
"Camera API not supported"
,
"Error"
);
return
;
}
var
options = { quality: 50,
destinationType: Camera.DestinationType.DATA_URL,
sourceType: 1,
// 0:Photo Library, 1=Camera, 2=Saved Photo Album
encodingType: 0
// 0=JPG 1=PNG
};
navigator.camera.getPicture(
function
(imageData) {
$(
'.employee-image'
,
this
.el).attr(
'src'
,
"data:image/jpeg;base64,"
+ imageData);
},
function
() {
app.showAlert(
'Error taking picture'
,
'Error'
);
},
options);
return
false
;
};
- Test the Application
Part 13: Sliding Pages with CSS Transitions
- Add the following classes to styles.css:
12345678910111213141516171819202122232425
.page {
position
:
absolute
;
width
:
100%
;
height
:
100%
;
-webkit-transform:translate
3
d(
0
,
0
,
0
);
}
.stage-
center
{
top
:
0
;
left
:
0
;
}
.stage-
left
{
left
:
-100%
;
}
.stage-
right
{
left
:
100%
;
}
.transition {
-moz-transition-duration: .
375
s;
-webkit-transition-duration: .
375
s;
-o-transition-duration: .
375
s;
}
- Inside the app object, define a slidePage() function implemented as follows:
1234567891011121314151617181920212223242526272829303132333435363738
slidePage:
function
(page) {
var
currentPageDest,
self =
this
;
// If there is no current page (app just started) -> No transition: Position new page in the view port
if
(!
this
.currentPage) {
$(page.el).attr(
'class'
,
'page stage-center'
);
$(
'body'
).append(page.el);
this
.currentPage = page;
return
;
}
// Cleaning up: remove old pages that were moved out of the viewport
$(
'.stage-right, .stage-left'
).not(
'.homePage'
).remove();
if
(page === app.homePage) {
// Always apply a Back transition (slide from left) when we go back to the search page
$(page.el).attr(
'class'
,
'page stage-left'
);
currentPageDest =
"stage-right"
;
}
else
{
// Forward transition (slide from right)
$(page.el).attr(
'class'
,
'page stage-right'
);
currentPageDest =
"stage-left"
;
}
$(
'body'
).append(page.el);
// Wait until the new page has been added to the DOM...
setTimeout(
function
() {
// Slide out the current page: If new page slides from the right -> slide current page to the left, and vice versa
$(self.currentPage.el).attr(
'class'
,
'page transition '
+ currentPageDest);
// Slide in the new page
$(page.el).attr(
'class'
,
'page stage-center transition'
);
self.currentPage = page;
});
},
- Modify the route() function as follows:
12345678910111213141516171819
route:
function
() {
var
self =
this
;
var
hash = window.location.hash;
if
(!hash) {
if
(
this
.homePage) {
this
.slidePage(
this
.homePage);
}
else
{
this
.homePage =
new
HomeView(
this
.store).render();
this
.slidePage(
this
.homePage);
}
return
;
}
var
match = hash.match(
this
.detailsURL);
if
(match) {
this
.store.findById(Number(match[1]),
function
(employee) {
self.slidePage(
new
EmployeeView(employee).render());
});
}
},