In this tutorial, you’ll create a fully functional employee directory application with Cordova.
What you will learn:
- How to use different local data storage strategies.
- How to use several Cordova 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 locally using the Cordova CLI (Command Line Interface).
Requirements:
To complete this workshop, 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.
Part 1: Creating a Cordova Project
- Make sure an up-to-date version of Node.js is installed on your system.
- Open Terminal (Mac) or a Command window (Windows), and type the following command to install the Cordova CLI:
npm install -g cordova
or on a Mac:
sudo npm install -g cordova
- Navigate to a directory where you store projects on your system. For example:
cd ~/Projects
- Create a project called “workshop”:
cordova create workshop com.yourname.workshop Workshop
- Navigate to the project directory:
cd workshop
- Add the platforms you want to support. For example, to add support for iOS and Android, type:
cordova platforms add ios cordova platforms add android
- Make sure you are in the “workshop” directory, and add basic plugins to your projects:
cordova plugin add org.apache.cordova.device cordova plugin add org.apache.cordova.console
- Examine the directory structure under workshop.
Part 2: Building a Cordova Project
iOS
You need the iOS SDK installed on your computer to build an iOS version of your application using the steps below.
cordova build ios
The project is built in the workshop/platforms/ios folder. Double-click Workshop.xcodeproj to open the project in XCode, and run it in the emulator or on your device.
You can also run the application in the iOS emulator directly from the command line. First install ios-sim:
sudo npm install -g ios-sim
Then run the application in the iOS emulator:
cordova emulate ios
Android
You need the Android SDK installed on your computer to build an Android version of your application using the steps below.
To build the project in the workshop/platforms/android folder and run it on an Android device connected to your computer using a USB cable, type:
cordova run android
To build the project in the workshop/platforms/android folder and run it in the Android emulator, type:
cordova emulate android
Part 3: Setting Up the Workshop Files
- Download the assets for the workshop here or clone this repository
.
- Unzip the file anywhere on your file system.
- Delete the contents of your project’s workshop/www folder with the exception of the config.xml file.
- Copy the contents of cordova-tutorial-master/www into your project’s workshop/www folder.
- Build and test your application: If you have a Mobile SDK installed on your system, repeat the steps in Part 2 above. If you don’t, simply open index.html in a browser on your computer.
- 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 4: Choosing a Local Storage Option
Step 1: Explore different persistence mechanisms
Open the following files in workshop/js/adapters, and explore the different persistence adapters:
- MemoryAdapter (in memory-adapter.js)
- JSONPAdapter (in jsonp-adapter.js)
- LocalStorageAdapter (in localstorage-adapter.js)
- WebSqlAdapter (in websql-adapter.js)
Step 2: Test the application with different persistence mechanisms
The application is initially configured to work with the in-memory datastore. To change the local persistence mechanism for the application:
- In index.html: instead of memory-adapter.js, import the .js file for the data adapter of your choice: jsonp-adapter.js, localstorage-adapter.js, or websql-adapter.js.
- In js/app.js, replace the instantiation of MemoryAdapter with the instantiation of the data adapter you imported in the previous step: JSONPAdapter, LocalStorageAdapter, or WebSqlAdapter.
- Test the application.
Part 5: Using Native Notification
A default JavaScript 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 JavaScript alerts when it is running in the browser.
- Add the native dialogs plugin to your project:
cordova plugin add org.apache.cordova.dialogs
- In index.html, add the following script tag (as the first script tag at the bottom of the body):
<script src="cordova.js"></script>
This instructs the Cordova CLI to inject a platform specific version of cordova.js at build time. In other words, cordova.js doesn’t need to be (and shouldn’t be) present in your project/www folder.
- When running on a device with the navigator.notification object available (the dialogs plugin is installed), override the window.alert() function and replace its default implementation with a call to navigator.notification.alert(). Add this code to the “Event Registration” block:
document.addEventListener('deviceready', function () { if (navigator.notification) { // Override default HTML alert with native dialog window.alert = function (message) { navigator.notification.alert( message, // message null, // callback "Workshop", // title 'OK' // buttonName ); }; } }, false);
- Test the application: click the Help button.
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 6: Avoid the 300ms Click Delay
- Test the application on your iOS device or in the iOS emulator: Tap the Help button, and notice the delay before the dialog appears.
This delay occurs because the operating system is waiting roughly 300ms to see if the user is going to tap the target again (and therefore perform a double-tap).
- In index.html, add the following script tag:
<script src="lib/fastclick.js"></script>
FastClick is an open source library built by the Financial Times. More information here.
- In app.js, register FastClick inside the deviceready event handler.
FastClick.attach(document.body);
- Test the application: Click the Help button. The message should now appear without delay.
Part 7: 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 page refreshes provides a more fluid and 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).
- Inside the immediate function in app.js, define a function named renderHomeView() (right after the findByName function). Implement the function to programmatically add the Home View markup to the body element.
function renderHomeView() { var html = "<h1>Directory</h1>" + "<input class='search-key' type='search' placeholder='Enter name'/>" + "<ul class='employee-list'></ul>"; $('body').html(html); $('.search-key').on('keyup', findByName); }
- Modify the data adapter initialization logic: when the adapter has been successfully initialized, call the renderHomeView() function to programmatically display the Home View.
var adapter = new MemoryAdapter(); adapter.initialize().done(function () { renderHomeView(); });
- Since you moved the registration of the keyup event inside the renderHomeView() function, make sure you remove the original event registration in the Event Registration section.
- Since the Help button is no longer there, remove the click event handler for the help button (in the Event Registration section).
- Test the application.
Part 8: Using Handlebars 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, including Mustache.js, Handlebars.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 Handlebars.js but the same result can be achieved using the other HTML template solutions.
Modify index.html as follows:
- Add a script tag to include the handlebars.js library:
<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:
<script id="home-tpl" type="text/x-handlebars-template"> <div class="topcoat-navigation-bar"> <div class="topcoat-navigation-bar__item center full"> <h1 class="topcoat-navigation-bar__title">Employee Directory</h1> </div> </div> <div class="search-bar"> <input type="search" placeholder="search" class="topcoat-search-input search-key"> </div> <div class="topcoat-list__container"> <ul class="topcoat-list list employee-list"></ul> </div> </script>
- Create an HTML template to render the employee list items. Add this script tag immediately after the previous one:
<script id="employee-li-tpl" type="text/x-handlebars-template"> {{#.}} <li class="topcoat-list__item"> <a href="#employees/{{id}}"> <img src="assets/pics/{{pic}}"> <p>{{firstName}} {{lastName}}</p> <p>{{title}}</p> <span class="chevron"></span><span class="count">{{reports}}</span> </a> </li> {{/.}} </script>
- Add topcoat-mobile-light.css and styles.css to the head of index.html
<link href="assets/topcoat/css/topcoat-mobile-light.css" rel="stylesheet"> <link href="assets/css/styles.css" rel="stylesheet">
Modify the immediate function in app.js as follows:
- Immediately before the adapter variable declaration, declare two variables that hold the compiled version of the templates defined above:
var homeTpl = Handlebars.compile($("#home-tpl").html()); var employeeLiTpl = Handlebars.compile($("#employee-li-tpl").html());
- Modify renderHomeView() to use the homeTpl template instead of the inline HTML:
function renderHomeView() { $('body').html(homeTpl()); $('.search-key').on('keyup', findByName); }
- Modify findByName() to use the employeeLiTpl template instead of the inline HTML:
function findByName() { adapter.findByName($('.search-key').val()).done(function (employees) { $('.employee-list').html(employeeLiTpl(employees)); }); }
- Test the application.
Part 9: Creating a View Class
It’s time to provide the application with some structure. If we keep adding all the core functions of the application to the immediate function that bootstraps the app, 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 named HomeView.js in the js directory, and define a HomeView constructor implemented as follows:
var HomeView = function (adapter, template, listItemTemplate) { }
The constructor function takes three arguments: the data adapter, the Home View template, and the employee list item template. - Define an initialize() function inside the HomeView constructor. 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.
var HomeView = function (adapter, template, listItemTemplate) { 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(); }
- Move the renderHomeView() function from app.js 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 HomeView, you can also rename it from renderHomeView() to just render().
this.render = function() { this.el.html(template()); return this; };
- Move the findByName() function from app.js to HomeView.
this.findByName = function() { adapter.findByName($('.search-key').val()).done(function(employees) { $('.employee-list').html(listItemTemplate(employees)); }); };
Step 2: Using the Home View
- In index.html, add a script tag to include HomeView.js (just before the script tag for app.js):
<script src="js/HomeView.js"></script>
- In app.js, remove the renderHomeView() function from the immediate function.
- Remove the findByName() function from the immediate function.
- Modify the adapter initialization logic to display the Home View when the adapter has been successfully initialized. Pass the adapter, the Home View template, and the employee list item template as arguments to the Home View constructor.
adapter.initialize().done(function () { $('body').html(new HomeView(adapter, homeTpl, employeeLiTpl).render().el); });
- Test the application
Part 10: Implementing Native Scrolling
Test the application. Specifically, test the list behavior when the list is bigger than the browser window (or the screen). Notice that the entire view (including the header) is scrolling. To anchor the header at the top of the screen, and scroll the employee list only:
- In the Home Page template, add a css class named scroller to the div surrounding the employee list ul.
<div class="topcoat-list__container scroller"> <ul class="topcoat-list list employee-list"></ul> </div>
- In assets/css/styles.css define the scroller class as follows:
.scroller { overflow: auto; -webkit-overflow-scrolling: touch; position: absolute; top: 141px; bottom: 0px; left: 0px; right: 0px; }
- Test the application.
On iOS, you can still scroll down the web view (including the header). To disable this behavior, open config.xml in the workshop/www folder and add the following preference as the last line within the widget tag:
<preference name="DisallowOverscroll" value="true" />
Part 11: 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:
<script id="employee-tpl" type="text/x-handlebars-template"> <div class="topcoat-navigation-bar"> <div class="topcoat-navigation-bar__item left quarter"> <a class="topcoat-icon-button--quiet back-button" href="#"> <span class="topcoat-icon topcoat-icon--back"></span> </a> </div> <div class="topcoat-navigation-bar__item center half"> <h1 class="topcoat-navigation-bar__title">Employee</h1> </div> </div> <div class='details'> <img src="assets/pics/{{pic}}" class="employee-image"> <h1>{{firstName}} {{lastName}}</h1> <h2>{{title}}</h2> <h2>{{city}}</h2> <div class="topcoat-list__container scroller"> <ul class="topcoat-list list actions"> {{#if managerId}} <li class="topcoat-list__item"><a href="#employees/{{managerId}}"><p>View Manager</p><p>{{managerName}}</p><div class="action-icon icon-manager"/></a></li> {{/if}} <li class="topcoat-list__item"><a href="tel:{{officePhone}}"><p>Call Office</p><p>{{officePhone}}</p><div class="action-icon icon-call"/></a></li> <li class="topcoat-list__item"><a href="tel:{{cellPhone}}"><p>Call Cell</p><p>{{cellPhone}}</p><div class="action-icon icon-call"/></a></li> <li class="topcoat-list__item"><a href="sms:{{cellPhone}}"><p>SMS</p><p>{{cellPhone}}</p><div class="action-icon icon-sms"/></a></li> <li class="topcoat-list__item"><a href="mailto:{{email}}"><p>Email</p><p>{{email}}</p><div class="action-icon icon-mail"/></a></li> </ul> </div> </div> </script>
Step 2: Create the EmployeeView class
- Create a file named EmployeeView.js in the js directory, and define an EmployeeView constructor implemented as follows:
var EmployeeView = function(adapter, template, employee) { }
- Define an initialize() function inside the HomeView constructor. Define a div wrapper for the view. The div wrapper is used to attach the view related events. Invoke the initialize() function inside the EmployeeView constructor function.
var EmployeeView = function(adapter, template, employee) { this.initialize = function() { this.el = $('<div/>'); }; this.initialize(); }
- Define a render() function implemented as follows:
this.render = function() { this.el.html(template(employee)); return this; };
- In index.html, add a script tag to include EmployeeView.js (just before the script tag for app.js):
<script src="js/EmployeeView.js"></script>
Step 3: Implement View Routing
- Open app.js. In the Local Variables section, declare a variable named employeeTpl that holds the compiled template for the employee details view:
var employeeTpl = Handlebars.compile($("#employee-tpl").html());
- In the Local Variables section, declare a variable named detailsURL that holds a regular expression to match employee details URLs.
var detailsURL = /^#employees\/(\d{1,})/;
- In the Event Registration section, add an event listener to listen to URL hash tag changes:
$(window).on('hashchange', route);
- In the Local Functions section, 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 hash tag matching the pattern for an employee details URL, display an EmployeeView for the specified employee.
function route() { var hash = window.location.hash; if (!hash) { $('body').html(new HomeView(adapter, homeTpl, employeeLiTpl).render().el); return; } var match = hash.match(detailsURL); if (match) { adapter.findById(Number(match[1])).done(function(employee) { $('body').html(new EmployeeView(adapter, employeeTpl, employee).render().el); }); } }
- Modify the adapter initialization logic to call the route() function when the adapter has been successfully initialized:
adapter.initialize().done(function () { route(); });
- Test the application.
Part 12: 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 an alert. 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.
- Add the geolocaton plugin to your project
cordova plugin add org.apache.cordova.geolocation
- In index.html, add the following list item to the employee-tpl template:
<li class="topcoat-list__item"><a href="#" class="add-location-btn"><p>Add Location</p></a></li>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Add Location list item.
this.el.on('click', '.add-location-btn', this.addLocation);
Make sure you add this line as the last line of the initialize() function (after this.el is assigned).
- In EmployeeView, define the addLocation event handler as follows:
this.addLocation = function(event) { event.preventDefault(); navigator.geolocation.getCurrentPosition( function(position) { alert(position.coords.latitude + ',' + position.coords.longitude); }, function() { alert('Error getting location'); }); return false; };
- Test the Application
Part 13: Using the Contacts API
In this section, we use the Cordova Contacts API to provide the user with the ability to add an employee to the device’s contact list.
- Add the contacts plugin to your project
cordova plugin add org.apache.cordova.contacts
- In index.html, add the following list item to the employee template:
<li class="topcoat-list__item"><a href="#" class="add-contact-btn"><p>Add to Contacts</p></a></li>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Add to Contacts list item:
this.el.on('click', '.add-contact-btn', this.addToContacts);
- In EmployeeView, define the addToContacts event handler as follows:
this.addToContacts = function(event) { event.preventDefault(); console.log('addToContacts'); if (!navigator.contacts) { alert("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); contact.phoneNumbers = phoneNumbers; contact.save(); return false; };
- Test the Application
Part 14: Using the Camera API
In this section, we use the Cordova 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.
- Add the camera plugin to your project
cordova plugin add org.apache.cordova.camera
- In index.html, add the following list item to the employee template:
<li class="topcoat-list__item"><a href="#" class="change-pic-btn"><p>Change Picture</p></a></li>
- In the initialize() function of EmployeeView, register an event listener for the click event of the Change Picture list item:
this.el.on('click', '.change-pic-btn', this.changePicture);
- In EmployeeView, define the changePicture event handler as follows:
this.changePicture = function(event) { event.preventDefault(); if (!navigator.camera) { alert("Camera API not supported", "Error"); return; } var options = { quality: 50, destinationType: Camera.DestinationType.DATA_URL, sourceType: 1, // 0:Photo Library, 1=Camera, 2=Saved Album encodingType: 0 // 0=JPG 1=PNG }; navigator.camera.getPicture( function(imageData) { $('.employee-image', this.el).attr('src', "data:image/jpeg;base64," + imageData); }, function() { alert('Error taking picture', 'Error'); }, options); return false; };
- Test the Application
Part 15: Sliding Pages with CSS Transitions
Modify index.html as follows:
- Add pageslider.css inside the head tag in index.html:
<link href="assets/css/pageslider.css" rel="stylesheet">
- Add a script tag to include the pageslider.js library:
<script src="lib/pageslider.js"></script>
Modify app.js as follows:
- In the Local Variables section, declare an instance of the PageSlider object as follows:
var slider = new PageSlider($('body'));
- In the route() function, replace the calls to $(‘body’).html() with calls to slider.slidePage() passing the same argument to the function.
slider.slidePage(new HomeView(adapter, homeTpl, employeeLiTpl).render().el);
and
slider.slidePage(new EmployeeView(adapter, employeeTpl, employee).render().el);