How to create an awesome hybrid mobile app using Cordova - Part II
Table of Contents
- 1. Introduction
- 2. Part II - Building the app
- 3. Let's get to work
- 4. Build the List page
- 5. Build the Single Photo View page
- 6. Configuring environments for iOS and Android
- 7. Install Cordova
- 8. Create deployment grunt tasks
- 9. Deploy the app in iOS device
- 10. Deploy the app in Android device
- 11. Summary
- 12. What's Next?
1. Introduction
Hello friends, finally.. I'm back to share the second part of creating the hybrid mobile app. Apologies for taking too long! From my experience it looks like writing is harder than coding! Thanks for all the readers who shared their valuable comments. In the previous part, we worked mainly on setting up the development environment and also we tried to create a "Walking Skeleton" by wiring up the crucial components. Sadly, we haven't seen any code specific to the mobile app yet. I bet this part is gonna throw away that. We are going to do some really cool stuff here! We are going to build two important pages: "list page" and "single photo view page". We'll also see how to deploy the app in an iPhone and Android. At the end of this part, you'll have a simple and nice app (with some features) smiling right at you from your mobile.
Actually, I tried to complete most of the core functionalities of the app in this part including the add/edit feature (which is kind of little complicated) but that ballooned the size of the article to a mini-book. Well, nowadays most people don't like to spend more time on reading lengthy articles and so I moved the add/edit feature to a separate part. You would be expecting two more parts from me. In the upcoming one, we'll see how to build the add/edit page and in the last part we'll see how to implement the login/registration feature and the related functionalities of it.
2. Part II - Building the app
In this part, we are going to complete the basic functionalities of the app. We are going to build the "home page" (also I call as "list page" in some places) that displays the list of persisted secret photos and the "single photo view page" that displays the details of a single persisted photo. Most importantly, we'll also see how to install Cordova and deploy the app in an iPhone and Android. I'll be using Mac for iPhone and Windows for Android. I didn't get enough time to try Mac for Android but there are enough resources available in Cordova website and other blogs that'll help you.
Before dive into coding, I would like to brief about some of the questions I had and the challenges I faced during the development.
One of the question that constantly bugged me was where I'm going to store the photos? I was sure about one thing that I'm not gonna store them in server. I was looking for some client-side options and there are more than one: FileSystem, LocalStorage and WebSQL. Initially I was thinking of using LocalStorage as the complete persistent option but the noises around it's persistence especially in older iOS versions and the maximum data size worried me. After getting some convincible replies from forums and doing myself some experiments I decided to use LocalStorage to store the metadata information of a photo like description, thumbnail and FileSystem to store the actual photo as base64 string. Since we are using two persistent stores to save a single photo I've to come-up with two Backbone models. One model contains properties to represent the metadata information and the other one to represent the actual photo.
The second challenge was regarding crafting the HTML markup adherence to Ratchet's standards. Ratchet expects you to drop the header and menu elements as direct descendants of <body>. I was thinking to create a main <div> element to represent the current page and place all the elements underneath it. By this way, whenever I need to render a new view or page, I'll remove the current <div> element and render the new one. Since Ratchet wants the header and menu as the first thing inside <body> I can't able to use a <div> element to represent the page and instead I've to use the <body> itself to represent it. This means, whenever the page changes I can't remove the <body> element (doing so will remove the referenced scripts as well) instead I should remove its contents. This kind of behavior is not common in Backbone based applications but still it's possible. I'm not sure how well you understood the problem but I hope you'll get a better understanding when we get there.
The third challenge was making the app browser runnable. Enabling the app browser runnable speeds up debugging and development. Unlike typical hybrid mobile apps, in our app we need to access the mobile hardware components like Camera, Storage etc. These APIs provided by Cordova to access these hardware components are not supported by browser and so when you try to run the app in browser they are gonna blow away. To solve this, I created fake implementations of those APIs and they are injected if the app is running inside browser. Since we are using RequireJs for injecting modules it was little tricky initially but I could solve it later.
The fourth challenge was regarding customizing the Backbone persistent strategy. As default Backbone persists the models to server through AJAX. But in our case we need to persist the data in client; half in FileSystem and half in LocalStorage. One thing I really love about Backbone is customization. We can easily override the persistent strategy at global or model level to store the data to a different place. After little brainstorming (brainwarming?) I decided to use LocalStorage as the persistent option at global level. Only for the file model - the model that represents the actual photo, we'll be overriding at that class level. There's already a Backbone plugin called backbone.localstorage available to persist data in LocalStorage. I tried to utilize it but it didn't work out very well in our situation. The problem in our case is before saving the data we've to encrypt it and after loading the data we've to decrypt it. The Crypto API that does the encryption/decryption is asynchronous in nature, because of that we've to make the complete persistence strategy asynchronous. Since writing from and reading to localstorage is a synchronous process, the backbone.localstorage plugin provides a synchronous API and sadly it's difficult to customize as well! At last I took a bruteful approach by creating my own custom plugin (based on the backbone.localstorage) that encrypts the data before persisting it and decrypts after loading it through an asynchronous API.
The final challenge I had was regarding storing the encryption/decryption key for the password. iOS provides something called Keychain that helps to store all the credentials in a very safe place. At the time of developing the app Android didn't provide any option like that and I'm not sure they still does though! At one point of time, I thought of using Keychain for iOS and LocalStorage for Android to store the credential but that complicated things for me. Finally, I've to come with a simple solution using LocalStorage.
That's all the important challenges I faced and the decisions I took against each one of them drove me towards the below design that depicts the core objects of the app.
3. Let's get to work
It's time to put our fingers to work. Following are the important stories we are going to work in this part.
- Build the list page
- Build the single photo view page
- Configure environments for iOS and Android
- Install Cordova
- Create deployment grunt tasks
- Deploy the app in iOS device
- Deploy the app in Android device
We'll build the other pages like add/edit, login, register in the upcoming parts.
4. Build the List page
In this story, we are going to build the list page. Let's relook at the list page mockup (Fig. 2) and list out the tasks we have to do!
The first thing we should do is to get the list of stored photos from localstorage and display them in a list. We should also provide a search textbox to search the photos based on description. We also need to provide links to navigate to add/edit page and logout from the app.
We've to prepare the models before building this page, keeping that in mind below are the list of tasks we've to do to complete the list page. Also.. don't forget to delete the sample model, view and the template we created in the previous part.
- Prepare the Models
- Create the View
- Update the HTML files
- Add the CSS styles
- Display the list of photos
- Implement Search functionality
4.1 Prepare the Models
As I told you before, we are going to store the actual photo in the FileSystem and the related metadata in the LocalStorage. Since we've two different persistent stores I decided to use two different models but that doesn't mean we can't go with a single model.
4.1.1 Create the Photo model
Let's first create a model called Photo to store the metadata information like description, thumbnail (base64 string) and the last saved date.
Create a new script file with name "photo.js" under the "models" folder. Drop the below code inside the file. Whenever I say paste or drop the code please don't do the same. I would recommend you to type it line by line.
define(['backbone'], function (Backbone) { 'use strict'; // Model that represents the meta-data information of the secret photo. // Contains properties to represent description, thumbnail and last-saved-date. var Photo = Backbone.Model.extend({ // Properties (attributes) defaults: { description: null, thumbnail: null, lastSaved: Date() }, // Validation rules. validation: { description: { required: true }, thumbnail: { required: true } } }); return Photo; });
Listing 1. Photo model
You've already seen about the backbone models in the previous part and I hope you can pretty much understand most of the bits in the above code. The default value for the lastSaved property is set to the current datetime. If you remember, in the last part I told you that Backbone provides only partial support for validation and to resolve that we've installed a plugin called backbone.validation. The validation property is used to supply the validation rules for each property to the plugin. To know more about the backbone.validation plugin please refer their docs. We've applied the required validation rule for both description and thumbnail.
4.1.2 Create the File model
To complete the List page we don't have to create the File model but for the sake of completeness let's do that. Create a new file with name "file.js" under "models" folder and paste the below code.
define(['backbone'], function (Backbone) { 'use strict'; // Model used to represent the actual image file. var File = Backbone.Model.extend({ defaults: { data: null // file content }, // Validation rules. validation: { data: { required: true } } }); return File; });
Listing 2. File model
The File model contains a single property called data which is used to store the actual base64 string of the photo. The File model may look plain now but we'll be adding more methods to it when we start working on the Add/Edit page.
4.1.3 Create the Photos collection
We haven't talked much about Backbone collections yet. Backbone Collections are nothing but ordered set of models and they are very powerful over plain arrays. For our List page we need a collection to store photos.
Create a new folder "collections" under "js" and create a new file "photos.js" under it. Paste the below code.
define(['backbone', 'models/photo'], function (Backbone, Photo) { 'use strict'; // Represent collection of photos. var Photos = Backbone.Collection.extend({ model: Photo, // Sort the photos in descending order by 'lastSaved' date. comparator: function(photo) { var lastSavedDate = new Date(photo.get('lastSaved')); return -lastSavedDate.getTime(); } }); return Photos; });
Listing 3. Photos collection
The model property is used to specify the type of model that will be stored in the collection. The Photos collection is used to store collection of photos and so we've set the value of model property to Photo. Models stored in the collection are not sorted by default and by specifying a comparator (a string or function) whenever models are added or removed they are automatically sorted. But remember, editing the properties doesn't sort the collection and you've to do that manually by calling the sort method.
4.2 Create the View
Our List page is the home view for our app. When I say list page or home page I mean the samething. Let's create a file with name "home.js" under "views" folder. We are going to build this view progressively. Let's start with some basic code.
define(['backbone', 'templates'], function(Backbone, templates) { 'use strict'; var Home = Backbone.View.extend({ template: templates.home, render: function() { this.$el.html(this.template()); return this; } }); return Home; });
Listing 4. Home view
The above code is not much different from the sample greeting view we saw in the previous part. Before adding more code to the view let's craft the layout first.
4.2.1 Create the HTML layout
From the mockup you can know that the List page contains three sections: header, body and footer. The header contains the title of the app and links to logout and add/edit new photo page. The body contains the search textbox and the photos list. The footer contains links to navigate to settings and info pages. Ratchet provides quite a bunch of UI components to build an app. Some of the important components are bars, lists, buttons, segmented controls, forms, popovers, modals and sliders. I would recommend you to spend some time on Ratchet's website to get a quick overview of these components. Coming back to our app, for the header we can use the title bar component. For the list we can use table view and for the footer we can use the tab bar.
Create a file with name "home.handlebars" under the "html" folder and paste the below markup.
<!-- HEADER --> <header class="bar bar-nav"> <a href="#logout" class="btn btn-link btn-nav pull-left"> <span class="icon icon-close"></span> </a> <a href="#add" class="btn btn-link btn-nav pull-right"> <span class="icon icon-plus"></span> </a> <h1 class="title">Secret Photos</h1> </header> <!-- FOOTER --> <nav class="bar bar-tab"> <a class="tab-item active" href="#photos"> <span class="icon icon-home"></span> <span class="tab-label">Home</span> </a> <a class="tab-item" href="#settings"> <span class="icon icon-gear"></span> <span class="tab-label">Settings</span> </a> <a class="tab-item" href="#info"> <span class="icon icon-info"></span> <span class="tab-label">Info</span> </a> </nav> <!-- CONTENT --> <div class="content"> <div id="search-form" class="content-padded"> <input id="search" type="search" placeholder="Search"> </div> <div id="list-container"> </div> </div>
Listing 5. home.handlebars
The markup is simple. One important thing to note down is the header and footer elements should be placed before the content. The content <div> element contains a search form and a container (#list-container) to hold the table view component that is used to display the list of photos.
4.2.2 Update the HTML files
We've added the reference to requirejs script file inside the <body> in both index.ios.html and index.android.html files. Because of the inconvenience in the Ratchet's HTML markup we are going to use the <body> element as the View's DOM. Every time we navigate to a different view we've to clean-up the <body>. Removing the children of the <body> also removes the script references. To avoid that, let's update the HTML files to move the script references to the head.
<!DOCTYPE html> <html> <head> ... <script src="../bower_components/requirejs/require.js" data-main="js/main"></script> <title>Safe</title> </head> <body> </body> </html>
Listing 6. Moving the script reference to head for both index.ios.html and index.android.html
4.2.3 Add the CSS styles
We need some additional CSS styles for structuring and decorating the pages. Instead of creating the styles on need basis, let's add all the styles required for the app right now!
Open the "safe.css" file and add the below styles.
*:not(input):not(textarea) { -webkit-user-select: none; /* disable selection/Copy of UIWebView */ -webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */ } /* Common styles */ .img-responsive { width: 100%; height: auto; } .text-center { text-align: center; } .text-left { text-align: left; } .text-right { text-align: right; } .bold { font-weight: bold; } .field-label { margin: 10px 0 10px 0; display: inline-block; } /* Login Page */ .login-page .content { padding-top: 65px !important; } .login-page form { margin-top: 15px; } .forgot-password { display: block; } #search-form { margin-bottom: 0px; } .description { margin: 10px; } #total { margin-top: 10px; }
Listing 7. safe.css
For Androids, Ratchet displays the navigation bar at the top and that's the nice thing because androids already come with a bottom menu and placing the tab bar at the bottom causes some usability issues. Actually, I chose Ratchet for these reasons in the first place. It provides separate behaviors for iOS and Androids. But unfortunately in this case I changed my mind to place the tab bar at the bottom same as in iOS devices (very bad!) and the reason is purely my laziness to work little bit on the styling especially in the title and the links if I've to place the tab bar at top.
To place the tab bar at bottom for Androids, we've to override some CSS classes in our "safe.android.css" file. Open the file and paste the below styles.
/* Android styles */ .bar-tab { bottom: 0; top: auto; } .bar .bar-nav { border-top: solid 1px #ccc; } .content { padding-bottom: 50px !important; } .title { text-align: center; }
Listing 8. safe.android.css
4.3 Update the Router
Before running the app to see the changes, we've to fix the routing! Open the "router.js" file, remove the home method and get rid of the dependencies to greeting model and view. Create a new routing method called photos (I thought of naming this as home, since I already named it as photos in my Github repo going with the same name here) and paste the below code.
define(['backbone', 'views/home'], function (Backbone, Home) { 'use strict'; var Router = Backbone.Router.extend({ routes: { '': 'photos', 'photos': 'photos' }, photos: function () { var homeView = new Home(); homeView.setElement($('body')); homeView.render(); } }); return new Router(); });
Listing 9. Configuring route for list page in router.js
All we doing in the photos method is instantiate and render the home view. But there is something interesting happening there. Do you see the statement homeView.setElement? It is used to set the <body> as the home view's DOM. If you remove the view now by calling homeView.remove(), it'll remove the complete body of the page. Don't worry about this now, later we'll introduce a new method to the views to remove only the contents of the body and not itself when we navigate to a different page.
Alright, we've some working code now. Let's quickly fire-up the browser and see how things look.
4.4 Run the app in browser
Open your terminal and run the command grunt serve. If there are no mistakes you'll see the index.ios.html launched in the browser and it should look as below.
If you are running the app in Chrome you can use the in-built emulation to preview it in any one of the dozens of supported devices. Below screen shot shows the emulation for iPhone 4.
That's cool! Let's see how we can display the photos.
4.5 Display the list of photos
To display the list of photos we should complete the save functionality. Implementing the save functionality is kind of complicated and we'll get to that once we start creating the add/edit page. But thankfully, that's not gonna stop us from completing the list page. We could pass some hard-coded data to the home view and complete the list page.
Backbone views are composable! In other words, they can be nested. A single view can have one or more child views. One thumb rule I follow while creating the views is, for each model I'll create a separate view. I can better explain this using an example. Let's say you've to display a collection of records in a grid. First you've to create a model class to represent the record and then you have to create a collection. For the grid you can actually use a single view that takes the collection and renders it. But doing so will force you to move away from the Backbone philosophy especially when you try to manage individual rows of the grid. A better practice is to create a separate view to represent row. That means, you'll have a grid view (that takes a collection) and it contains an array of row views (each taking a model). By this way you'll have a very maintainable code but fairly complicated!
Coming back to our home view, we are going to create two more views. One view to display the photos list (nothing but Ratchet's table view component) and another one to display an individual photo item (table view cell).
4.5.1 Create photo item view
The photo item view is used to display the details of the single row in the list. Create a new file with name "photoitem.js" under "views" folder. As usual, throw the below code to the file.
define(['backbone', 'templates'], function(Backbone, templates) { 'use strict'; // Photo item view. var PhotoItemView = Backbone.View.extend({ tagName: 'li', className: 'table-view-cell media', template: templates.photoitem, render: function() { this.$el.html(this.template(this.model.toJSON())); return this; } }); return PhotoItemView; });
Listing 10. Photo item view
The PhotoItemView is used to render a single photo. As default Backbone views create a <div> element and we can change that into any other using the tagName property. In our case, the PhotoItemView should render an <li> element and so we've set the tagName property to "li". Also to transform the <li> element to a Ratchet table view cell we've applied some specific CSS classes through the className property. The render method is simple and straight. It just pass the input model to the template and render it.
If you see the mockup you'll notice that each list item displays the thumbnail of the photo and the description. To achieve that we need some additional markup inside the <li> element and that means we need a template!
Create a new file with name "photoitem.handlebars" under the "html" folder with the below markup.
<a href="#photos/{{id}}" class="navigate-right"> <img class="media-object pull-left" src="data:image/png;base64, {{thumbnail}}"> <div class="media-body"> {{description}} </div> </a>
Listing 11. photoitem.handlebars
The template contains an anchor element and on touching it'll open up the single photo page. Inside the anchor element, we've the <img> element to display the thumbnail and a <div> for description. We already saw about handlebars templates in the last part. The strings enclosed in double braces are nothing but handlebars expressions. The expressions used above are replaced by the properties of the model passed to it. Handlebars provides quite a bunch of expressions for iterating (#each) arrays, checking conditions (#if) etc. There's not quite a lot to learn about Handlebars and just a fair idea is enough to build this app. To learn more about handlebars please take a look at their website.
4.5.2 Create photos list view
The photos list view is used to display the list of photos. It takes the Photos collection as input and renders it as a table view component.
Create a new file with name "photos.js" under "views" folder. Paste the below code. I would request you to read the code closely... but anyway explanation follows.
define(['underscore', 'backbone', 'views/photoitem'], function (_, Backbone, PhotoItemView) { 'use strict'; // Photos list view. var Photos = Backbone.View.extend({ tagName: 'ul', className: 'table-view', initialize: function () { this.childViews = []; this.listenTo(this.collection, 'reset', this.refresh); }, render: function () { // Iterate through the collection, construct and render the photoitemview for each item. this.collection.each(_.bind(function (model) { var photoItemView = new PhotoItemView({ model: model }); this.childViews.push(photoItemView); this.$el.append(photoItemView.render().el); }, this)); return this; }, // Remove the child views and re-render the collection. refresh: function () { _.each(this.childViews, function(childView) { childView.remove(); }); this.render(); } }); return Photos; });
Listing 12. Photos view
We've three important methods in the above view: initialize, render and refresh You know well about the render method. It's time to talk about the initialize method which is one of the built-in methods of Backbone views. The initialize method is called when the view is first created. It is the right method to initialize variables, listen to events etc. We are doing couple of things in our initialize method. First we are initializing an array to store the photo item views and then listening to the reset event of the passed photos collection to re-render the view. What we are doing in the render method is easily understandable. We are iterating the photos collection, instantiating photo item view for each model and appending to it's DOM (Note that we don't need a separate template for this view). The refresh method is a custom method that is used to re-render the view whenever the collection changes.
We've the child views ready and it's time to update the parent view to put them to use.
4.5.3 Update home view to render the photos list
Update the define method of home view to include the photos view as a dependency and assign it to the variable PhotosView. Instantiate the PhotosView in the initialize method passing the collection and render it inside the container. Alright, below is the updated home view and it's all yours!
define(['backbone', 'templates', 'views/photos'], function(Backbone, templates, PhotosView) { 'use strict'; var Home = Backbone.View.extend({ template: templates.home, initialize: function () { // Throw error if photos collection not passed. if(!this.collection) { throw new Error('collection is required'); } // Instantiate the child view that displays the photos list passing the collection. this.listView = new PhotosView({ collection: this.collection }); }, render: function() { this.$el.html(this.template()); this.$('#list-container').append(this.listView.render().el); return this; } }); return Home; });
Listing 13. Home view
Finally... let's update the router to pass some sample photos so we can see them displayed in the list page.
4.5.4 Update Router to pass the collection
Update the define statement to add dependencies to photos collection and photo model. Update the photos method to create a photos collection with some sample photos and pass to the home view. I've used a base64 string of a car as the sample thumbnail image. For the sake of article, I'm gonna use the same to represent the full-size image as well!
define(['backbone', 'views/home', 'collections/photos', 'models/photo'], function (Backbone, Home, Photos, Photo) { 'use strict'; var Router = Backbone.Router.extend({ routes: { '': 'photos', 'photos': 'photos' }, photos: function () { var thumbnailImage = 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAUCAIAAAB00TjRAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAASAAAAEgARslrPgAABntJREFUSMfFlltsXEcZx+d2rrvnnF2vvU7WGzv1JTZJHSeo4SJVpEoRVZMmQpF4KFRCgFCAqoISURGpAsFbHyCRKpSKh0IkLi+lKFBUEWhEColwkiq4abK4tvc4Xttr733PZc/ZOWdmeFg1LU1TCC/M42i++X3/b77LQCEE+P8t8r+ZCSHAnX7/+w6EECD04ffAe1LfOwwhvIfznEOEwF1MyD2xe2AeRZ1G06vVOrV65DggCLgAiDMt8ADtcs6lZNLM55Nj4zC/DWAMhACcf2AkyH9J7klubWwslUoLhcLy4mJlY9N33U6r6QRBrl55OPJvJtOerJgDA7qiMqdtMbZ7W/7+T38m+dmjQNc/0IP/FHwhBAAQQk7p4lvXl1bXYoRkWaaUMs6zAwMXLl364+/OfrVT//vknhUzPWwae2Z2p1IpWVF++9JLWjccsouPWsbe7/9Q7NkL7/CA3FUuAADCnuig3b755tx62+nLZjVNgwgRQiCEYRheX1g6Urml7ZrelRvMr6x0+9O35uereuLgY4cmxyfcIFAeOvDqfGHzW08d+N4PpAMPA8YgxndR38vndxyMOWdxHDjtzfXyar1BXS/2/dB1Q8+J43ijXLZXV8ySPbq5erLe2acTmjDHHzloZfpBGN6X29pcX/eazUEjqfRlSn+98HkEBp87iWdm3vsKd+AhZJx3NiqdcjlsNGLXizwfRRQ4rWqpVKlstquVsF7lXsugXpbgoUTykuNfazaHjdSAqm7T1SRCMoSIc0yIJMmKJGNMVCu13KjLYx8Z/+4Ja2K8B3o/vhOG3StXa394pWnbvtPqek6347HAbfj+ApJxKt3vNtOAYwiSqi6ribjjD5umme7XJSyrmsA4hhBgLDBUNH3JXuBcjE3txKpaLC6W/vHG+GAumx7QvnTMPHq0F4N38QFj1TNn0C/PVIeH/XoV/XOOSiigoS3Qr5GV3r33/k89NHT2Fx9dLvRv30ETZpPDQQm5NCYSlOIuoAGPujyiIo4EiwBjgjHOAYEiZrwd49EtW+eY2Gi1jhx4ZPDHpyHBQAgCABCcQ4RaNwvOn89BQ0fZrN6sglQmiGjoOkF+NDG8y4R8X3lpa702PLX396538upb9bA7LuipB6bjtuutLRJZ4QIACASAEEIAIYKQQ8Qg6AKyLbPlN+XNH4VSPQjP/ezFFyYmMk99BwoBBecAwrBaaT9/anWp2KJd5ro0jiljgdvUOCWE0FTGYlGeiWQq/Ze1+adtN5PLKUKUvM5Mc/25HTm/VVcJYlyIdzojAAABwCGCMU3kpgRCx0qbgZmxdG3pzbmf7P/Y5145LzgngnOIcbO08refP58fHOgjWJIkggkiUqSDLkMAITlyoSSjpFENN18sN7ZunxpKG2YqndqsFDvujdXVCUOLaSTebfYQQAgggBAyhBUVzTUa1Mw8/fVjjuefKhaXlQQAgAtBejUgjdz3TWa0rt3SCUacYSAQEH39WYax4/m58bFcc/VZ5DQi5siDZkJ9/AtPjI6NvXD6tL2yUqH+MJI6qskhYgiHMQtiFgkQcUEj3uHy0M3CLOVkYsuyveyFoSxJ2U882Cs00qsBFaHtmCwwrkiSpKkxYwJAKABotrIQKEvFB/oMJ8bX235Nj0cxee38+dnLV1rtNsbo7UZH1awShQCCDg2sTMbKZpCsyImEput9lnXj7YVfnfvTx2e0G4VCJwi+/cwzX3zySQAAJoRAhAAAyXT6p2fPijh69sSJV187P6Br3TAgRuIbX/nycqHgNpt2HFfNkZJWW51ffHD//lqtbtv2jsnJ4Xx+ciinyvLcyy9fuHLV0PWvPf6EOTzChJAURVYULZHYue+TyhvXLr7++mOHDyuyPLVzp9/pCCEURYGu61JKwzBknMeMz16ePX78+HqpBAB49MiRQ4cPl0qlWr3utNoIQtsuXr54MZvNHjx0iBByeXa2aNuyqvq+H8cxiyJJlgVjQghFlhVF0TRN17RUOtU/kAUQpixrbGxsenp6ZGRkaGjINE24sbHhOE65XLZtu1gs1uv1dqu1trYWBIHneZVKRQjRS2bBuaIoCcNgjFmWlc1mNU3r6+szDSOVSpmWaVmpZCJhmKZhGIlEQtd1VVVVVZVlGWOsKIokSb2RffvH8P6JJ4SIoogxRimllHLO4zjmnAMAEEIYY4SQJEkIY4kQRVHwe+bHh39SboMghLfx/wLg1oT6h+nc9AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wMS0xN1QwODoxMjo1Mi0wNTowMD3Ny5MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDEtMTdUMDg6MTI6NTItMDU6MDBMkHMvAAAAAElFTkSuQmCC'; var homeView = new Home({ collection: new Photos([ new Photo({ id: 1, thumbnail: thumbnailImage, description: 'HDFC Bank Credit Card' }), new Photo({ id: 2, thumbnail: thumbnailImage, description: 'Passport Photo' }), new Photo({ id: 3, thumbnail: thumbnailImage, description: 'Driving License' }) ]) }); homeView.setElement($('body')); homeView.render(); } }); return new Router(); });
Listing 14. Update router to pass sample photos
4.5.5 Run the app
If the grunt serve is still running in the background you might have seen the below screen. Sometimes I noticed grunt is too lazy to detect the new files added and you might see no changes and in that case wake it up from sleep by re-running the grunt serve command.
That looks good! Let's complete the search functionality to complete the list page.
4.6 Implement Search functionality
The final pending task in the list page is the search functionality. We are going to implement the typical wildcard text search based on description. We also going to display the total count of photos that matches the search in a label below the search textbox as part of this functionality.
The following are the tasks we are gonna do.
- Implement a new method in the photos collection to search and return photos based on description
- Update the home view to listen to the search textbox change event and update the list
- Update router to pass a new model to store the search text and the total count of persisted photos
4.6.1 Implement a new method in the photos collection to search and return photos based on description
Create a new method in the photos collection called search that filters the models based on description and returns a new collection. If the passed text is empty the method returns all the photos else returns the ones that contains the passed text in the description.
define(['backbone', 'models/photo'], function (Backbone, Photo) { 'use strict'; // Represent collection of photos. var Photos = Backbone.Collection.extend({ ... // Text search by 'description'. search: function (text) { if (!text) { return this.models; } return this.filter(function (photo) { return photo.get('description').toLowerCase().indexOf(text.toLowerCase()) > -1; }); } }); return Photos; });
Listing 15. Adding search method to Photos collection
4.6.2 Update the home view to listen to the search textbox change event and update the list
We need a model to bind with the search text and the photos count. By using a model we don't have to listen to the search textbox change event directly and update the list, instead we can listen to the model change event and refresh the list which is more backbony way. Also, let me point you again that Backbone don't have two way binding and if you remember we've downloaded a plugin called backbone.stickit just for that. The stickit plugins wires up the DOM elements with the model properties and serve two-way binding.
Here is the updated home view.
define([ 'underscore', 'backbone', 'views/photos', 'templates' ], function ( _, Backbone, PhotosView, templates ) { 'use strict'; // Home view. // Renders the home view that displays the list of all secrets and handles the UI logic associated with it. var Home = Backbone.View.extend({ // Set the template. template: templates.home, // Set the bindings. bindings: { '#search': 'search' }, initialize: function () { // Throw error if photos collection not passed. if(!this.collection) { throw new Error('collection is required'); } // Throw error if model is not passed. if(!this.model) { throw new Error('model is required'); } // We need this collection because of search. this.filtered = new Backbone.Collection(this.collection.models); // Instantiate the child view that displays the photos list passing the collection. this.listView = new PhotosView({ collection: this.filtered }); // Run search whenever user type or change text in the search textbox. this.listenTo(this.model, 'change:search', this.searchPhotos); // Update the count label. this.listenTo(this.model, 'change:total', this.updateLabel); }, render: function () { // Render the outer container. this.$el.html(this.template(this.model.toJSON())); // Bind the model props to DOM. this.stickit(); // Render the child list view. this.$('#list-container').append(this.listView.render().el); // Cache the DOM els for quick access. this.$total = this.$('#total'); return this; }, searchPhotos: function () { var filteredCollection = this.collection.search((this.model.get('search') || '').trim()); this.model.set('total', filteredCollection.length); this.filtered.reset(filteredCollection); }, updateLabel: function () { var total = this.model.get('total'), message = ''; if (total > 0) { message = total + ' photo(s) found'; } else { message = 'No photos found'; } this.$total.html(message); } }); return Home; });
Listing 16. Updating Home view for search
I hope by seeing the code you can understand what's going on. I would like to highlight some important points though! First, you can see a new property called bindings that is used to map the DOM elements with the model properties for two-way binding (stickit). The key represents the DOM element, in this case it's the id of the search textbox and the value represents the model property search.
Second, you can see now we are not passing the passed collection to the photos view and instead we are passing a filtered collection. The reason for doing so is, every time the search changes we don't want to read the photos from the disk. We are keeping the original collection untouched and instead we are performing the search over it that gives back a new filtered collection which is passed to the photos view.
Third, we are listening to change events of both the search and total properties. Whenever the search property changes we are calling the searchPhotos method to search the collection and re-render the list. Whenever the total property changes we are updating the label.
Fourth, you should note down couple of things in the render method. In rendering the template, now, we've to pass the model as well and the next thing is the call to stickit method (which is attached as a mixin to views by the backbone.stickit plugin) which is what actually triggers the binding operation.
Last but not least don't forget to update the template "home.handlebars" to display the search textbox on condition and to display the count label.
... <div class="content"> {{#if total}} <div id="search-form" class="content-padded"> <input id="search" type="search" placeholder="Search"> </div> {{/if}} <p id="total" class="text-center"> {{#if total}} {{total}} photo(s) found {{else}} No photos found {{/if}} </p> <div id="list-container"> </div> </div>
Listing 17. home.handlebars
4.6.3 Update router to pass a new model to store the search text and the total count of persisted photos
We've to pass the model that binds to the search text and count to the home view from the router. To keep things simple, instead of creating and instantiating a custom model we can instantiate directly from Backbone.Model.
Modify the photos method as below.
define(['backbone', 'views/home', 'collections/photos', 'models/photo', 'stickit'], function (Backbone, Home, Photos, Photo) { ... var Router = Backbone.Router.extend({ ... photos: function () { var thumbnailImage = ... var photos = new Photos([ new Photo({id: 1, thumbnail: thumbnailImage, description: 'HDFC Bank Credit Card'}), new Photo({id: 2, thumbnail: thumbnailImage, description: 'Passport Photo'}), new Photo({id: 3, thumbnail: thumbnailImage, description: 'Driving License'}) ]); var homeView = new Home({ model: new Backbone.Model({ search: '', total: photos.length }), collection: photos }); homeView.setElement($('body')); homeView.render(); } }); return new Router(); });
Listing 18. Router
If you see the code, we've updated the home view creation code to pass a model instance with two properties search and total along with the collection. Don't forget to include the dependency to "stickit" in the define statement else the binding don't work. We'll refactor the router soon and create a separate file where all the extensions and mixins will be added.
If you have done everything right you would see the home screen without any errors. Type some text in the search textbox and verify the list is updated with only the photos whose description matches!
Congratulations! we've completed the first page. Now I would request you to press the pause button and relook what you've done so far that would help you down the road. Our next target is the single photo view page.
5. Build the Single Photo View page
As the name, the single photo view page is used to display the metatdata information and the actual image of a single photo. It also has links to navigate to edit photo and list pages. The user will be taken to the single photo view page by selecting any item from the list page. Though the page looks simple in the mockup we've to do some additional work to make it functional. In single photo view page we are going to use the notification API of Cordova for displaying alerts. We are not going to directly use the Cordova's notification API in our code instead we are going to deal with it through an adapter. We've to do this else we can't make our app browser runnable! We'll also see how to create a fake object that looks similar like the adapter and helps the app to run in browser.
Following are the tasks we've to do to complete this page.
- Create the View
- Temporarily fix the model
- Create the notification adapter
- Refactor and extend
- Fix the router
5.1 Create the View
The single photo view has to show the complete information of a photo and so it needs instances of both the Photo and File models. We are going to use lazy loading to display the photo image. In other words, first we are going to show the metadata information and then we are going to fetch the file asynchronously and display the image. Create a new file with name "photo.js" under "views" folder. Below is the complete code of the single photo view. Compared to other views (excluding setting and info) this is a simpler one!
define([ 'underscore', 'backbone', 'templates', 'adapters/notification' ], function ( _, Backbone, templates, notification ) { 'use strict'; // Photo view. // Renders the photo view and handles the UI logic. var PhotoView = Backbone.View.extend({ template: templates.photo, initialize: function(options) { // If model is not passed throw error. if(!this.model) { throw new Error('model is required'); } // If file model is not passed throw error. if(!(options && options.file)) { throw new Error('file is required'); } // Set the passed file model to a property. this.file = options.file; }, render: function () { this.$el.html(this.template(this.model.toJSON())); this.$photo = this.$('#photo'); this.loadImage(); return this; }, loadImage: function () { this.file.fetch() .done(_.bind(function () { this.$photo.attr('src', 'data:image/png;base64,' + this.file.get('data')); }, this)) .fail(function () { notification.alert('Failed to load image. Please try again.', 'Error', 'Ok'); }); } }); return PhotoView; });
Listing 19. Single photo view
If you scan the initialize method, you can understand we are passing both the Photo and File model instances to the view. The passed Photo model instance is directly available in the model property (Backbone does this magic behind the scene!) and the File model instance is available in the options object. The render method is quite simple to understand. First we are rendering the basic information by rendering the template passing the model and then we are calling the loadImage method.
In the loadImage method we are reading the stored base64 image from the disk and showing it using an <img> tag. Note that, the fetch method returns a jQuery promise object (to know about promises please read this). In the done method we are reading the actual base64 string from the data property and set it to the <img>'s src attribute. In the fail method we are displaying a lame notification to the user. We don't have the notification object ready and we'll create it soon once we done with other work.
Let's create the view's markup (template). Create a new file with name "photo.handlebars" under the "html" folder. Paste the below markup.
<header class="bar bar-nav"> <a href="#photos" class="btn btn-link btn-nav pull-left"> <span class="icon icon-left-nav"></span> </a> <a href="#edit/{{id}}" class="btn btn-link btn-nav pull-right"> <span class="icon icon-edit"></span> </a> <h1 class="title">View Photo</h1> </header> <nav class="bar bar-tab"> <a class="tab-item active" href="#photos"> <span class="icon icon-home"></span> <span class="tab-label">Home</span> </a> <a class="tab-item" href="#settings"> <span class="icon icon-gear"></span> <span class="tab-label">Settings</span> </a> <a class="tab-item" href="#info"> <span class="icon icon-info"></span> <span class="tab-label">Info</span> </a> </nav> <div class="content"> <div class="content-padded"> <div class="description">{{description}}</div> <img id="photo" class="img-responsive" src="data:image/png;base64,{{thumbnail}}"> </div> </div>
Listing 20. photo.handlebars
The markup contains header, footer and content. The header contains links to home and edit pages. The content contains a <div> element to show the description and an <img> tag to display the photo from the base64 string.
5.2 Temporarily fix the File model
If you see the File model (listing 2) we don't have a fetch method! The fetch method is one of the built-in methods of the Backbone model. As the name, it is used to fetch the data from the configured persistent store. Backbone also provides methods like validate, save, destroy etc. As default, Backbone uses server as the persistent option and whenever you do a CRUD operation on a model like fetch or save it uses AJAX to communicate to the server in the configured URL (through the url property in the model or collection). In our case, we are not storing the file in the server and so we cannot go with the default way. Since we haven't set the url property in the file model, when you call the fetch method Backbone will throw some error. Actually, what we want to do is: read the encrypted file from the disk, decrypt it and set to the data property. One way we could this is override the fetch method and do the necessary work there. We can do the same for save and destroy methods as well. It looks like there is a better way to solve this. As I told in the very beginning Backbone is very flexible enough to change the persistent option. When the persistent mechanism changes instead of overriding quite bunch of the model methods and scatter the work we can override a single method call sync. All the CRUD calls (fetch, save or destroy to a model or collection is routed through the sync method and by overriding this one you can totally change the default persistent option. We'll see more about this once we started working on the add/edit page.
For the time being, to complete the single photo view page let's override the fetch method of the File model temporarily and return a hardcoded base64 image (the same one we used for thumbnail). We'll really fix the File model while we start working on the add/edit functionality.
define(['backbone', 'jquery'], function (Backbone, $) { 'use strict'; // Model used to represent the actual image file. var File = Backbone.Model.extend({ ... fetch: function () { var d = $.Deferred(); var image = 'iVBORw0KGgoAAAANSUhEUgAAACoAAAAUCAIAAAB00TjRAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAASAAAAEgARslrPgAABntJREFUSMfFlltsXEcZx+d2rrvnnF2vvU7WGzv1JTZJHSeo4SJVpEoRVZMmQpF4KFRCgFCAqoISURGpAsFbHyCRKpSKh0IkLi+lKFBUEWhEColwkiq4abK4tvc4Xttr733PZc/ZOWdmeFg1LU1TCC/M42i++X3/b77LQCEE+P8t8r+ZCSHAnX7/+w6EECD04ffAe1LfOwwhvIfznEOEwF1MyD2xe2AeRZ1G06vVOrV65DggCLgAiDMt8ADtcs6lZNLM55Nj4zC/DWAMhACcf2AkyH9J7klubWwslUoLhcLy4mJlY9N33U6r6QRBrl55OPJvJtOerJgDA7qiMqdtMbZ7W/7+T38m+dmjQNc/0IP/FHwhBAAQQk7p4lvXl1bXYoRkWaaUMs6zAwMXLl364+/OfrVT//vknhUzPWwae2Z2p1IpWVF++9JLWjccsouPWsbe7/9Q7NkL7/CA3FUuAADCnuig3b755tx62+nLZjVNgwgRQiCEYRheX1g6Urml7ZrelRvMr6x0+9O35uereuLgY4cmxyfcIFAeOvDqfGHzW08d+N4PpAMPA8YgxndR38vndxyMOWdxHDjtzfXyar1BXS/2/dB1Q8+J43ijXLZXV8ySPbq5erLe2acTmjDHHzloZfpBGN6X29pcX/eazUEjqfRlSn+98HkEBp87iWdm3vsKd+AhZJx3NiqdcjlsNGLXizwfRRQ4rWqpVKlstquVsF7lXsugXpbgoUTykuNfazaHjdSAqm7T1SRCMoSIc0yIJMmKJGNMVCu13KjLYx8Z/+4Ja2K8B3o/vhOG3StXa394pWnbvtPqek6347HAbfj+ApJxKt3vNtOAYwiSqi6ribjjD5umme7XJSyrmsA4hhBgLDBUNH3JXuBcjE3txKpaLC6W/vHG+GAumx7QvnTMPHq0F4N38QFj1TNn0C/PVIeH/XoV/XOOSiigoS3Qr5GV3r33/k89NHT2Fx9dLvRv30ETZpPDQQm5NCYSlOIuoAGPujyiIo4EiwBjgjHOAYEiZrwd49EtW+eY2Gi1jhx4ZPDHpyHBQAgCABCcQ4RaNwvOn89BQ0fZrN6sglQmiGjoOkF+NDG8y4R8X3lpa702PLX396538upb9bA7LuipB6bjtuutLRJZ4QIACASAEEIAIYKQQ8Qg6AKyLbPlN+XNH4VSPQjP/ezFFyYmMk99BwoBBecAwrBaaT9/anWp2KJd5ro0jiljgdvUOCWE0FTGYlGeiWQq/Ze1+adtN5PLKUKUvM5Mc/25HTm/VVcJYlyIdzojAAABwCGCMU3kpgRCx0qbgZmxdG3pzbmf7P/Y5145LzgngnOIcbO08refP58fHOgjWJIkggkiUqSDLkMAITlyoSSjpFENN18sN7ZunxpKG2YqndqsFDvujdXVCUOLaSTebfYQQAgggBAyhBUVzTUa1Mw8/fVjjuefKhaXlQQAgAtBejUgjdz3TWa0rt3SCUacYSAQEH39WYax4/m58bFcc/VZ5DQi5siDZkJ9/AtPjI6NvXD6tL2yUqH+MJI6qskhYgiHMQtiFgkQcUEj3uHy0M3CLOVkYsuyveyFoSxJ2U882Cs00qsBFaHtmCwwrkiSpKkxYwJAKABotrIQKEvFB/oMJ8bX235Nj0cxee38+dnLV1rtNsbo7UZH1awShQCCDg2sTMbKZpCsyImEput9lnXj7YVfnfvTx2e0G4VCJwi+/cwzX3zySQAAJoRAhAAAyXT6p2fPijh69sSJV187P6Br3TAgRuIbX/nycqHgNpt2HFfNkZJWW51ffHD//lqtbtv2jsnJ4Xx+ciinyvLcyy9fuHLV0PWvPf6EOTzChJAURVYULZHYue+TyhvXLr7++mOHDyuyPLVzp9/pCCEURYGu61JKwzBknMeMz16ePX78+HqpBAB49MiRQ4cPl0qlWr3utNoIQtsuXr54MZvNHjx0iBByeXa2aNuyqvq+H8cxiyJJlgVjQghFlhVF0TRN17RUOtU/kAUQpixrbGxsenp6ZGRkaGjINE24sbHhOE65XLZtu1gs1uv1dqu1trYWBIHneZVKRQjRS2bBuaIoCcNgjFmWlc1mNU3r6+szDSOVSpmWaVmpZCJhmKZhGIlEQtd1VVVVVZVlGWOsKIokSb2RffvH8P6JJ4SIoogxRimllHLO4zjmnAMAEEIYY4SQJEkIY4kQRVHwe+bHh39SboMghLfx/wLg1oT6h+nc9AAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNS0wMS0xN1QwODoxMjo1Mi0wNTowMD3Ny5MAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTUtMDEtMTdUMDg6MTI6NTItMDU6MDBMkHMvAAAAAElFTkSuQmCC'; this.set({data: image}); d.resolve(); return d.promise(); } }); return File; });
Listing 21. Overriding Fetch method in File model
We've pretty much completed the work required in this page except for one thing: notification. Let's do it!
5.3 Create the notification adapter
For handling notifications across mobile devices Cordova provides a common notification API. This API contains methods to support visual, audible and other kind of notifications. We are not going to directly call the Cordova's notification API methods from our views because doing so will break our app in the browser. We are going to solve this problem by using adapters and fake objects. For notification and for any Cordova API we use in future, we'll create two objects. One is a wrapper object (adapter) that directly calls the Cordova's API methods and the other one is a fake object. Both these objects follows a common interface. Based on the current running environment, we either inject the real object that calls the Cordova's API or the fake one that calls the browser's native methods. The fake notification object will call the browser's notification methods like window.alert, widow.confirm etc.
5.3.1 Create the real object
Create a new folder called "adapters" under the "js" folder. Create a new file called "notification.js" under it. For the sake of completeness I'm going to implement methods for all types of notifications (alert, vibrate, beep) provided by Cordova. Also remember we cannot check this notification object until we run our app inside a device.
Below is the the complete code.
// Notification module. // A wrapper to the cordova notification plugin. // Contains methods to alert, vibrate, beep etc. define(['jquery'], function ($) { 'use strict'; return { alert: function (message, title, buttonName) { var d = $.Deferred(); navigator.notification.alert(message, d.resolve, title, buttonName); return d.promise(); }, confirm: function (message, title, buttonLabels) { var d = $.Deferred(); navigator.notification.confirm(message, d.resolve, title, buttonLabels); return d.promise(); }, prompt: function (message, title, buttonLabels, defaultText) { var d = $.Deferred(); navigator.notification.prompt(message, d.resolve, title, buttonLabels, defaultText); return d.promise(); }, beep: function (times) { navigator.notification.beep(times); }, vibrate: function (milliseconds) { navigator.notification.vibrate(milliseconds); } }; });
Listing 22. Notification adapter
All the methods delegates the calls to the Cordova's notification API (navigator.notification). You can see most of the methods returns a promise object. Cordova APIs are pretty much asynchronous in nature and they always need callbacks. It is difficult to work with callbacks and doing so will produce a code that is unfamously called as "Callback Hell". To avoid that we've embraced the promise pattern and we are using jQuery promises throughout the code. This is an another advantage of using an wrapper/adapter over the actual API, we can hide complexities!
5.3.2 Create the fake object
Create another new folder called "fake" under "js" folder. Create a new file under it with the same name "notification.js" and paste the below code. The fake object is pretty much same like the real one. It provides the same number of methods with the same names taking the same arguments (we've ignored unused arguments though!). If you see the implementations they call the window's alert, confirm or prompt, making it suitable to make some noise in browser.
define(['jquery', 'underscore'], function ($, _) { 'use strict'; return { alert: function (message) { var d = $.Deferred(); window.alert(message); d.resolve(); return d.promise(); }, confirm: function (message) { var d = $.Deferred(); var result = window.confirm(message); if(result) { d.resolve(1); } else { d.reject(); } return d.promise(); }, prompt: function (message) { var d = $.Deferred(); window.prompt(message); d.resolve(); return d.promise(); }, beep: _.noop, vibrate: _.noop }; });
Listing 23. Notification fake object
We've both the real and fake object for notification ready. It's time to inject them into the app based on the environment it's running, whether it is in mobile or browser. We can figure out the environment by checking the userAgent property of the navigator object. We can inject the fake object by simply changing the path of "adapters" to "fake" in requirejs configuration.
Open the "main.js" file and paste the below snippet above the final require() call.
var isDevice = navigator.userAgent.match(/(iPhone|iPod|iPad|Android|BlackBerry|IEMobile)/); if(!isDevice) { require.config({ paths: { adapters: 'fake' } }); }
Listing 24. main.js
5.4 Refactor and extend
Before proceeding further we've to refactor things little bit. As I said in the last part the "main.js" file is the starting point of the application. In the "main.js" we are triggering the backbone history to start the app and this happens at-once the script is loaded in the browser. In Cordova based applications it's better to start the things in the "deviceready" event. This event is used by Cordova to signal the app that the device APIs have loaded and are ready to serve. So, basically we've to run the startup code as we do now for browser and in the "deviceready" event for mobile. Instead of doing this in "main.js", let's do it in a separate file (app.js) that contains all the startup code.
5.4.1 Create app.js
Create a new file with name "app.js" under "js" folder and paste the below code.
// Startup module. define(['backbone', 'extensions'], function (Backbone) { 'use strict'; // Starts the backbone routing. function startHistory() { Backbone.history.start(); } return { // Add extension methods and start app on device ready. start: function (isDevice) { // If the app is running in device, run the startup code in the 'deviceready' event else on document ready. if (isDevice) { document.addEventListener('deviceready', function () { startHistory(); }, false); } else { $(startHistory); } } }; });
Listing 25. app.js
If you see the define statement there are two dependencies: "backbone" and "extensions". "extensions" is a new module which we are going to create soon that will contains all the custom extensions. The start method is the entry point of the app and it takes a single parameter called isDevice. The isDevice parameter is a boolean argument which we will pass from "main.js" that says whether the app is running in browser or mobile. Based on this argument we are triggering the Backbone history in the "deviceready" event or atonce.
Let's update the "main.js" file to call the start method and pass the isDevice argument.
if(!isDevice) { require.config({ paths: { adapters: 'fake' } }); } require(['app'], function (app) { app.start(isDevice); });
Listing 26. main.js
5.4.2 Create extensions.js
Let's create a new file to add the extensions and override the built-in methods of Backbone classes. Create "extensions.js" under "js" folder and drop the below code. Don't get scared by the code, I'll explain about each extension once you done with your pasting :)
define([ 'jquery', 'underscore', 'backbone', 'router', 'validation', 'stickit', 'touch' ], function ( $, _, Backbone, router ) { 'use strict'; function S4() { /*jslint bitwise: true */ return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } // Add custom methods to underscore. _.mixin({ // extension method to create GUID. guid: function () { return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4()); }, // return a promise object with resolve. resolve: function () { var d = $.Deferred(); d.resolve.apply(null, arguments); return d.promise(); }, // return a promise object with reject. reject: function () { var d = $.Deferred(); d.reject.apply(null, arguments); return d.promise(); } }); // Extend backbone model to perform custom validation. _.extend(Backbone.Model.prototype, Backbone.Validation.mixin); // Extend backbone views with custom methods. _.extend(Backbone.View.prototype, { // Stop listening to events and remove the children. ghost: function () { this.unstickit(); this.stopListening(); this.undelegateEvents(); this.$el.html(''); return this; }, // A delegate to router's navigate method. navigateTo: function (page, trigger) { router.navigate(page, trigger || true); } }); });
Listing 27. extensions.js
We've added three new extension methods to underscore: guid, resolve and reject. The guid method is used to create an unique identifier. The resolve and reject are added to deal with promises (you'll understand it better once we see the places where they used).
We've extended the Backbone Model to add advanced validation behaviors through the plugin that we downloaded earlier. Also, we extended Backbone View to add couple of methods: ghost and navigateTo. The ghost method is a very important one and we need that because in our app the view's DOM represents the body element. Typically in Backbone driven apps when the view changes they remove the old view and append the new one. We can't do the same here because if we remove the view the body will be removed and that'll remove all the scripts attached to it as well. So instead of removing we are ghosting it. Means, we are removing the innerHTML and cleaning up all the events attached to it. The navigateTo method adds a shortcut for views to navigate to different views through the router.
That's all the refactoring and extensions we need for now. Let's do the last thing before wrapping up this page - fix the router!
5.5 Fix the router
We've to configure a new route to handle the requests for the single photo view page. We've to wire-up a handler to the new route that takes the photo id as an input parameter and renders the single photo view passing the persisted Photo and an empty File model instance to it. This is the second route we are going to add to our router. Before adding it, we should refactor our router little bit so that we can have some common methods all the handlers can share.
Following are the important points we are going to consider for refactoring.
- Create a separate method (getPhotos) to retrieve all the persisted photos. The retrieved photos are cached in a private variable (photos).
- Create a separate method (renderView) to render the view. All the route handlers calls this method to render the view. This method removes the previously rendered view and render the new one.
Here is our refactored router.
define([ 'jquery', 'underscore', 'backbone', 'adapters/notification', 'models/photo', 'collections/photos', 'views/home' ], function ( $, _, Backbone, notification, Photo, Photos, HomeView ) { 'use strict'; var currentView, // Represents the current view photos; // Collection of photos var Router = Backbone.Router.extend({ routes: { '': 'photos', 'photos': 'photos' }, photos: function () { this.getPhotos() .done(_.bind(function () { this.renderView(new HomeView({ model: new Backbone.Model({ search: '', total: photos.length }), collection: photos })); }, this)) .fail(function () { notification.alert('Failed to retrieve photos. Please try again.', 'Error', 'Ok'); }); }, getPhotos: function () { var d = $.Deferred(); if (photos) { photos.sort(); d.resolve(photos); } else { var thumbnailImage = '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'; photos = new Photos([ new Photo({ id: 1, thumbnail: thumbnailImage, description: 'HDFC Bank Credit Card' }), new Photo({ id: 2, thumbnail: thumbnailImage, description: 'Passport Photo' }), new Photo({ id: 3, thumbnail: thumbnailImage, description: 'Driving License' }) ]); d.resolve(); } return d.promise(); }, renderView: function (view) { if (currentView) { currentView.ghost(); } currentView = view; currentView.setElement($('body')); currentView.render(); } }); return new Router(); });
Listing 28. Refactored router
We've introduced two private variables: currentView and photos. The currentView is used to store the currently rendered view. Every time we render a new view we'll check whether a view is already rendered by checking the currentView is null and if it's not null we remove the the previously rendered view and render the new one. The photos variable is used to store all the persisted photos. We are not going to read the persisted photos from localStorage every-time. We'll retrieve it for the first time and set it to to the photos variable. Note that, the collection stored in the photos variable contains only the meta-data information and not the actual photos.
We've created two private methods: getPhotos and renderView. The getPhotos method retrieves all the persisted photos for the first time and cache it in the photos variable. The renderView method is called from each route handler to render the corresponding view. The renderView removes (Not completely removes the view's DOM. Calling the ghost method removes the children and removes all the event handlers) the previously rendered view and renders the new one.
5.5.1 Add route for single photo view page
Let's add a new route in the routes object to handle the requests to single photo view page.
routes: { '': 'photos', 'photos': 'photos', 'photos/:id': 'photo' }
Listing 29. Adding single photo view route to Routes property
The segment ":id" maps to the query parameter of the hash segment. In our case it represents the photo id which will be passed to the route handler. Below is the route handler code and drop it below the photos method.
// Renders the photo view. photo: function (id) { this.getPhotos() .done(_.bind(function () { var photo = photos.get(id); if (!photo) { notification.alert('Photo not exists.', 'Error', 'Ok'); return; } this.renderView(new PhotoView({ model: photo, file: new File({ id: id }) })); }, this)) .fail(function () { notification.alert('Failed to retrieve photo. Please try again.', 'Error', 'Ok'); }); }
Listing 30. The single photo view route handler
Thank god! Finally, we've completed the single photo view page. Yes, that's a little more work! Hopefully, this will reduce our effort while we work on the coming pages. There are still some more work in the File model but don't worry we'll see once when we start working on the add/edit photo page.
If everything went cool grunt would have refreshed the browser with all the changes at this time. If you click any of the item from the list you should see the below screen.
6. Configuring environments for iOS and Android
We've been working in a Cordova based mobile app without installing Cordova so far. That's the cool thing about hybrid mobile apps we can develop them pretty much like any usual JavaScript application. But, I think now we've reached a point to install Cordova. Actually we can still go on and create add/edit and other pages but I think it's time to press the pause button and see how the app looks in mobile and make sure there are no blocking issues. Before installing Cordova first you've to configure your environment.
You've to configure your machines before running your mobile app in emulator or device. As I said earlier, I've used Mac for iOS and Windows for android development. Initially I had some challenges in configuring the environments for both the platforms. For iOS, you've to join to the Apple Developer Program and create a provisioning profile to run your app in mobile. I thought of writing in detail about configuring both the Mac and Windows environment but after seeing the documentation provided in Cordova website I said myself, it's not required! Cordova website has a very good documentation. They've a dedicated section called "Platform Guides" there they explained in detail about configuring the mobile platforms for different environments. For configuring iOS platform you can read this and for Android you can read this. If you are stopped by any challenges you know what to do, post a comment!
7. Install Cordova
To work with Cordova-based projects we need to install something called Cordova CLI (Command Line Interface). Cordova CLI is shipped as a node package and you can install it using NPM.
Please run the below command to install Cordova CLI.
$ sudo npm install -g cordova
Listing 31. Installing Cordova at global level
Cordova CLI provides a set of commands for creating projects, adding platforms, installing plugins, running in emulators and deploying in devices. Let me give you a quick primer on how to create a sample cordova project, add platforms, build, emulate and deploy.
7.1 Create a sample Cordova project
You can create a Cordova project using the create command. This command creates a set of predefined folders with some sample files that helps to quick start the development.
To create a project with name "HelloWorld" you've to run the below command from terminal.
$ cordova create hello com.example.hello HelloWorld
Listing 32. Create sample Cordova project using "create" command
The first argument "hello" specifies the folder that will be created by Cordova where all your project files and assets lives. The second argument "com.example.hello" that looks like a reverse domain indentifier is used to uniquely identify the app. The third and the final argument "HelloWorld" represents the app's display title and it's optional. We could also change the display title through "config.xml" file. The "config.xml" file is a global configuration file through which we can control many behaviors of the app.
After running the command you'll see the folder "hello" created with the below contents.
Let me tell you about the importance of each folder.
hooks - contain special scripts to customize the cordova commands
platforms - contains platform specific files and assets
plugins - contains built-in and third party plugins
www - this is the working directory that contains the common files (HTML, CSS and JS) that will be copied to all platforms when you run the build command. If you expand this folder you'll see separate folders to store CSS, JavaScript and images.
Let's see how we can add platforms and plugins.
7.2 Adding platforms and plugins
7.2.1 Adding platforms
We can add or remove platforms using the platform command. For example you can add the iOS platform by running the below command. Note, you've to run the command from the "hello" folder.
$ cordova platform add ios
Listing 33. Adding iOS platform
This will add the ios platform specific projects files under the "platforms" folder. You can also add other platforms by changing the platform name in the command.
7.2.2 Adding plugins
We can add or remove plugins using the plugin command. Even for the built-in plugins like Camera, FileSystem you've to add them separately to use them in the app.
The below command tells you how we can add a Camera plugin.
$ cordova plugin add https://git-wip-us.apache.org/repos/asf/cordova-plugin-camera.git
Listing 34. Adding Camera plugin
7.3 Build the project
You can build your Cordova project using the build command. Running the build command generates the platform specific code within the project's platforms subdirectory along with copying all the resources from the "www" folder to respective platform folders.
$ cordova build
Listing 35. Running "build" command
You can also build targeting a particular platform by passing the platform name.
$ cordova build android
Listing 36. Running "build" command for android platform
7.4 Running in emulator
The SDKs for mobile platforms come with emulators. From my experience, Android emulators are slow compared to iOS ones. You can emulate your app by running the emulate command from your cordova project folder.
$ cordova emulate ios
Listing 37. Emulating for iOS
7.5 Deploying to device
We can deploy to a device using the run command.
$ cordova run ios
Listing 38. Deploying to iOS device
We've seen so far how to create a sample Cordova project, run it in an emulator and deploy to a device. Next, we are going to see how we can install our app "Safe" to iPhone or Android. We are going to do that through grunt and for that let's create the pending grunt tasks required for deployment activities.
8. Create deployment grunt tasks
8.1 The Cordova grunt plugin
We've created quite some grunt tasks to simplify development and it's time now to create additional grunt tasks to simplify deployment. In the last section we've installed Cordova and saw how to create a sample project, add platforms and plugins, build, deploy etc. Actually you don't have to all these things manually. Remember, we are using Grunt and we should find how we can automate all these things. Luckily there is a nice plugin available called grunt-cordovacli that does pretty much most of the things like creating project, adding platforms, adding plugins, building, deploying etc. All we've to do is install this plugin and create grunt tasks to take care of Cordova related duties.
8.1.1 Install the plugin
We've installed Cordova globally but unfortuntately the grunt-cordovacli plugin needs Cordova to be installed locally, let's do that.
$ npm install cordova --save-dev
Listing 39. Installing Cordova locally to the project
Once it's installed, run the below command to install our plugin.
$ npm install grunt-cordovacli --save-dev
Listing 40. Installing grunt-cordovacli plugin
8.1.2 Create the cordova grunt task
In the sample Cordova project we created earlier, the "www" folder is the working directory where we dump all our HTML, CSS and JS files. In our case, we are using a separate folder "src" that is not part of the Cordova project as the working directory. Keeping our working directory as a different folder gives us a lot of control and flexibility in deployment. If we use "www" folder as the working directory then we lose the flexibility of minifying the files (it's not impossible but it's not easy) before they are copied to individual platform folder during build.
We are going to see the Cordova project as our distribution folder and we create and keep it outside of our distribution folder. We create separate grunt tasks that minifies and copied the assets to "www" folder before kicking the Cordova build process using our grunt-cordovacli plugin.
Let's create the grunt task to create cordova project, add platforms/plugins, build, emulate and run.
Open your grunt file and add the below task after the "watch". It doesn't matter where you add but arranging the tasks in proper order simplifies your life in finding them.
// Task to install platforms and plugins and to build, emulate and deploy the app. cordovacli: { options: { path: './<%= config.dist %>' }, install: { options: { command: ['create', 'platform', 'plugin'], platforms: '<%= config.supported %>', plugins: [ 'camera', 'file', 'dialogs', 'https://github.com/VJAI/simple-crypto.git', 'https://github.com/wymsee/cordova-imageResizer.git' ], id: 'com.prideparrot.safe', name: 'Safe' } }, build: { options: { command: 'build', platforms: ['<%= config.platform %>'] } }, emulate: { options: { command: 'emulate', platforms: ['<%= config.platform %>'] } }, deploy: { options: { command: 'run', platforms: ['<%= config.platform %>'] } } }
Listing 41. "cordovacli" task
The "cordovacli" task contains different sub-tasks to do different things like install, build, emulate etc. The path property we set in the options specifies the name of the Cordova project folder that'll be created after running the "install" sub-task. The "install" sub-task should be run once to create the cordova project, add platforms (ios and android) and all the required plugins.
Like the "serve" composite task (we created in the last part for development) we need several additional composite tasks deployment. Let's create them one-by-one.
8.2 Create task to create project
We already have the "cordovacli" task that has a sub-task called "install" to create the project. We've to run this task mostly once. Instead of invoking this task as grunt cordovacli:install from terminal it would be nice if we run this something like grunt create. For that, we've to create a wrapper task as below. Drop this task after "serve".
// Create cordova project, add platforms and plugins. grunt.registerTask('create', [ 'cordovacli:install' ]);
Listing 42. "create" task
Now open the terminal and run grunt create. Once the command has run successfully you would see a new folder called "cordova" created at the root level with all the Cordova folders.
8.2.1 The config.xml file
I briefly told you about the "config.xml" file, it is the global configuration file where you can specify many settings that controls the behaviors of an app. For example, you can specify the plugins, preferences, splash screens, icons etc. To know more about this file please read this. When you run the "create" command the "config.xml" file is automatically created for you under the "cordova" folder. Actually you can modify this file directly to add plugins and configure other settings. But instead of doing that, I would encourage to copy the same file to the root folder and then modify it according to our wish. Later when we build our app copy the modified "config.xml" to the "cordova" folder. The advantage of doing so is, let's say for some reason you've to recreate the cordova project and then the changes you've made to the "config.xml" will be lost. To avoid that it's better to work in a copy of this file and drop it to the "cordova" folder before build.
For the time being instead of copying create a new "config.xml" in the root and paste the below content. Don't forget to change the details in the <author> element.
<?xml version='1.0' encoding='utf-8' ?> <widget id="com.prideparrot.safe" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <name>Safe</name> <description> Cordova based mobile app to manage secret photos. </description> <author email="vijay.prideparrot@gmail.com" href="http://prideparrot.com"> Vijaya Anand </author> <content src="index.html" /> <plugin name="Camera" value="CDVCamera" /> <plugin name="File" value="CDVFile" /> <plugin name="com.webXells.imageResizer" value="ImageResize" /> <plugin name="SimpleCrypto" value="SimpleCrypto" /> <plugin name="Notification" value="CDVNotification" /> <preference name="orientation" value="portrait" /> <preference name="SplashScreen" value="screen" /> <preference name="SplashScreenDelay" value="3000" /> <platform name="android"> <preference name="AndroidPersistentFileLocation" value="Internal" /> </platform> <platform name="ios"> <preference name="iosPersistentFileLocation" value="Library" /> </platform> </widget>
Listing 43. config.xml
8.3 Create task to build project
Running "cordovacli:build" is not enough to build our app. Remember our working directory lives outside the Cordova project. Before firing the cordova build process we've to execute a chain of tasks. Most importantly we've to concatenate + minify the resources and copy all of them to the "www" folder. The below diagram depicts the chain of tasks we've to run to build our app.
The diagram is pretty self-explanatory! Some of the tasks you see are already in place like "jshint", "handlebars" and "cordovacli:build". For the pending ones let's download the necessary plugins and create them.
8.3.1 Create task to clean folders
Everytime we build our app we need to clean-up the "www" folder. The grunt-contrib-clean exist just for that. Let's install it first and then create the task.
$ npm install grunt-contrib-clean --save-dev
Listing 44. Installing grunt-contrib-clean plugin
Below is the clean task definition. I would suggest you to paste it after the "jshint" task.
// Empty the 'www' folder. clean: { options: { force: true }, dist: ['<%= config.dist %>/www'] }
Listing 45. "clean" task
Just to remind you from the previous part the value of the config.dist property is nothing but "cordova". Let's work on the next task to minify JS files.
8.3.2 Create task to minify requirejs modules
There are quite some plugins available to minify JS files. One of the popular is grunt-contrib-uglify. This plugin concatenates the JS files, minify the output and drop it in a particular destination. In our case this plugin alone is short for the job! Our JS files are requirejs modules and they are not just typical JavaScript files. Luckily requirejs provides an optimizer tool to minify requirejs modules and more good news is there is already a grunt plugin (grunt-requirejs) available that uses the optimizer in the grunt environment to optimize our JS files.
Let's download the grunt-requirejs and grunt-contrib-uglify plugins.
$ npm install grunt-requirejs --save-dev $ npm install grunt-contrib-uglify --save-dev
Listing 46. Installing grunt-requirejs and grunt-contrib-uglify plugins
Below is the "requirejs" grunt task definition.
// Optimize the javascript files using r.js tool. requirejs: { compile: { options: { baseUrl: '<%= config.src %>/js', mainConfigFile: '<%= config.src %>/js/main.js', almond: true, include: ['main'], out: '<%= config.dist %>/www/js/index.min.js', optimize: 'uglify' } } }
Listing 47. "requirejs" task
The out property represents the destination directory where the single minified JS file (index.min.js) will be dropped. We are going to drop the file under "js" sub-directory of "www" folder. I'm too lazy to explain about the other options. If you are keen to know and I believe you are please refer their github doc.
Before slipping to the next task we've to do one more thing. Since we are minifying all the requirejs modules and pre-loading them we don't need the complete requirejs library at runtime. There is an awesome minimal AMD implementation available called "almond" (from the same guy who create requirejs) and we could use that as an alternative to requirejs during runtime.
Currently if you notice our "bower.json" file we've the requirejs library as runtime dependency. Since we are going to use almond at runtime we can move requirejs to development dependency. Move the "requirejs" key from "dependencies" section to "devDependencies" and install almond.
{ "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", "backbone.validation": "~0.7.1", "backbone.localStorage": "~1.1.16", "backbone.stickit": "0.8.0", "backbone.touch": "~0.4.2" }, "devDependencies": { "requirejs": "~2.1.19" } }
Listing 48. bower.json
Let's install almond through bower.
$ bower install almond --save
Listing 49. Installing almond through bower
Cool! Let's move to the next task now.
8.3.3 Create task to minify CSS files
For concatenating and minifying CSS files all you need is the grunt-contrib-cssmin plugin.
$ npm install grunt-contrib-cssmin --save-dev
Listing 50. Installing grunt-contrib-cssmin plugin
After the download please add the below task definition to your grunt file.
// Optimize the CSS files. cssmin: { compile: { files: { '<%= config.dist %>/www/css/index.min.css': [ 'bower_components/ratchet/dist/css/ratchet.css', 'bower_components/ratchet/dist/css/ratchet-theme-<%= config.platform %>.css', '<%= config.src %>/css/safe.css', '<%= config.src %>/css/safe.<%= config.platform %>.css' ] } } }
Listing 51. "cssmin" task
The key of the files object represents the destination directory where the minified CSS file will be dropped and the value contains an array of files that has to be combined and minified.
8.3.4 Create task to update HTML files
If you see the CSS and JS references of both the "index.ios.html" and "index.android.html" files they points to the unminified versions. Before moving these files to the "www" folder we've to update their references to the minified versions. Initially I thought to create separate HTML files for deployment. Later seeing the power of grunt I googled to see if there is any plugin for rescue and yes there is one called grunt-processhtml. The grunt-processhtml plugin processes the html files at build time to modify them depending on the release environment through some special comments.
For example, let's take the below snippet, you've two script references. One to a library file (lib.js) and other to a local file (script.js) and at the time of build you would have probably combined them into a new file (app.min.js). To replace the two script references to one you've to wrap them in a special comment as shown below.
<!-- build:js app.min.js --> <script src="my/lib/path/lib.js"></script> <script src="my/deep/development/path/script.js"></script> <!-- /build -->
Listing 52. Sample html with special build comments
You can read more things than this. Please go through their NPM documentation quickly and then open the HTML files to make the below changes.
<!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" /> <!-- build:css css/index.min.css --> <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" /> <!-- /build --> <!-- build:js cordova.js --> <!-- /build --> <!-- build:js js/index.min.js --> <script src="../bower_components/requirejs/require.js" data-main="js/main"></script> <!-- /build --> <title>Safe</title> </head> <body> </body> </html>
Listing 53. Updated index.ios.html with processhtml build comments
<!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" /> <!-- build:css css/index.min.css --> <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" /> <!-- /build --> <!-- build:js cordova.js --> <!-- /build --> <!-- build:js js/index.min.js --> <script src="../bower_components/requirejs/require.js" data-main="js/main"></script> <!-- /build --> <title>Safe</title> </head> <body> </body> </html>
Listing 54. Updated index.android.html with processhtml build comments
We've are not done until we create the task. Let's download the plugin and create the task.
$ npm install grunt-processhtml --save-dev
Listing 55. Installing grunt-processhtml plugin
Here is the task and it's very simple!
// Change the script and css references to optimized ones. processhtml: { dist: { files: { '<%= config.dist %>/www/index.html': ['<%= config.src %>/index.<%= config.platform %>.html'] } } }
Listing 56. "processhtml" task
Alright, we've created all the tasks that are required for preparing the assets and now we left with the final one - copy the images and fonts to the cordova folder.
8.3.5 Create task to copy assets and config.xml
In this task we'll copy the dependent images, fonts and most importantly the "config.xml" file. For copying files and folders you've to download the plugin grunt-contrib-copy.
$ npm install grunt-contrib-copy --save-dev
Listing 57. Installing grunt-contrib-copy plugin
Create the below "copy" task in your grunt file.
// Copy the static resources like fonts, images to the platform specific folder. copy: { config: { expand: true, dot: true, src: 'config.xml', dest: 'cordova' }, fonts: { expand: true, dot: true, cwd: 'bower_components/ratchet/fonts', dest: '<%= config.dist %>/www/fonts', src: ['{,*/}*.*'] }, images: { expand: true, dot: true, cwd: '<%= config.src %>/images', dest: '<%= config.dist %>/www/images', src: ['{,*/}*.*'] } }
Listing 58. The "copy" task
I don't think I need to explain more here. Let the code talk!
8.3.6 Create the "build" task
We've completed the chain of tasks required to build our app. Now we need to create a composite task that chains all these tasks. We call the task as "build".
grunt.registerTask('build', [ 'jshint', 'clean', 'handlebars', 'requirejs', 'cssmin', 'processhtml', 'copy', 'cordova:build' ]);
Listing 59. The "build" task
For emulation and deployment also we need to execute all the tasks in the chain except the last one. Instead of duplicating the same in other places let's refactor and create another task with name "buildweb" with all those common tasks and call that in the "build" task.
grunt.registerTask('buildweb', [ 'jshint', 'clean', 'handlebars', 'requirejs', 'cssmin', 'processhtml', 'copy' ]); grunt.registerTask('build', [ 'buildweb', 'cordovacli:build' ]);
Listing 60. "buildweb" and "build" tasks
8.4 Create task to emulate
For emulation we've to run pretty much the same tasks except one thing. We've to replace the "cordova:build" with "cordova:emulate".
grunt.registerTask('emulate', [ 'buildweb', 'cordovacli:emulate' ]);
Listing 61. "emulate" task
8.5 Create task to deploy
This is just straight forward as the previous one.
grunt.registerTask('deploy', [ 'buildweb', 'cordovacli:deploy' ]);
Listing 62. "deploy" task
We've pretty much created most of the grunt tasks required during development and deployment. The main grunt tasks we created so far are: to setup a server and serve the files ("serve") during development, to build the app ("build"), to emulate ("emulate") and to deploy ("deploy"). In the last part, we'll create some additional grunt tasks for running unit tests but until that let's don't worry about grunting!
9. Deploy the app in iOS device
We've completed the development for this part. So far we've seen our app "Safe" only in browser. Now it's time to see how the app looks and behaves in a mobile. Before deploying into a device, it's good to run in an emulator to get a quick response. But emulators comes with limitations and they don't support all the hardware features. Since our app uses Camera, FileSystem and other hardware features how much emulators supports them is a question. To test the app thoroughly I would recommend to deploy it in a device. Anyway let's see how our app looks in an emulator.
9.1 Run the app in iOS emulator
I believe you've already configured your Mac machine for mobile development, if not please go through the Cordova platform guide for iOS and get the machine ready. Once everything is setup, open your terminal and run the below grunt command.
$ grunt emulate
Listing 63. Running Safe in iOS emulator
9.2 Deploy the app to iPhone device
Assuming your default platform is iOS, you've to run the below command to deploy to an iOS device.
$ grunt deploy
Listing 64. Deploying Safe to iPhone
In my Mac, for some unknown reason the app is still opened in emulator. I burnt quite some time to figure out the issue without success. I believe it's something to do with the grunt-cordovacli plugin. Finally, I ended up using XCode to deploy the app to my iPhone 3GS device. If you also facing the same issue then you can use XCode to deploy the app to device. First run the grunt build command to build the app for iOS, open the project "Safe.xcodeproj" located under "cordova/platforms/ios" folder in XCode, select the target as your device and click the "Play" button.
10. Deploy the app in Android device
10.1 Run the app in Android emulator
If you've configured your environment successfully for Android development, you can run the below command to run the app in android emulator.
$ grunt emulate --platform=android
Listing 65. Running Safe in Android emulator
10.2 Deploy the app to Android Moto E
To deploy to an Android device first you've to enable the developer settings in your mobile and then run the below command.
$ grunt deploy --platform=android
Listing 66. Deploy Safe to Android Moto E
The source code is attached to the bottom of the article. Once you downloaded the source code you've to run the npm install and bower install commands to install the node packages and bower components.
11. Summary
I hope this part has been an interesting one for you. We've done quite lot of things in this part. We've developed two important pages: List page and the Single photo view page. We created adapters to access Cordova APIs and also fake objects to make the app browser runnable. We spent quite some time on refactoring our code and hopefully that'll reduce lot of effort in future. We installed Cordova and had a quick primer on that and then we created the grunt tasks required for deployment. Finally, we deployed our app in iOS and Android devices!
12. What's Next?
In the upcoming part we are going to build the complicated page: the add/edit page. Once we complete that then we could able to save our secret photos safely and access it anywhere anytime. But note that without authentication our app is not complete and that's what we are gonna do in our last part. There are lot of exciting things to come and learn! Stay tuned!!