How to create an awesome hybrid mobile app using Cordova - Part I
Table of Contents
- 1. Preface
- 2. Safe - A Hybrid Mobile App
- 3. Part I - Create the Walking Skeleton
- 4. Prerequisites
- 5. Get the air clear
- 6. Mock up
- 7. Choose the tools
- 8. Setup the environment
- 9. Create the project structure
- 10. Download the libraries
- 11. Install Grunt
- 12. Create grunt tasks
- 13. Wire-up everything
- 14. Wrapping up
- 15. What's Next?
1. Preface
It's been little over an year I've published anything in my blog. Every time I inspect my Google analytics page I sense the traffic is getting down 😣. I've been patience for all this time working now and then with sheer confidence that I could complete this taunting work. People who have been following my recent blog posts know that it's no longer a typical blog where enthusiastic developers write about creating todo app in some got-popular-lately technology or comparing two frameworks and describing their pros and cons in a table. I encourage learning by building something for real. I'm driving my website in that path. It's a place where I can share the experience about something that I built lately. It's easy said but it takes a lot of time and effort to build something useful and share with others. In all those months passed I was working in this hybrid mobile app so called "Safe" that helps to store photos safely. The app runs successfully in both iOS and Android platforms. I would like to share the experience I had in creating this app through this series of blog posts. I strongly believe that it'll help a lot of people to build their own *dream* app 🌝.
The idea (💡) of building a mobile app came to me three years back when I was facing difficulty in keeping sensitive information like passwords, credit-cards and photos. I own an iPhone 3GS device and was thinking that developing an app would help me to keep my secrets safely and easily accessible from anywhere. There are already a lot of apps out there that solves this problem but I want to build one myself so I can learn something new. I ended up finally developing an app that could store only the photos but not the textual information. The good news is the app runs fine in both iOS and Android devices.
Being only good at .NET and JavaScript, I wasn't interested to go with native implementation to build the app instead I chose the hybrid path using well known technologies like HTML, CSS and JavaScript. Using hybrid technologies will also give an additional benefit to run the app in different platforms. There are many frameworks exists in the market that helps to build hybrid apps. Some of the notable ones are Intel XDK, Titanium, Sencha Touch, Kendo UI, Xamarin and Cordova. I chose Cordova because when I was thinking about hybrid I was only aware of it. Also, if I'm using Cordova, I can use my existing knowledge in web technologies to build the app. To develop the app for iPhone or any iOS device you need a Mac. Though there are ways like PhoneGap build and PhoneGap developer app that come save but I feel without the machine it's difficult. Since I didn't own a Mac at that time, I thought of developing the app using Windows 8 and later I could borrow a Mac to kick the build and install the app in my iPhone. When days passed.. I came to a state that even if I can't build the app for my personal use I just want to try it. At the starting phase, I tried to develop the app using jQuery Mobile (JQM) and Knockout which didn't work out very well. The main reason is JQM tries be to too smart by doing more things than I need. I dumped the source code and soon got busy into other things. Time was not waiting and many important events happened in my life. I changed my company, got married, become father and then.. time become thin! 🙍
After I changed my company I missed the Windows 8 machine and all I was left with a Dell Windows 7 laptop. To develop the app for windows phones you need a Windows 8 machine. Previously I tricked my wife 💆 to buy a Nokia Lumia to help in my development and now that also left with no use for me. At that point of time, I stopped developing for Windows phones. In all those time lost I was stupid and I wasn't thinking about Androids. You can develop the app for Androids using Windows 7 machine itself. I cursed myself for buying a Lumia instead of an Android phone. I told myself that I can still use an emulator instead of a real device. I again started to work on the mobile app targeting Android this time. Meanwhile, I got exposed to Backbone with a whole set of new libraries like RequireJs, Handlebars and Underscore.
Developing an app with Backbone comes with it's own pain. Backbone is small and tries to be unopinionated giving developers a lot of freedom to structure their code. As we all know, "With great freedom comes great responsibility". Sometimes I confused whether I need to instantiate a model in the view or router. Backbone don't force one or other and leaves that to us. Backbone misses controllers and that makes your router fat. After all these things still I liked Backbone for the same reason (being unopinionated) and decided to go with that. I was left with one last problem. I need a library for user interface. Backbone gives you only structure not the look. We need skeleton and flesh. I googled and discovered quite some libraries but nothing seems to be interesting. One of the library I found was ChocolateChip-UI which also suffers from the same problem like jQuery Mobile. I need the library to only provide the look not more than that. I also came to know about this awesome Ionic framework, pretty interesting! But the problems with Ionic is, it is backed by AngularJs which I don't have much knowledge and also the library provide same styles for both Android and iOS which I really don't want. Finally, I came to know about this one called Ratchet created by the Bootstrap folks. Ratchet is lightweight and provide unique styles for both Android and iOS. Though Ratchet don't provide much features like Ionic (at the time of writing this) but I like their approach and decided to give a try.
Luckily I got a chance to buy a MacBook Pro 🍏 (still I've a lot to pay) and that made me to develop the app for both iOS and Android platforms. After I started developing the app I came to see the front-end development world has changed quite a lot. I was living in the world where I used to do many things manually like downloading libraries, unzipping and dropping them in the right folders, running code quality tool, running unit tests, minifying resources, setting-up server and so on. It looks like people automated all these boring tasks and made front-end development no more less than back-end. Node, bower and grunt are some of the awesome tools that changed things a lot. It took quite some time to get myself familiar with all these new kids in the town and that really helped me a lot down the road.
In the initial stage of development, I was changing my project structure quite often. Since I was targeting the app for both iOS and Androids, I thought of creating unique designs for both the platforms similar like the native ones. When I say unique design, it's not just in terms of CSS but to the level of tailoring pages and creating animations. This complicated many things in my code and finally I decided to go with a simple and same UI structure for both the platforms except the CSS styles could be different.
The initial idea of the app is to store both textual information (passwords, credit-cards etc.) and images in an encrypted way. After I realised the keychain in iOS already solves the problem of persisting textual secrets I changed the idea to store only images. I had difficulty in finding the way to encrypt images. JavaScript is single threaded and there is no way you can encrypt the images using some client-side library like crypto.js that will take years to encrypt an image. I've to rely on native engine to do the encryption. I know there is some Cordova plugin that will save my life and I was lucky to finally discover one that works for both the platforms.
Once I had achieved some progress, I wanted to test the app in my iPhone 3GS device. I thought I could just plugin the device and run the command to deploy the app... but it didn't work out. I had to subscribe to the Apple's 99$ developer program to install the app in my device, SIGH! I was thrilled to see the app in my iPhone first. Thanks to Safari debugging support, it helped me lot to knock out some major issues. One of the strange issue I encountered was, the JavaScript and CSS files are not getting downloaded into the app. I checked the path and everything was right. For quite some time I was wondering what the heck going on? After burning an hour or more I experienced the "Aha" moment. I named the combined JS and CSS files as Safe.min.js and Safe.min.css. The iPhone 3GS device blocked the files since they are named as "Safe". Strange isn't it? After I renamed the files to "index" things started to work.. Aha! 🙌
Once I successfully tested the app in my iPhone I wanted to test in Android. I borrowed Nexus devices from couple of my colleagues and installed the app to see how it behaves. After fixing some minor issues I realised the encryption is taking tooooo long ⏳. Though those devices exceeds in processing power and memory than my poor old iPhone still they are slow. I noticed both of them had lot of apps running in the background. Is the encryption is slow due to that? I didn't have enough freedom to do more RnD on their devices (don't want to screw their phones). Finally I ended up buying a cheap Moto-E Android device to test the app. The performance was super fast. I didn't get much time to install lot of apps in my android device and see how the app behaves. Instead, I thought let provide a setting to developers to turn off encryption if they see the performance is too bad, which I don't recommend anyway. In this case the safety of photos is protected only by authentication.
2. Safe - A Hybrid Mobile App
Safe is a hybrid mobile app that keeps your photos safe from unauthorised access by authentication and encryption. Safe stores the photos in the device not in the server. Currently the app supports only iOS and Android platforms. It's because couple of plugins I used works only in those two. The goal of building this app is to guide developers how to build a mobile app using hybrid technologies and I've kept the features to bare minimum. I encourage readers to fork the code from Github and try out more fancy things. On any question please free to post a comment.
From now on... I'll try to pretend that I haven't created such an app and we are going to create one through this series of blog posts. I expect the readers to be little familiar with Object Oriented JavaScript before diving into this. It would be great if you are familiar with any one of the client-side MV* library out there. You can use either Mac or Windows. Remember that, for iPhones you need a Mac. Most of the places the development is going to be same in both platforms but in some places they differ. I pretend to be neutral and whenever things are different I'll explicitly tell what Mac users has to do and what Windows users has to do. If you got stuck in any place please post a comment I'll be glad to help out. I've split the complete work into three parts so that it would be easier for you to grasp, digest and cherish.
Alright! let's build a mobile app 🚀.
3. Part I - Create the Walking Skeleton
In this first part, we are gonna create the "walking skeleton" of our app. A walking skeleton is nothing but a tiny implementation of the system from end-to-end. We are gonna plan, create mockups, setup environment, download libraries and wire up everything. Though you don't see much code for the mobile app in this post, reading this one is very important to follow the succeeding ones.
The development environment we are gonna setup is platform agnostic. We can use either Windows or Mac or both to develop the app. I used both, Mac in home and Windows in office. Thanks to Dropbox for keeping code in sync.
4. Prerequisites
- Mac or Windows (for iOS you need Mac)
- Some text editor (I use WebStorm)
- Chrome or Safari browser
- Real iPhone and Android devices (I used iPhone 3GS running iOS 6.1.6 and Moto E running Android 4.4.4)
- Knowledge on OOP in JavaScript
We don't need a real mobile device until we started to play with Camera and other native features of the mobile device. We'll be mostly using browser as the tool to build the app.
5. Get the air clear
Before start to get our hands dirty.. stop a bit.. take some time to plan.. visualise.. and come up with a list of todos. We need to think about the purpose of the app and the important functionalities it should provide to fulfill it. The functionalities also include some additional features that we may wish to have.
Purpose: It should secure an user's photos from unauthorised access.
Functionalities:
- It should allow an user to select a photo from the album.
- It should encrypt the selected photo before saving to file system.
- It should prevent unauthorised access by authentication.
- It should allow an user to register, login and change password.
- It should allow an user to retrieve the lost password.
Before creating mockups and getting into code we've to ask ourselves some questions. Are we clear with the purpose? Are we clear with all the functionalities? Can we able to implement all the functionalities and features? How about allowing user to select photo from the album? Is that possible? Yes, it is. Cordova provides a camera plugin and through that you can either shoot a picture from camera or select from album. How about encryption? Is there any built-in plugin available to do that? Is it possible to resize images to create thumbnails?
Friends, we got two important questions here. One is about encryption and other is about resizing. Without having answers to them it's not wise to move further. We badly need encryption and if it's not possible then we've to ditch this idea altogether. It looks like there are no built-in plugins available in Cordova that could do that (at the time of writing this). After googling I came to know that mobile devices supports native encryption. For example, iPhone 3GS and later devices all contains an encryption engine that does AES 256 encryption. Android devices also supports native encryption. Since most smart devices supports encryption natively if there are no ready made plugins available it's possible to create one. But it looks like that isn't necessary. Thanks a lot to disusered for sharing the code of a custom Cordova plugin that does encryption. For the second question, there are readymade plugins available in Github that does the image resizing job pretty well. Great, I think we got our grey areas wiped out. Let's move on! ...🏄
6. Mock up
It's time to relax and think about our app's UI design. We need to think about the different pages, layouts, menus, navigation etc. If you remember the list of functionalities, we need to build the following important pages.
List page - Displays the list of encrypted photos. Each photo contain a description and a thumbnail.
View page - Displays the full size photo with description in a single page.
Add/edit photo page - Allows user to add or edit a photo with description.
Registration page - Allows user to register by entering a password with security question and answer. The security questions are predefined and displayed in a dropdown.
Login page - Provides a password field for user to login. Also, contains a link to the forgot password page.
Forgot password page - Displays the security question dropdown and and an empty security answer textbox. If the user enters the correct answer he'll be taken to the change password page.
Change password page - Displays the password field to allow user to enter the new password.
Change security question/answer page - Provides the security question dropdown and the answer textbox.
We also need couple of more pages like a Settings page (where user will have links to navigate to change password and change security question pages) and an Info page.
The design is simple and straight, not too fancy! That's good, because it'll keep us focussed on the demon 👿 not the beauty 👸.
7. Choose the tools
Let's choose the tools to build this thing. First, we need Cordova. For the people who haven't heard about it, it's a wrapper and a bridge (thanks to Coenraets for this concise description). When I say it's a wrapper it means Cordova helps you to wrap your HTML, CSS and JavaScript code and make them run in different mobile platforms. When I say it's a bridge, means it acts as a bridge between your JavaScript code and the native APIs like camera, contacts, accelerometer etc. There is some confusion exist in the names. When I say Cordova or PhoneGap I mean the same thing but in reality they are not. Cordova helps you to code once and run anywhere by providing a generic abstraction over the native APIs. For example to access camera it provides the same interface for all the platforms. We need our app to run in both iOS and Android and we need Cordova period.
Cordova itself is enough to build the hybrid app. But for people who are developing client-side applications know how hard is to maintain JavaScript. I've seen files with thousands of lines of code and I remember how hard it was to find the place where the developer is disabling that submit button. We are gonna use quite a lot of JavaScript in our app and we need a well known structure to keep things easy for maintenance and for others to understand and contribute. We need some kind of MV* library. Looks like there are lot of libraries lingering in Github. I chose Backbone and the reasons are: it's light-weight, clean and... very importantly I'm good at it. I know a little bit of AngularJs. I thought of using it when I was thinking about Ionic Framework but later changed my mind. At the end of day what does matters is choose the one you are good at. I chose Backbone, though I know the fact that I'll be writing more code and using more additional libraries than in Angular. If you want to use AngularJs, go with that. Though you miss a lot of stuff from here but certainly there is something you can learn though.
If you are using Backbone usually you'll choose some of it's friends too... jQuery, Underscore, Handlebars and RequireJs. Backbone don't have built-in DOM manipulation methods and it delegates that to jQuery, Zepto or Ender. We use jQuery. Backbone needs Underscore. Underscore is a popular utility library written by the same guy who created Backbone. Underscore provides a set of helpful functions to work with arrays, functions and objects. Handlebars is a templating library and we badly need that. Without that we'll be spending our life crafting HTML with '+' or some other nasty ways. RequireJs? Why we need this one? This library is not mandatory but using this one helps me to organize my code as modules, which I really love to. Whenever a module depends on other modules this guy takes care of injecting them and thus reducing global warming 🌞.
We need a library for designing the user interface. Ratchet is one of the best tool. It's young and riding in the right path. We may need some additional plugins which we'll see once we get into development.
Here is the summary of libraries and frameworks we need to build this app.
- Cordova
- Backbone
- jQuery, Underscore, Handlebars and RequireJs
- Ratchet
What's next? Setup the environment!
8. Setup the environment
Before downloading the libraries and setting up the project, I kindly introduce you some of the new rockstars ..🎸..🎺..🎷.. rocking the front-end world. Ohhh man.. they changed everything!
8.1 Node.js, Bower and Grunt, OMG!
I had no idea how much the front-end world has changed. I got introduced about these new tools when I was working in a client project. They changed the way I used to work with JavaScript applications forever. Many of the mundane works are completely automated by these tools. Bower and Grunt both runs on top of Node. Bower is a client-side package manager. With Bower you don't have to worry about downloading and upgrading client-side packages. Also, you don't have to check-in these packages to your source control. All you check-in is a metadata file that contains the list of packages required for the project.
Grunt is for one thing and that's automation. All those unexciting tasks like minifying files, running tests, setting up server and much more can be automated by Grunt. You don't have to press F5 anymore when you change a file. Doesn't that sounds cool? Setting up an environment using these tools saves a lot of ticks in your clock ⏰. Let's setup this environment in your machine, whether it's Mac or Windows.
8.2 Install Node.js and supporting packages
Node.js is primarily used for server-side development to create non blocking I/O applications using JavaScript. In our case we are using it to assist front-end development. Both Bower and Grunt are nothing but node packages. I recommend Mac users to install Homebrew first and then install Node using it. Windows users can directly install node using the installer. The Node Package Manager (NPM) is also a part of your installation and it's primary purpose is to download and install node packages from the registry. Once you done with your installation, you can verify both Node and NPM are installed successfully by running node -v and npm -v from your terminal. Both commands displays the installed version of Node and NPM. In your case the installed versions could be possibly different.
$ node -v v0.10.31 $ npm -v 1.4.26
Listing 1. Displaying the installed versions of node and NPM
8.2.1 Install Bower
You can install a node package using NPM at global level or project level. The packages installed at global level (or machine level) is available to all projects. I recommend to install Bower at global level. But wait.. Bower needs Git. Mac users can download Git from here and Windows users from here. In Windows, you might also have to add the Git path (ex. C:\Program Files (x86)\Git\bin) to the system environment variable depending upon your installation. Once you've Git installed, run the npm install command from your terminal to install bower.
$ npm install -g bower
Listing 2. Installing Bower using NPM
The "-g" flag tells NPM to install the package at global level. Mac users may need to add "sudo" at the beginning to install packages at global level but remember doing so will ask to enter your system password. Like install command NPM also provides commands to uninstall and update packages. To know the list of all commands run npm help.
8.2.2 Install Grunt CLI
For Grunt, things are little different. In old days people used to install Grunt at global level and that forced all projects to use the same version. For any project if you need the latest Grunt you've to upgrade the globally installed one and that broke other projects. Nowadays, to support multiple versions of Grunt in a single machine you need to install something called Grunt CLI (command line interface) at global level and Grunt at project level. The job of Grunt CLI is to find and run the version of Grunt that's installed locally.
Install Grunt CLI at global level.
$ npm install -g grunt-cli
Listing 3. Installing Grunt CLI
We'll install Grunt once we setup our project. You can ensure both Bower and Grunt CLI are installed globally by running npm -g ls. This displays all the globally installed packages with their dependencies in a treeview.
We've installed the main components to assist our development. In the following sections we'll create the project structure, download the libraries, create the grunt tasks and wire-up everything!
9. Create the project structure
One of the goal I had before creating the app is, I want the app to be browser runnable. Since most of our code is in HTML, CSS and JavaScript why can't we use a browser as a tool to develop the app instead of using an emulator or a real device. Using an emulator or a real device is like riding over a snail 🐌.. slows down the development! Everytime you make a change you've to redeploy the app. To make the app browser runnable you've to pay a little extra tax. We've to fake the native APIs like Camera, Notification and others. We've to do a little extra work and we'll see that once we reach there. The point I'm trying to make is, we are gonna develop this app just like any client-side app. Once we reach a point that to continue the work we need an emulator or a device and at that time we'll create the Cordova project, add the platforms and plugins and see how we can compile our code to run in an emulator or device. Till that time... all you need is your favorite browser!
While I was creating the project structure I surprisingly came to know about Yeoman. Yeoman is a scaffolding tool that contains thousands of generators to kickstart new projects quickly. Generators are nothing but project templates. You can install Yeoman and generators through NPM. There is also a generator available to create Cordova based apps. Using generators saves a lot of time and they enforce some best practices. I didn't turn my eyes to Yeoman because I wanted to create everything from scratch! Alright, let's create the folders.
Create the main folder with name "Safe". We are gonna have all the node packages, bower components, Cordova directories and everything under this folder. Create two sub-folders underneath with names "src" and "tests". As their names, all the source code goes under "src" and all the unit tests goes under "tests".
Under "src" create four sub-folders: "js", "css", "html" and "images". The "html" folder is going to contain the handlebar templates and you know what other folders going to contain. Don't you? We need to create three CSS stylesheets under "css" folder with names safe.css, safe.ios.css and safe.android.css. The safe.css file will contain the common styles required for both the platforms. The other two CSS files will contain the platform specific styles. Since we are using separate CSS stylesheets for each platform, for development we need to create separate html files as well. I tried to use a single html file to develop for both platforms but it didn't work out. Let's create two html files with names index.ios.html and index.android.html at the root of the "src" folder for each platform. The index.ios.html will reference the CSS files for iOS platform and the other one reference the CSS files for the Android platform.
After all this work, this is how your project folder structure should look like.
Our next task is to prepare the html files. Copy and paste the below markup to your index.ios.html file. There is nothing special about the markup. All we've is some extra meta tags and CSS references.
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta name="format-detection" content="telephone=no"/> <!-- Sets initial viewport load and disables zooming --> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, target-densitydpi=device-dpi"/> <meta name="msapplication-tap-highlight" content="no"/> <link rel="stylesheet" type="text/css" href="css/safe.css"/> <link rel="stylesheet" type="text/css" href="css/safe.ios.css"/> <title>Safe</title> </head> <body> <h1>Safe</h1> </body> </html>
Listing 4. index.ios.html
The content of index.android.html which is shown below is mostly same with some minor changes. The reference to safe.ios.css is replaced with safe.android.css. If you notice little close you can see some extra attributes included in the viewport meta tag. Actually these attributes are removed from index.ios.html to avoid some display issues in iOS devices which I can't remember now..🙍.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="format-detection" content="telephone=no" /> <!-- Sets initial viewport load and disables zooming --> <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" /> <meta name="msapplication-tap-highlight" content="no" /> <link rel="stylesheet" type="text/css" href="css/safe.css" /> <link rel="stylesheet" type="text/css" href="css/safe.android.css" /> <title>Safe</title> </head> <body> <h1>Safe</h1> </body> </html>
Listing 5. index.android.html
We've the project structure and the HTML files ready. It's time to download the libraries.
10. Download the libraries
We are going to use Bower to install the client-side libraries. In Bower world, the client-side libraries are called as "bower components". In this article, whenever I say component it means a library or plugin or tool required for client-side. If you remember we've installed Bower at global level and that gives us freedom to run bower commands from any location. Bower uses similar convention like NPM for installing and uninstalling components. You can install any component by running bower install <comp_name> from the terminal. Before installing any component I would request you to create an empty file called bower.json at the root of the project.
10.1 bower.json
The bower.json is a metadata file used to store information about the project like it's name, version and dependencies. Whenever you download a component using bower by passing some additional flags (we'll see about them soon) you can tell bower to save it as a dependency in bower.json. Usually you check-in only the bower.json to your source control and not all the downloaded files. Anyone has this file can download all the required components by firing a single command bower install.
Actually... you don't have to create this file yourself. If you run the command bower init you'll be taken through a series of questions and finally you've this one created. Some of the notable questions you'll be asked are: package name, description, version, license and author. For the sake of this tutorial, I would prefer to create this file manually, so, go ahead and create this file at the root of the project. Once you create this file, all I wanted is you to specify the project name, version and description in the file. Actually there are quite bunch of things you can specify in bower.json. Since we are developing an app not a bower package we can ignore them.
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe" }
Listing 6. bower.json
10.2 Download the main libraries
We should start downloading the below main libraries first. We may need some additional plugins which we'll download following them.
- jQuery -> DOM query and manipulation
- Underscore -> Utility functions
- Backbone -> Structure
- Handlebars -> Templating
- RequireJs -> Module loader
- Ratchet -> UI
Let's install jQuery first. Open the terminal quickly and run the below command from your project root folder.
$ bower install jquery --save
Listing 7. Install jQuery through bower
Once you run the command, you'll notice a series of requests firing from your machine.. bunch of files are getting downloaded.. and finally a new folder called "bower_components" created at the root. If you scan the "bower_components" folder you'll see a new folder called "jquery". The "jquery" folder contains a hell lot of files other than the distribution ones ("dist" folder). For any component you install through bower you'll notice a lot of files downloaded to your machine. The same nuisance exists in NPM too..😡. That's why people don't prefer to check-in the "bower_components" folder to the source control. Hopefully this will get addressed in the future.
The "-save" flag is very important. It tells Bower to update the bower.json to include this as a dependency. If you open bower.json, you'll see a new node called dependencies that contains details about the newly added package's name and version. Note that, for me the installed version of jQuery is 2.1.4 and for you it could be different. You might puzzled by the "~" sign at front of the version no. It tells Bower to download the most recent minor version when you run bower install. You can also affix "^" character to download the most recent major version (These rules are applicable to NPM as well).
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe", "dependencies": { "jquery": "~2.1.4" } }
Listing 8. bower.json
Let's install the remaining libraries.
$ bower install underscore --save $ bower install backbone --save $ bower install handlebars --save $ bower install ratchet --save $ bower install requirejs --save
Listing 9. Installing remaining libraries
After running all these, this is how your bower.json file should look like... and again the version numbers could be different for you 🌝.
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe", "dependencies": { "jquery": "~2.1.4", "underscore": "~1.8.3", "backbone": "~1.2.1", "handlebars": "~3.0.3", "ratchet": "~2.0.2", "requirejs": "~2.1.19" } }
Listing 10. bower.json
10.3 Installing the plugins
Backbone is an unopinionated framework. It provides only a minimum set of functions. It doesn't have full support for validation, data binding and other things but we can achieve all of them through plugins. One good thing about being unopinionated is, we can easily customize, replace or extend. For example, as default Backbone persists data to server through Ajax. But we can easily override this behavior to persist the data to a different place like localstorage. As a matter of fact, there are quite number of plugins already available that helps to extend/customize Backbone.
To build our mobile app we need a little help from these guys.
Backbone.Validation -> A plugin that validates model as well as form input.
Backbone.localStorage -> In our app, we are not going to persist data in server. We are going to store a part of the data in the file system and the other part in the local storage. The actual photo (base64 string) is stored in the file system and other information like description and thumbnail are stored in localstorage. This plugin helps to override Backbone at global level to persist the data to localstorage.
Backbone.stickit -> Unlike Knockout or Angular, Backbone don't support data binding. Thanks to stickit! It helps to achieve two way data binding in Backbone as well.
Backbone.touch -> This plugin optimizes the experience for touch devices by replacing the default browser events with touch events.
Let's install these plugins.
$ bower install backbone.validation --save $ bower install backbone.localStorage --save $ bower install backbone.stickit#0.8.0 --save $ bower install backbone.touch --save
Listing 11. Installing backbone plugins
Sadly, I encountered some issues while installing the backbone.stickit plugin. The latest version of the plugin got some compatability issues with Backbone and Underscore and I've to go with 0.8.0 version. If you also facing any conflict please install the said version. You can install a particular version by prefixing "#" with version number following the component name.
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe", "dependencies": { "jquery": "~2.1.4", "underscore": "~1.8.3", "backbone": "~1.2.1", "handlebars": "~3.0.3", "ratchet": "~2.0.2", "requirejs": "~2.1.19", "backbone.validation": "~0.7.1", "backbone.localStorage": "~1.1.16", "backbone.stickit": "0.8.0", "backbone.touch": "~0.4.2" } }
Listing 12. bower.json
We've the necessary libraries and plugins installed. Next, we need to setup some grunt tasks to aid our development. For that, first we need to install grunt followingly separate node packages for each task.
11. Install Grunt
We'll be doing a lot of tasks repeatedly during development and deployment. I call it as grunt work or may be donkey work. Some of the tasks you'll be doing more often during development are: running jshint, compiling handlebar templates, refreshing the browser etc. We'll also doing more additional tasks during deployment like minifying JS/ CSS files, copying resources, running cordova build tasks and lot of other boring things. Gone are the days people were doing these tasks manually. Every single task you can think of can be automated using tools like Grunt or Gulp. I wish there could be a grunt task to do development 😃.
If you remember we've installed only the Grunt CLI at the global level. We need to install Grunt at the project level. Before doing that first you should create a file called package.json at the root. This is similar like bower.json but used to specify the package name and dependencies at node side. You can also create this file using npm init command and doing so will walk you through a series of questions and have this file created automatically at the end. For now, let's create this manually and specify the name, version and description of the project as you did in bower.json. There are lot of other things we can specify in the file and again since we are not going to publish this as a node package we can safely ignore them.
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe" }
Listing 13. package.json
Run the below command to install Grunt locally to the project.
$ npm install grunt --save-dev
Listing 14. Installing Grunt
If you see the above command we used a new flag called "--save-dev". If you remember, we used "-save" before while installing the bower components. Both NPM and Bower follows similar conventions. The "--save" is also applicable in NPM. What's the difference between "--save" and "--save-dev"? There are two kind of dependencies we need in a project: production dependency and development dependency. Production dependency means we need that at the runtime. For example, our app has dependency with Backbone and we need it not only at the time of development but also at production. So, Backbone is a production dependency and so do other libraries and plugins installed through Bower. Grunt is a best example of development dependency. We need it only at the time of development but not at runtime. Production dependencies are installed using "-save" and development dependencies using "-save-dev". Both dependencies are represented separately in the metadata files. I hope things are clear now let's rush to create tasks.
12. Create grunt tasks
We need quite a lot of grunt tasks to build our mobile app but let's start with ones that we need during development.
The below table illustrates the different tasks we need and the respective grunt plugin required to achieve that. Remember grunt plugins are nothing but node packages.
Task | Grunt plugin |
---|---|
We need a task to check the quality of all JS file and make sure there are no typos or errors. This task is often called as linting. | grunt-contrib-jshint |
Handlebar templates are often compiled to boost performance and we need a task to compile handlebar templates. | grunt-contrib-handlebars |
For easy development, we want our app to be browser runnable. To achieve that we need to setup a mini server to serve our HTML, CSS and JS files. | grunt-contrib-connect |
Finally and most importantly we need a watch dog! A task to monitor the files and trigger respective actions. For example whenever you modify a handlebar template we want to run the compilation and refresh the browser without any manual effort. | grunt-contrib-watch (This plugin is really a gift for us) |
Let's install these plugins. Remember we need all these plugins only to assist during development and/or deployment so we've to use "-save-dev" flag.
$ npm install grunt-contrib-jshint --save-dev $ npm install grunt-contrib-handlebars --save-dev $ npm install grunt-contrib-connect --save-dev $ npm install grunt-contrib-watch --save-dev
Listing 15. Installing grunt plugins
You can also install all these plugins using a single statement like npm install grunt-contrib-jshint grunt-contrib-handlebars .. --save-dev.
Verify your package.json file looks like below. Remember if you face any challenge down the road please install the particular version of the plugin specified in the below file. Recently I faced some issues with the latest grunt-contrib-connect plugin and I advice you guys to go with the "0.10.1" version. Unlike Bower, in NPM you can install a particular version using the "@" character (npm install grunt-contrib-connect@0.10.1 --save-dev).
{ "name": "Safe", "version": "0.1.0", "description": "Cordova based hybrid mobile app to keep your photos safe", "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-connect": "^0.10.1", "grunt-contrib-handlebars": "^0.10.2", "grunt-contrib-jshint": "^0.11.2", "grunt-contrib-watch": "^0.6.1" } }
Listing 16. package.json
To create the grunt tasks first we've to create a JavaScript file called Gruntfile.js at the root of the project. We are gonna compose all our tasks inside this file. For people who aren't familiar with grunt I highly recommend to take a quick look at their Getting Started guide.
Our Gruntfile.js is nothing but a node module. Module is a fundamental building block in node. The function or object that needs to be exposed outside should be assigned to the exports property of the module object.
The below code listing shows how we should start our grunt file.
module.exports = (function () { 'use strict'; return function (grunt) { // TODO: create the tasks }; })();
Listing 17. Gruntfile.js
The right hand side of the assignment is wrapped in an immediately executing function (IEF). IEFs are commonly used to create modules in JavaScript. In our case, we used it purposefully to pass the jshint validation. The IEF returns a function where we configure all our tasks and if you notice the function the grunt object is available as a parameter to it.
The grunt object provides several methods to create and configure tasks which we are gonna see soon.
Before creating the tasks, first we've to load the grunt plugins. We can do that by calling grunt.loadNpmTasks passing the plugin names.
module.exports = (function () { 'use strict'; return function (grunt) { // Load all the grunt plugins grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-handlebars'); grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-watch'); // TODO: create the tasks }; })();
Listing 18. Loading grunt plugins
Instead of loading each plugin one by one we can load all of them at-once using the load-grunt-tasks node module. Let's install it first.
$ npm install load-grunt-tasks --save-dev
Listing 19. Installing load-grunt-tasks module
Remove all the grunt.loadNpmTasks calls from Gruntfile.js and replace it as below.
module.exports = (function () { 'use strict'; return function (grunt) { // Load all the grunt tasks. require('load-grunt-tasks')(grunt); // TODO: create the tasks }; })();
Listing 20. Gruntfile.js
The next step is to create and configure the tasks. For that, we've to use the grunt.initConfig method. The initConfig method takes a single JSON object that contains information about all the tasks.
// TODO: create the tasks grunt.initConfig({ task1: {}, task2: {} });
Listing 21. grunt.initConfig method
Let's create the tasks needed for our development one-by-one.
12.1 Create task to check quality of JS files (jshint)
JavaScript is a dynamic language. We developers are prone to do mistakes. A single typo can shut down an entire application. Instead of spending hours in debugging it would be great if there's some tool that detects errors and other potential problems upfront during development. JSHint is a powerful JavaScript code quality tool that not only detects errors but also checks if they adhere to some well known standards. There are lot of JSHint plugins available for different IDEs. The plugin grunt-contrib-jshint helps to lint JavaScript files for grunt based projects.
To create the "jshint" task we've to create a new property in the passed object called jshint. You can't use any name for the property, usually it matches with the last part of the plugin name.
grunt.initConfig({ jshint: { // set task options } });
Listing 22. "jshint" task
The value of the task is going to be a JSON object where we specify the path of JS files that needs to be linted and other things. Discussing more about the grunt plugins and their configuration properties are outside the scope of the article. I'll try to explain a little bit but I request you to refer their github docs.
The below code listing shows our "jshint" task definition.
grunt.initConfig({ jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish') }, gruntfile: 'Gruntfile.js', src: ['src/js/**/*.js', '!src/js/templates.js'] } });
Listing 23. "jshint" task
We've defined three properties in the above task: options, gruntfile and src. The gruntfile and src are nothing but targets. Targets are more like sub-tasks. The options property contains the global options that applied to all targets. The names of the targets not necessarily to be the same as above but it could be anything. Let's take a closer look at the options. We've defined couple of properties over there: jshintrc and reporter. jshintrc is used to specify the configuration file that is used to customize jshint validation. The reporter property is used to specify a custom reporter to format the output nicely. Let's talk about the targets. We've created separate targets for linting different types of files to manage things easier. The gruntfile is a target to which we assigned the full path of the grunt file that needs to be linted. The src target takes an array of strings. The first item in the array represents the JavaScript source files that needs to be linted and the second string starts with "!" represents the file that should be excluded. The templates.js is created when we compile the handlebar templates. We don't want to run code quality over it and so we excluded it.
Next, we've to create a configuration file to customize jshint. Create a file with name .jshintrc at the root of the project. This file contains a JSON object through which we can customize the jshint warnings. I'm not gonna explain about the different configuration properties. I request you to refer the JSHint Options Reference. For the time being.. close your eyes..🙈 close your ears..🙉 don't talk..🙊 and copy paste the below code to your .jshintrc file.
{ "node": true, "browser": true, "esnext": true, "bitwise": true, "camelcase": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": true, "newcap": true, "noarg": true, "quotmark": "single", "undef": true, "unused": true, "strict": true, "trailing": true, "smarttabs": true, "jquery": true, "jasmine": true, "globals": { "define": true, "cordova": true, "Camera": true, "LocalFileSystem": true, "ImageResizer": true } }
Listing 24. .jshintrc file
Though it's not mandatory we also need to download jshint-stylish to format the errors output for better readability. jshint-stylish is nothing but a node module. Let's install it.
$ npm install jshint-stylish --save-dev
Listing 25. Installing jshint-stylish plugin
We've our first grunt task ready! It's time to put it into test! Before that, how we can run a grunt task? We can run a grunt task by running the command grunt <task-name> from the terminal. You can pass additional parameters to a grunt task by appending --{parameter name}={value} following the command. The passed parameters can be accessed using the grunt.option method from the task.
To execute our "jshint" task you should run the below command.
$ grunt jshint
Listing 26. Running "jshint" task
Below is the output you'll see in the terminal. You can notice that both the targets ("gruntfile" and "src") are run one by one.
Since we don't have much JS files, the output is not interesting enough!
Let's play little more. Create a simple test JS file under "src/js" folder with name test.js and drop the below lines. Don't forget to delete this file later :)
x = 12; if(x == 5) console.log("numbers are different");
Listing 27. test.js
Run again grunt jshint and you'll see the below output.
The errors are nicely formatted by the help of jshint-stylish generator.
Instead of running all the targets you can also run a single target by appending the target name after ":".
For ex. to invoke only the gruntfile target you can do,
grunt jshint:gruntfile
Listing 28. Running "gruntfile" target
That's it! Looks like our "jshint" task works just fine. One thing to point out is, we haven't automated our task. We want to kick it automatically whenever we change any JS file. We'll see how to achieve that when we work on our last task - "watch".
12.1.1 The "config" object
Before working on the next task, I would like to refactor the code little bit. As a good practice, developers use a config object to specify the source, destination directories and other properties instead of duplicating them everywhere. Without much questioning, I would also like to do the same thing here.
First let's create a config object.
var config = { src: 'src', // working directory tests: 'tests', // unit tests folder dist: 'cordova', // distribution folder supported: ['ios', 'android'], // supported platforms platform: grunt.option('platform') || 'ios' // current target platform };
Listing 29. The "config" object
Our config object contains a bunch of properties. Let me explain them.
src -> Specifies the directory that contain all the source files.
tests -> Specifies the directory that contain all the unit tests.
dist -> Specifies the directory where the final distributable files will be dropped (We'll discuss more about this one in the next part).
supported -> Specifies the platforms (iOS and Android) that are supported.
platform -> Specifies the current platform at which we are targeting the grunt commands. We are gonna design the tasks such a way that, at a time we can target for a single platform, that means we can't run grunt commands in parallel that performs operations for both the platforms. Let's say you want to build the app for both ios and android, you've to run two separate grunt commands sequentially. We've to pass the platform parameter as an argument from the command line (for ex. grunt deploy --platform=android). If we don't pass the platform parameter then "ios" is considered as the current targeted platform. Developers who are developing the app for only androids can change the default platform to "android" so they don't have to pass the platform parameter every time they run a task.
Let's refactor our "jshint" task to use the config object.
var config = { src: 'src', // working directory tests: 'tests', // unit tests folder dist: 'cordova', // distribution folder supported: ['ios', 'android'], // supported platforms platform: grunt.option('platform') || 'ios' // current target platform }; grunt.initConfig({ config: config, jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish'), }, gruntfile: 'Gruntfile.js', src: ['<%= config.src %>/js/**/*.js', '!<%= config.src %>/js/templates.js'] } });
Listing 30. Using "config" object in "jshint" task
One important thing I should tell you is about how we access the config object from the tasks. Usually developers assign the config object as a property to the passed object so that they can access it's properties easily anywhere using <%= config.{prop} %> .
12.2 Create task to compile handlebar templates
Let's create our second task: compile handlebar templates.
Below is the task definition. Explanation follows it.
handlebars: { compile: { options: { amd: true, processName: function (filepath) { var pieces = filepath.split('/'); return pieces[pieces.length - 1].split('.')[0]; } }, src: ['<%= config.src %>/html/{,*/}*.handlebars'], dest: '<%= config.src %>/js/templates.js' } }
Listing 31. "handlebars" task
Let me explain what the above task definition does. It compiles the handlebar templates into a single JavaScript file called templates.js. Each compiled template is available as a function in a JavaScript object. We've set the amd property to true so we can load the compiled JS file like any AMD module using requirejs. The src property is used to specify the location of the handlebar templates and the dest property is used to specify the destination location of the compiled JS file. We've overridden the processName method to make the individual compiled template function easily accessible as direct properties of the template object. Now we can run grunt handlebars to compile the templates, but wait.. we don't have any templates right now, we'll put this task into test once we wire-up everything.
12.3 Create task to setup server
As I said, we are gonna develop this app as browser runnable. For that, we need to setup a server to serve the files. We are not going to use IIS or Apache as the web server. We are already using node and using the grunt-contrib-connect plugin we can easily setup a mini web server that runs on node.
As you might already guessed the name of this task is going to be "connect".
connect: { options: { hostname: 'localhost', open: true, livereload: true }, app: { options: { middleware: function (connect) { return [ connect.static(config.src), connect().use('/bower_components', connect.static('./bower_components')) ]; }, port: 9000, open: { target: 'http://localhost:9000/index.<%= config.platform %>.html' } } } }
Listing 32. "connect" task
Like other tasks, the "connect" task also contains an options property. In that property we specified the global options like hostname and two more properties. The livereload option is intended to work with grunt-contrib-watch plugin to auto-refresh the browser whenever you change a file. We'll discuss more about it when we get into the "watch" task.
The app property is a target or sub-task that is used to run the development server to serve the files from the source directory. In the options property of the target we've setup the middleware to deliver the static files, setup the port and the html that should be opened in the browser. To know more about this plugin please refer their github docs.
Now you can start the server by running the command,
$ grunt connect:app:keepalive
Listing 33. Running "connect" task
You'll see the message "Started connect web server on http://localhost:9000" message in the terminal with index.ios.html file is opened up in the browser. Note that I've passed an additional argument called "keepalive" after the target to keep the server running else the server will start and stop immediately and you don't see anything happening in the browser.
If you want to run the server for android platform you've to pass the platform parameter as "android" like below.
$ grunt connect:keepalive --platform=android
Listing 34. Running "connect" task targeting "android" platform
This will open up the index.android.html file in the browser. To stop the server you've to press CTRL + C.
12.4 Create task to watch file changes and invoke other tasks
During development time, we need to trigger some tasks whenever we change some file. For example, whenever we change a JavaScript file we like to run the "jshint" task. Whenever we change a handlebar template we have to run the "handlebars" task. There is no fun in using Grunt if we don't automate these tasks. Instead of running these tasks manually, by using the grunt-contrib-watch plugin we can monitor the file changes and fire-up the respective tasks.
Here is our watch task definition.
watch: { // Watch grunt file. gruntfile: { files: ['Gruntfile.js'], tasks: ['jshint:gruntfile'] }, // Watch javascript files. js: { files: [ '<%= config.src %>/js/**/*.js', '!<%= config.src %>/js/templates.js' ], tasks: ['jshint:src'], options: { livereload: true } }, // Watch handlebar templates. handlebars: { files: [ '<%= config.src %>/html/{,*/}*.handlebars' ], tasks: ['handlebars'], options: { livereload: true } }, // Watch html and css files. livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ '<%= config.src %>/index.<%= config.platform %>.html', '<%= config.src %>/css/safe.css', '<%= config.src %>/css/safe.<%= config.platform %>.css' ] } }
Listing 35. "watch" task
That's quite a big task. If you take a close look at the definition everything makes sense. We've setup separate targets to watch gruntfile, JS files under "src/js" folder, handlebar templates, HTML and CSS files. For each target, we've specified the tasks that should be invoked in the tasks property. For example if you see the handlebars target, it watches the handlebar templates and when any template changes it triggers the handlebars task and that recompiles the handlebar templates.
One interesting thing I should talk about is livereload. The targets that have the livereload property set to true auto-refreshes the browser every time the files specified in the task changes. That means you don't have to press F5. Finally, you've a target with name livereload where we've specified the livereload server port number and the remaining files (HTML and CSS) that needs to be watched. Let's put the "watch" task into some tests.
First, run the watch task from terminal.
$ grunt watch
Listing 36. Running "watch" task
This will print out the following output in the screen.
Our watch task has just started his work for the day. Now go ahead modify the grunt file and save it. You'll see some new messages in the screen.
Actually what happened behind the scene is when you changed and saved the grunt file the watch task identified it and invoked the associated task which is jshint:gruntfile. To test the livereload feature you've to run both the "connect" and "watch" tasks one by one. Whenever you want to run more than one task you've to go for composite tasks and that's what we are gonna see next.
12.5 Create composite task
Usually we need to run two or more grunt tasks sequentially during development or deployment. For example, during development first we need to run the task to start the server and then we need to run the task to watch for file changes. We need to run both the "connect" and "watch" tasks sequentially. Whenever you need to run more than one task you've to go for composite tasks.
In grunt, you can create a composite task by calling grunt.registerTask method. This method accepts the first argument as the composite task name and the second argument as an array of tasks that has to be invoked in a linear fashion. For the record we can also run grunt tasks in parallel but we are not going to do that here.
Let's create a composite task called "serve" to start the server and watch for changes. This is the task you'll be kicking every time before starting development. You should add the below lines after the grunt.initConfig method.
grunt.registerTask('serve', [ 'connect', 'watch' ]);
Listing 37. "serve" task
Let's run the task and see what happens.
$ grunt serve
Listing 38. Running "serve" task
You'll see the index.ios.html file is opened in the browser (if you remember the default platform is "ios") and from the terminal you can notice the "watch" task has started and waiting... Now if you change any files specified in the watch task you'll see not only the specified tasks will get executed but also the changes are immediately reflected in the browser.
Before wrapping up the "serve" task I would like to add two more tasks to it. Every-time I start my development, I also need to check the quality of JS files and compile the handlebar templates before starting the server. Let's stack up both the jshint and handlebars tasks to the list.
grunt.registerTask('serve', [ 'jshint:src', 'handlebars', 'connect', 'watch' ]);
Listing 39. "serve" task
Below is the complete code for Gruntfile.js.
module.exports = (function () { 'use strict'; return function (grunt) { // Load all the grunt plugins require('load-grunt-tasks')(grunt); // Config object. var config = { src: 'src', // working directory tests: 'tests', // unit tests folder dist: 'cordova', // distribution folder supported: ['ios', 'android'], // supported platforms platform: grunt.option('platform') || 'ios' // current target platform }; grunt.initConfig({ config: config, // Make sure code styles are up to par and there are no obvious mistakes. jshint: { options: { jshintrc: '.jshintrc', reporter: require('jshint-stylish') }, gruntfile: 'Gruntfile.js', src: ['<%= config.src %>/js/**/*.js', '!<%= config.src %>/js/templates.js'] }, // Precompile the handlebar templates. handlebars: { compile: { options: { amd: true, processName: function (filepath) { var pieces = filepath.split('/'); return pieces[pieces.length - 1].split('.')[0]; } }, src: ['<%= config.src %>/html/{,*/}*.handlebars'], dest: '<%= config.src %>/js/templates.js' } }, // Grunt server settings connect: { options: { hostname: 'localhost', open: true, livereload: true }, app: { options: { middleware: function (connect) { return [ connect.static(config.src), connect().use('/bower_components', connect.static('./bower_components')) ]; }, port: 9000, open: { target: 'http://localhost:9000/index.<%= config.platform %>.html' } } } }, // Watch files for changes and runs tasks based on the changed files. watch: { // Watch grunt file. gruntfile: { files: ['Gruntfile.js'], tasks: ['jshint:gruntfile'] }, // Watch javascript files. js: { files: [ '<%= config.src %>/js/**/*.js', '!<%= config.src %>/js/templates.js' ], tasks: ['jshint:src'], options: { livereload: true } }, // Watch handlebar templates. handlebars: { files: [ '<%= config.src %>/html/{,*/}*.handlebars' ], tasks: ['handlebars'], options: { livereload: true } }, // Watch html and css files. livereload: { options: { livereload: '<%= connect.options.livereload %>' }, files: [ '<%= config.src %>/index.<%= config.platform %>.html', '<%= config.src %>/css/safe.css', '<%= config.src %>/css/safe.<%= config.platform %>.css' ] } } }); // Start the server and watch for changes. grunt.registerTask('serve', [ 'jshint:src', 'handlebars', 'connect', 'watch' ]); }; })();
Woohoo! we've completed all the grunt tasks. We'll be creating more in future but what we've here is enough to kickstart the work. It's OK to take a break (🍵 or 🍹) before proceeding further.
Listing 40. Complete Gruntfile.js
13. Wire-up everything
We've done quite some work so far. We've installed the necessary tools, setup the project structure, downloaded the libraries and created grunt tasks. Now, it's time to wire-up the components and finish the skeleton. Once we done with this work we'll have all the pieces connected and we'll also see a sample view rendered in the browser.
To complete the skeleton, we've to finish the below tasks.
- Add references to Ratchet CSS stylesheets in our html files
- Add reference to require.js library
- Configure require.js
- Render a sample view
Before starting to work run the grunt serve task.
$ grunt serve
Listing 41. Running "serve" task
This will open up the index.ios.html file in the browser. From now on, you don't have to worry about compiling the templates or refreshing the browser. Let grunt do it's job!
13.1 Add references to Ratchet CSS stylesheets in our html files
If you remember all the downloaded libraries exists in the "bower_components" folder. The Ratchet CSS files exist under the "ratchet/dist/css" folder. The "css" folder contains files for both the platforms.
Update the html files to include the reference to the common ratchet.css file and the platform specific css file. The index.ios.html file should've reference to both ratchet.css and ratchet.ios.css. The index.android.html file should have reference to both ratchet.css and ratchet.android.css.
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="format-detection" content="telephone=no" /> <!-- Sets initial viewport load and disables zooming --> <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, target-densitydpi=device-dpi" /> <meta name="msapplication-tap-highlight" content="no" /> <link rel="stylesheet" type="text/css" href="bower_components/ratchet/dist/css/ratchet.css" /> <link rel="stylesheet" type="text/css" href="bower_components/ratchet/dist/css/ratchet-theme-ios.css" /> <link rel="stylesheet" type="text/css" href="css/safe.css" /> <link rel="stylesheet" type="text/css" href="css/safe.ios.css" /> <title>Safe</title> </head> <body> </body> </html>
Listing 42. index.ios.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="format-detection" content="telephone=no" /> <!-- Sets initial viewport load and disables zooming --> <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" /> <meta name="msapplication-tap-highlight" content="no" /> <link rel="stylesheet" type="text/css" href="bower_components/ratchet/dist/css/ratchet.css" /> <link rel="stylesheet" type="text/css" href="bower_components/ratchet/dist/css/ratchet-theme-android.css" /> <link rel="stylesheet" type="text/css" href="css/safe.css" /> <link rel="stylesheet" type="text/css" href="css/safe.android.css" /> <title>Safe</title> </head> <body> </body> </html>
Listing 43. index.android.html
13.2 Add reference to require.js library
Since we are using require.js for managing modules, all we need is reference to the require.js library in our html files. All the other JS files (modules) are dynamically loaded by requirejs on demand.
<body> <h1>Safe</h1> <script src="../bower_components/requirejs/require.js" data-main="js/main"></script> </body>
Listing 44. Adding require.js library reference in html files
The data-main attribute in the above script tag represents the relative path of the JS file that acts as the main entry point of the program. In our app, the main entry point will be a file with name main.js under "js" folder and that's why we set the value as "js/main" (Note that, we don't have to specify the extension ".js").
13.3 Configure require.js
Requirejs helps us to write modular code by taking the burden of dynamically loading and injecting modules. For people who aren't worked with requirejs I recommend to spare some time on their website. A basic knowledge on this library will help you to follow the coming paragraphs easier.
Create main.js file under "js" folder. We are gonna write all the configuration and the startup code in this file. The configuration basically contains the mappings of the module names with the physical file paths.
Here is the configuration code.
(function () { 'use strict'; require.config({ baseUrl: 'js', paths: { jquery: '../../bower_components/jquery/dist/jquery', underscore: '../../bower_components/underscore/underscore', backbone: '../../bower_components/backbone/backbone', validation: '../../bower_components/backbone.validation/dist/backbone-validation-amd', stickit: '../../bower_components/backbone.stickit/backbone.stickit', touch: '../../bower_components/backbone.touch/backbone.touch', handlebars: '../../bower_components/handlebars/handlebars.runtime' }, shim: { jquery: { exports: '$' }, underscore: { deps: ['jquery'], exports: '_' }, backbone: { deps: ['underscore', 'jquery'], exports: 'Backbone' }, handlebars: { exports: 'Handlebars' } } }); // TODO: startup code })();
Listing 45. Configuring requirejs in main.js
The require.config method takes a single object that contains all the configuration properties. The baseUrl property specifies the base location from where the modules are loaded by default. All the others paths specified in the mappings are relative to this baseUrl. The paths object contains the mapping between the module names with their actual physical paths. The shim property is used to configure the non-AMD dependencies (the libraries that are not AMD based like jQuery) used in the project. To know more about requirejs configuration please refer their docs.
Next, we've to write the startup code. The startup code is nothing but the main function called by requirejs once the main.js file is downloaded to the browser.
Below is our startup code.
// TODO: startup code require(['backbone'], function(Backbone) { Backbone.history.start(); });
Listing 46. requirejs startup code
Before talking about what we are starting up let me explain about the require method. The require method takes two parameters. The first parameter is an array of strings. Each string represents a module (dependency) name. The second parameter is the callback function that will be called once all the modules are loaded. The loaded modules (either function or object) are then passed as arguments to the callback function.
In the above code, we are asking requirejs to load the module with name "backbone". What we are doing in the callback function is something important. We are starting the Backbone's history! This will trigger the default route that is configured to the Backbone router. In the default route handler we are gonna render a sample view that is wired with a sample model.
13.4 Render a sample view
Before getting into further coding you should know a little bit about Backbone - the spine of our app.
13.4.1 Backbone
Backbone gives structure to our web applications through its components like Models, Collections, Views and Router. Backbone is neither MVC (like Angular) nor MVVM (like Knockout). It's a special mutant! Views are the core components of Backbone next to Models and usually they are hulky. Backbone provides an events module and all of it's components uses this module to emit events. Backbone is un-opinionated, we can easily extend/customize its behaviors. I strongly recommend people who are new to Backbone to visit their website and also most importantly go through the Todo application. Both are very helpful resources.
Let's create a sample model, view and render it in the default route. By doing this, we'll get the picture of how all things will be connected and this will help us when we work on each page in our app. In the next part, we are gonna throw away the sample model and we'll be creating the actual models required for our mobile app.
Before getting into work, first create three folders with names "models", "collections" and "views" under "js" folder.
13.4.2 Create a sample model
Models are the heart 💛 of Backbone. Like any typical model, they wrap the data and the logic associated with it. They also emit events.
In Backbone applications, you can create a model by simply instantiating the type Backbone.Model.
For ex.,
var hello = new Backbone.Model({ message: 'Hello, this is Vijay demonstrating how to build a hybrid mobile app' });
Listing 47. Instantiating Backbone.Model
The properties of the model are directly passed as an object to the constructor. You can access the properties using the get method passing the property name.
var message = greeting.get('message'); console.log(message);
Listing 48. Getting property value using "get" method
You can set new values to the properties using the set method.
hello.set('message', 'Thank you, bye..');
Listing 49. Setting property value using "set" method
Usually in applications we don't create model instances directly from Backbone.Model. Instead we create custom models by extending Backbone.Model and create objects from them (like we do in static-typed languages like C# or Java).
The below code shows how we can create a custom model Greeting by extending Backbone.Model.
var Greeting = Backbone.Model.extend({ defaults: { message: null } }); var hello = new Greeting({ message: 'Hello, this is Vijay demonstrating how to build a hybrid mobile app' }); console.log(hello.get('message'));
Listing 50. A sample backbone model
One of the crucial feature of the Backbone models are they emit events. We'll see more about events once we start using them. To complete our skeleton, the Greeting model we created above is sufficient.
Create a new file with name greeting.js under "js/models" folder and copy paste the code from below listing to it. Since we are using requirejs, we've to declare our Greeting model as an AMD module. All the dependencies (like Backbone) needed to define our model should be injected to it. To declare a module in requirejs we've to use the define method which is similar like the require method which we saw before. The require method is mostly used at the starting point of your application and the define is used to declare modules which will be loaded by requirejs on demand.
define(['backbone'], function(Backbone) { 'use strict'; var Greeting = Backbone.Model.extend({ defaults: { message: null } }); return Greeting; });
Listing 51. greeting.js
Now we've a sample backbone model ready. Let's go ahead and create the view.
13.4.3 Create view
Views are the second most important component in Backbone (actually there are also collections we'll see about them in the next part). They do all the heavy lifting 💪 in backbone applications. They accept the model, render the HTML, handle the events etc.
Let's create a simple view to display the greeting. We don't create views by directly instantiating Backbone.View. Instead, as we did in models, we extend the Backbone.View class and create custom views.
Create a new file greeting.js under "Views" folder and drop the below content.
define(['backbone'], function(Backbone) { 'use strict'; var GreetingView = Backbone.View.extend({ render: function() { this.$el.html('' + this.model.get('message') + '
'); return this; } }); return GreetingView; });
Listing 52. Backbone view
One of most important method of a Backbone view is render and one of the most important property is $el. The render method is where we generate the HTML. Views also has other important methods like initialize and destroy which we'll see about them later. As default, Backbone creates a "div" element for the view but we can change that to an "ul", "input" or any HTML element. The $el property represents the DOM element created by the view. The good thing about Backbone is it provides the $el as a jQuery object so that we can manipulate the view's DOM by all those *powerful* jQuery methods.
In our above view, we've used the jQuery's html method to append the message to the dynamically generated "div" element. Note that, the model you pass to the constructor while creating the view is automatically available as a property in the view (this.model).
The below snippet shows how you can instantiate and use the GreetingView.
var helloGreeting = new GreetingView({ model: new Greeting({ message: 'Hello, this is Vijay demonstrating how to build a hybrid mobile app' })}); $('body').append(helloGreeting.render().el);
Listing 53. Instantiating GreetingView
The HTML we generated in the GreetingView is very less but usually that's not the case. It's hard to craft HTML inside views; it's better use any of the templating library out there like handlebars. Let's create a handlebar template file and move the HTML into it.
Create a file with name greeting.handlebars under the "html" folder. Copy and paste the below line into it.
<h1>{{message}}</h1>
Listing 54. greeting.handlebars
If you notice the template we've a string "message" enclosed in double braces. It's nothing but a handlebar expression. When handlebars compile the template it replaces that expression with the value of the message property in the passed Greeting model instance. We'll see more about Handlebars expressions in the next part.
To use the template you've to refactor the GreetingView a little bit.
define(['backbone', 'templates'], function(Backbone, templates) { 'use strict'; var GreetingView = Backbone.View.extend({ tpl: templates.greeting, render: function() { this.$el.html(this.tpl(this.model.toJSON())); return this; } }); return GreetingView; });
Listing 55. Using template in GreetingView
We've used a custom property called tpl to assign the template (Note that, the templates are compiled to functions and available in the injected template object). In the render method we've modified the code to render the HTML by calling the template function passing the model to it. One important thing to keep in mind is we've to serialize the model before sending it to template.
Let's complete the final piece - router.
13.4.4 Create router
It's the router where the ceremony 💏 happens. Backbone router listen to URL hash change events and sees whether it matches with any of the configured route and if yes it invokes the handler mapped to that route. Usually the job of the handler is to pull the model, feed it to the view and render it. You can create a router by extending the class Backbone.Router.
Create a new file with name router.js under "js" folder. You've to fill the file with the below code. As usual, the explanation follows :)
define(['backbone', 'models/greeting', 'views/greeting'], function(Backbone, Greeting, GreetingView) { 'use strict'; var Router = Backbone.Router.extend({ routes: { '': 'home' }, home: function() { var greetingView = new GreetingView({ model: new Greeting({ message: 'Hello, this is Vijay demonstrating how to build a hybrid mobile app' })}); $('body').prepend(greetingView.render().el); } }); return new Router(); });
Listing 56. A custom backbone router
There are really some important things to explain about the router. We've created a custom router by extending the Backbone.Router. The dependencies needed for our router like backbone, greeting model and view are passed in an array to the define method . The routes property contains the route mappings. The key of the mapping object represents the route pattern and the value represents the handler that will be invoked when the URL matches that pattern. We've defined a single route which is the default route and it has the key as empty string and whenever the URL don't contain any hash segment it matched this route and the home function will be called.
What we are doing in the home function is, we are instantiating the GreetingView passing the Greeting model and finally rendering it inside the body. The most important thing to talk about is the last line return new Router();. Unlike in Greeting model or view, instead of returning the type we are returning an object of it. The reason of why we are doing so is we need a single instance of router throughout our application. Requirejs caches the instance of the router we return and whenever needed it provides the same instance from the cache.
We've to do one more final thing before seeing the sample view in the browser. We've to update the startup code in the "main.js" to require the router. As I said earlier, calling the Backbone history will trigger the default route and render the sample view.
// Startup code require(['backbone', 'router'], function(Backbone) { Backbone.history.start(); });
Listing 57. Requirejs start-up code in main.js
Woohoooo! looks like we've completed our skeleton. If you remember we run the command grunt serve before started coding. Whatever changes we've done so far should be reflected in the browser at this time. If you've done everything right you should see the below screen. If you don't see the same.. re-run the "grunt serve" command or check the terminal to know what went wrong!
In bottom you'll see the links to download the source code and to the github repository where the complete working source code lives. The attached code don't have the bower and node packages. Once you download it you should run the npm install and bower install commands before firing the grunt commands.
14. Wrapping up
In this part we've completed the base work (which I call as Walking Skeleton) to develop the hybrid mobile app. We saw how to configure the environment, download the libraries, set-up the tasks and finally we connected all the parts and rendered a sample view. The walking skeleton we created in this part will help very much in the upcoming parts to complete all the functionalities of the app.
15. What's Next?
I think this part might have been little tedious for you and also you haven't seen any code directly related to the mobile app yet. But wait.. the coming ones are gonna be very interesting! In the next part, we'll see how to add, edit and delete a secret photo. We'll also see how to build the list page to display the stored photos. In the third part, we'll see how to implement the login, register and all other related functionalities. I bet we'll face some interesting challenges on the way and we'll also learn how to knock-out 👊 them with our fist.
Please free to post a comment if you've any question or suggestion or just to say 'good job'. Your comments are the catalyst to publish the next part quicker. Bye, see you soon! 🙋