By Nick Stuart
Let’s walk through the process of getting a Yeoman based AngularJS application up and running inside Cordova/PhoneGap environment on an Android device. While we will cover Android only, the same general steps should apply for an iOS device as well, and the Yeoman project should be able to be shared between the different Cordova projects.
The goal of this will be to have a functional Yeoman, Angular, Cordova application that can be built and deployed with the Android build process and still have all the JavaScript tools available during development time to aid in the process (Grunt, Yo, Karma, etc). This will let us build client side HTML5/JavaScript apps with traditional tools and easily deploy those applications to any Cordova compatible device.
Prerequisites
- Android SDK installed and configured (http://developer.android.com/sdk/index.html)
- Ability to build/deploy a basic Android application
- NodeJS Installed (http://nodejs.org/)
- Ruby with Compass gem installed (http://www.ruby-lang.org http://compass-style.org/)
Yo! Setup
The first step is to set up our initial project structure and Yeoman project base to use as our HTML asset directory.
For full details of Yeoman and it’s setup go to their site at http://yeoman.io/Choose a directory to make your project in and make a few empty directories to follow along with:
Now we can finally get our project going, run the following and accept all the defaults when it asks you a bunch of questions:
The above will generate the default Yeoman Angular directory structure and base set of files, as well running Bower to install all the needed JavaScript components. To run a quick sanity check you should be able to run:
and have the tests run and execute (assuming you have Chrome installed) and the ‘dist’ directory should appear as well with the bootstrapped application. So far this has all been pretty basic Yeoman setup work, now we can get this running on our Android device and built along with our standard Android build process. Currently this is setup through the traditional Ant approach, but configuring the newer Gradle build would be even simpler.
Cordova/PhoneGap install
Download and unzip the Cordova src file, then go into that directory and unzip the cordova-android.zip file. You should end up with a ‘bin’ directory with a ‘create’ command in it. You will need to make sure this folder is in your shell environments PATH variable before continuing. Back in our top level ‘ang-droid’ folder, run the following:
create ang-droid
Once finished, we should have the following directory layout for everything:
ang-droid/ ang-droid/ //home of our Cordova android app ang-droid-frontend //home for all our html assets
Cordova Integration
We need to make some modifications in order to get our Yeoman items into the build process of the Android app. The easiest way to do this is to add some quick properties and custom build rules to the Android app. First, copy the cordova*.js file from the assets folder as we will be needing this file, but removing all the other default ones.
After you copy this file over, go into ‘ang-droid-frontend/app/’ and edit the index.html. You want this file to be loaded up before the angular scripts, so you should have something like the following:
You will also need to edit your jshint properties to ignore this file, as it complains about a lot of things in there. Open up Gruntfile.js, find the jshin: { all: [] } section and edit it to look like the following:
all: [ 'Gruntfile.js', '<%= yeoman.app %>/scripts/{,*/}*.js', '!<%= yeoman.app %>/scripts/cordova-2.7.0.js' ]
Now we can remove the existing Cordova template files created during the initial project setup.
rm -rf ang-droid/assets/www
You should also ignore this directory in your VCS of choice, as it should NOT be checked in. It should be treated like any other binary/built source. In ang-droid/ang-droid, edit the file ant.properties’ and add the following two properties:
html.source.dir=../ang-droid-frontend html.asset.dir=assets/www
Also create ang-droid/custom_rules.xml and add the following:
<?xml version="1.0" encoding="UTF-8"?> <project name="Angular Droid Example" default="help"> <target name="-pre-compile" depends="-html-clean,-html-build,-html-dev,-html-prod"> </target> <target name="-html-clean"> <delete dir="${html.asset.dir}" failonerror="false" /> <mkdir dir="${html.asset.dir}"/> </target> <target name="-html-build" unless="html.skip"> <exec dir="${html.source.dir}" command="grunt" failifexecutionfails="true" failonerror="true"/> </target> <!-- Copies the unminimized version of the css and js files for easier development and debugging. --> <target name="-html-dev" unless="html.production"> <copy todir="${html.asset.dir}"> <fileset dir="${html.source.dir}/app" includes="**/*" /> </copy> <copy todir="${html.asset.dir}/styles/"> <fileset dir="${html.source.dir}/.tmp/" includes="**/*.css" /> </copy> </target> <!-- copy over the minified files that should be production ready --> <target name="-html-prod" if="html.production"> <copy todir="${html.asset.dir}"> <fileset dir="${html.source.dir}/dist" includes="**/*" /> </copy> </target> </project>
This should be all the setup that is required for the android side of things. When you run ‘ant debug’ or anything else to build the Android app it will remove the www directory, and copy over the assets needed from the Yeoman project. Having the two tasks -dev and -prod will let you use the appropriate set of resources for whether you are building for production or development purposes. Now, when you run your app you should get the welcome screen for the default yo-angular application. Not very exciting, but it works. Let’s add a little bit of Cordova integration in there to make sure we are able to hook into it’s API. Go into the ang-droid-frontend directory and run the following:
'use strict'; angular.module('angDroidFrontendApp’).factory('CordovaReady', [ '$q', //(1) function($q) { return function(scope) { //(2) var deferred = $q.defer(); document.addEventListener('deviceready', function() { //(3) if(scope){ scope.$apply(function(){ //(4) deferred.resolve(); //(5) }); }else{ deferred.resolve(); //(5) } }, false); return deferred.promise; }; }]);
This will generated the needed files for a new Angular service and also add it to your index.html file. Open up the generated file (‘app/scripts/services/CordovaReady.js’) and copy over the contents with the following, details explained below.
'use strict'; angular.module('angDroidFrontendApp’).factory('CordovaReady', [ '$q', //(1) function($q) { return function(scope) { //(2) var deferred = $q.defer(); document.addEventListener('deviceready', function() { //(3) if(scope){ scope.$apply(function(){ //(4) deferred.resolve(); //(5) }); }else{ deferred.resolve(); //(5) } }, false); return deferred.promise; }; }]);
Let’s walk through this, a few interesting things going on:
- We are using Angular’s injection to get a reference to the built in $q service. This service is basically a factory to help build up promises that can be used in asynchronous executions.
- Our service is going to be a factory method itself, and will take an option scope variable to apply the execution of the fulfilled promise to available.
- Cordova will fire a ‘device ready’ event as soon as it has bootstrapped itself and is ready to use the native device actions and API’s. We need this anytime we want to interact with native services so that we know Cordova is ready to accept requests from our Angular application. If you try calling things like navigation api, or other built in services before this event is fired bad things will ensue.
- Since we are binding to a document event, and not something executing from within Angular’s world we should use scope.$apply() to make sure Angular does its magic when resolving the promise.
- Both of these are fulfilling the promise. We have no deferred.reject() call as we either get the event or not, so no error handling is really available here.
To use the service you would do something like the following:
//inject 'CordovaReady' CordovaReady(scope).then(function(){ console.log(‘Cordova services available!’);
Conclusion
So, what does this all give us? It gives us the ability to keep our application code base (html/javascript/etc) out of the Cordova specific build environment so that we can easily move from one platform to the other with minimal effort. This will also allow you to build your applications locally, but still easily take advantage of the PhoneGap build process if you want.
Really there is not anything ground breaking here besides some nice automation and separation of concerns. I hope you all find this at least a little bit helpful going forward!
Update
This post has since been updated by Nick to reflect how the process has changed with the latest releases of Cordova and Yeoman.
This is an update to my previous article which talked about integrating AngularJS with Cordova. We’ll go over how the process has changed with newer version of Cordova(3.4) and Yeoman(1.1.2). You do not really have to read the old article to make use of this one. A lot of the work we did previously was to ‘massage’ the Yo/Angular application structure to fit in a Cordova based application. This new approach is vastly simpler and easier to maintain.
Dependencies and Other Boring Stuff
Again, I will assume you have working knowledge of getting a simple Cordova application up and running on either Android or iOS. I will be using Android during the example, but there is no platform specific code involved, so substituting ‘android’ with ‘ios’ during the Cordova steps should work just fine.
Below is the list of versions for the installed artificats:
- npm 1.3.15
- cordova 3.4
- yo 1.1.2
- generator-angular 0.7.1
- grunt-cli 0.1.11
The general idea of the following setup is that we have our Angular application, and our Cordova application, as two separate projects. The Cordova application will link to the Angular application at build time to pull in all the resources. The source structures need not step on each other, and can live in their own directories. There is a very thin wrapper around the Angular bootstrap process that will let us use Cordova plugins and features worry free from inside the Angular world.
To Action!
First we need to setup our Angular application through yo, just like we normally would:
mkdir c3a cd c3a; yo angular; #accept all the defaults #in order for grunt to pass I had to run the following first: npm install karma-jasmine --save-dev; npm install karma-chrome-launcher --save-dev; #now you should be able to run grunt;
Now it’s time to setup our cordova app:
cordova create c3c --link-to=c3a/app #KEY ARGUMENT --link-to cd c3c cordova platform add android
Thats it (kind of)! You now should be able to run your simple Cordova+Angular app on a device of your choosing.
Of course we are not really ‘integrated’ with Cordova as all we have right now is a fancy HTML application run in an embedded browser on a device, not terribly useful! Lets get talking to the device, shall we?
Bootstrapping Anuglar
This all takes your code in the Angular application, the Cordova side of things does not need to change at all. First, I like to have an empty placeholder ‘cordova.js’ at the root level. This file will get replaced by the cordova build process, but if you run your app in a browser for testing you at least won’t get false 404 errors for it.
Next, we need a hook into cordova. We’ll replace Angular’s normal boot process by not using the ng-app directive, instead we’ll manually bootstrap it.
'use strict'; var CordovaInit = function() { var onDeviceReady = function() { receivedEvent('deviceready'); }; var receivedEvent = function(event) { console.log('Start event received, bootstrapping application setup.'); angular.bootstrap($('body'), ['c3aApp']); }; this.bindEvents = function() { document.addEventListener('deviceready', onDeviceReady, false); }; //If cordova is present, wait for it to initialize, otherwise just try to //bootstrap the application. if (window.cordova !== undefined) { console.log('Cordova found, wating for device.'); this.bindEvents(); } else { console.log('Cordova not found, booting application'); receivedEvent('manual') } }; $(function() { console.log('Bootstrapping!'); new CordovaInit(); });
There really is not much to go through here. We initialize a listener for the ‘deviceready’ event if Cordova is available, and as soon as we get that we bootstrap our Angular application. The advantage to that is that from within or Angular code we know that any device specific items will be available without us having to check or wait on any other Cordova related events.
Job done!
A few small items are still left un-done, but should be considered. The biggest is that when development is done the ‘dist’ directory of your Angular application should be used. With that though, you will want check that you don’t ‘cdn-ize’ the links to your third party libraries if you want the application to run offline.
A full working example can be found at github: https://github.com/nick-pww/cordova-3-angular