Getting Started With Ionic & NgCordova
In my most recent engagement, I’ve been working on a hybrid mobile app built using Ionic and ngCordova. Functionality-wise, the app itself is fairly straightforward, but since this is my first project that directly targets mobile devices (as opposed to responsive web), I’ve learned a few things that I think are worth sharing. Like most posts, the information contained has been cobbled together from many different sources during my time on this project. The purpose of this post is to walk through how to configure a new, fully testable project using Ionic and ngCordova. Like always, the code for this post is in Github at (https://github.com/jrodenbostel/getting-started-with-ionic-and-ngcordova).
Ionic (http://ionicframework.com) Hybrid mobile app development frameworks have been around for quite some time now. The Ionic Framework is one of the better entries I’ve seen to date. Based on current web technologies and frameworks (HTML5, CSS3, AngularJs (https://angularjs.org)), and leveraging a tried and true native container that runs on many devices (http://cordova.apache.org), Ionic provides a mostly-familiar starting point for folks new to mobile development. On top of that, Ionic is also packaged with a nice set of UI components and icons that help applications look nice as well as function smoothly.
ngCordova (http://ngcordova.com) The ngCordova project basically wraps the Cordova API to make it more Angular-friendly by giving the developer the ability to inject Cordova components as dependencies in your Angular controllers, services, etc. This project is still new and changing rapidly, but simplifies development greatly, and makes code that calls Cordova from within Ionic more readable and more easily testable.
Others (Yeoman, Grunt, Bower, Karma, Protractor, Mocha, Chai) These are the tools we’ll use to build our app. They are used for a variety of things, all are introduced from the same source – Yeoman (http://yeoman.io). Remember how revolutionary the scaffolding features of Rails were when they first surfaced? Yeoman provides scaffolding like that, and anyone can write a generator. There happens to be a generator for Ionic, and in my opinion, it’s all but necessary to use. Out of the box, you get a working app shell, a robust Grunt (http://gruntjs.com) script for app assembly, packaging, emulation, etc, dependency injection via Bower (http://bower.io), and example Mocha (http://mochajs.org) tests running via Karma (http://karma-runner.github.io/0.12/index.html). The only item we’ll add is support for end to end integration tests with Protractor (https://github.com/angular/protractor).
Prerequisites Before we start, you’ll need to have node.js (http://nodejs.org) and npm (https://www.npmjs.org) installed on your machine. Installation instructions can be found here (http://nodejs.org/download/) and here (http://blog.npmjs.org/post/85484771375/how-to-install-npm).
Step 1 – Scaffold Install Yeoman using the following command:
[code language=”bash”]
npm install -g yo
[/code]
Install the Ionic Generator for Yeoman using the following command:
[code language=”bash”]
npm install -g generator-ionic
[/code]
Create a folder for your project and use the newly install generator to build the shell of an Ionic app. Be sure you’re executing these commands in the root of your project folder. You can play around and answer the questions however you’d like. If you’re interested in following along, I’ve included the answers I’ve used and the relevant output below:
[code language=”bash”]
Justins-MacBook-Pro:getting-started-with-ionic-and-ngcordova justin$ yo ionic
_ _
(_) (_)
_ ___ _ __ _ ___
| |/ _ | ‘_ | |/ __|
| | (_) | | | | | (__
|_|___/|_| |_|_|___|
[?] Would you like to use Sass with Compass (requires Ruby)? Yes
Created a new Cordova project with name "GettingStartedWithIonicAndNgcordova" and id "com.example.GettingStartedWithIonicAndNgcordova"
[?] Which Cordova plugins would you like to include? org.apache.cordova.console, org.apache.cordova.device
[?] Which starter template [T] or example app [A] would you like to use? [T] Tabs
Install plugins registered at plugins.cordova.io: grunt plugin:add:org.apache.cordova.globalization
Or install plugins direct from source: grunt plugin:add:https://github.com/apache/cordova-plugin-console.git
Installing selected Cordova plugins, please wait.
Installing starter template. Please wait
info … Fetching http://github.com/diegonetto/ionic-starter-tabs/archive/master.tar.gz …
info This might take a few moments
[/code]
Step 2 – Run! Validate there weren’t any issues running the generator by starting the app. The Yeoman generator we’ve used includes a full-featured build script that includes a variety of ways to start up our app. We’ll use more features of the script later, but for a complete list of available commands visit the generator’s Github page (https://github.com/diegonetto/generator-ionic).For now, we’ll serve the app with the simple http server included as part of our sample app (courtesy of the Yeoman generator) using the following command (from the root of your project folder:
[code language=”bash”]
grunt serve
[/code]
This should have started the server and opened your default browser. In the browser, you should see something similar to the screenshot below:
Step 3 – ngCordova We’re off to a nice start – a fully functional app, running in the browser, with automated chai tests (using Karma (http://karma-runner.github.io/0.12/index.html) via grunt test) and some static code analysis (using jshint (http://jshint.com/) via grunt jshint) in place, all as a result of our Yeoman generator. If we explore the generated code, we notice that the app itself is very simple. As soon as we start writing code that depends on device APIs (checking for a network connection, identifying the current device, etc), we run into a problem: there’s only a global reference to Cordova, and there isn’t a nice way to inject Cordova into our Angular controllers, especially for testing. This is where ngCordova comes into play. Here, we’ll write some simple code that checks the device platform the app is currently running on, and display it on the opening screen. Let’s start by writing a test* that looks for an object in scope of the DashCtrl called ‘devicePlatform’. First, there are a few different ways to run the tests. One enables watching, but doesn’t run the tests immediately (you have to leave this on, and it runs tests as/when files in your project change), and the other just runs the tests on demand. With watching:
[source language=”bash”]
grunt test
[/source]
On demand:
[source language=”bash”]
grunt karma
[/source]
At the bottom of ‘/test/spec/controllers.js’, add a test for the DashCtrl with the following code:
[code language=”javascript”]
describe(‘Controller: DashCtrl’, function () {
var should = chai.should();
// load the controller’s module
beforeEach(module(‘GettingStartedWithIonicAndNgcordova’));
var DashCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
DashCtrl = $controller(‘DashCtrl’, {
$scope: scope
});
}));
it(‘should inspect the current devicePlatform’, function () {
scope.devicePlatform.should.equal(‘ios’);
});
});
[/code]
Immediately after adding that code to our test file (if you’re using ‘grunt test’) or running the tests on demand (using ‘grunt karma’), we should see results in our terminal window, and we should see that this test has failed because ‘devicePlatform’ is undefined in the DashCtrl’s scope.
[code language=”bash”]
PhantomJS 1.9.8 (Mac OS X) Controller: DashCtrl should inspect the current devicePlatform FAILED
TypeError: ‘undefined’ is not an object (evaluating ‘scope.devicePlatform.should’)
[/code]
Next, we’ll install ngCordova and implement the logic this test is exercising. Detailed instructions on installing ngCordova can be found here (http://ngcordova.com/docs/install/).The simplest install is via Bower using the following command:
[code language=”bash”]
bower install ngCordova
[/code]
Add a reference to the newly installed ngCordova to your app/index.html file, above the reference to cordova, such that:
[code language=”html”]
<script src="lib/ngCordova/dist/ng-cordova.js"></script>
<!– cordova script (this will be a 404 during development) –>
<script src="cordova.js"></script>
[/code]
To get the device OS, we’ll need to use Cordova’s Device plugin. If you haven’t already, we’ll need to make sure that it’s installed. Use the following command to install it:
[code language=”bash”]
cordova plugin add org.apache.cordova.device
[/code]
Next, we’ll add ngCordova to our project as a module. In app/scripts/app.js, change this line:
[code language=”bash”]
angular.module(‘GettingStartedWithIonicAndNgcordova’, [‘ionic’, ‘config’, ‘GettingStartedWithIonicAndNgcordova.controllers’, ‘GettingStartedWithIonicAndNgcordova.services’])
[/code]
to:
[code language=”bash”]
angular.module(‘GettingStartedWithIonicAndNgcordova’, [‘ionic’, ‘config’, ‘GettingStartedWithIonicAndNgcordova.controllers’, ‘GettingStartedWithIonicAndNgcordova.services’, ‘ngCordova’])
[/code]
Next, let’s write the code that adds the device platform to the DashCtrl scope. Start by injecting the device plugin into the DashCtrl using the code below:
[code language=”javascript”]
.controller(‘DashCtrl’, function($scope, $cordovaDevice) {
})
[/code]
Then create the devicePlatform scope variable and set it’s value to the device’s actual platform using the following code:
[code language=”javascript”]
.controller(‘DashCtrl’, function($scope, $cordovaDevice) {
$scope.devicePlatform = $cordovaDevice.getPlatform();
})
[/code]
Finally, add a reference to the device plugin to templates/tab-dash.html:
[code language=”html”]
<ion-view title="Dashboard">
<ion-content class="has-header padding">
<h1>Dash</h1>
<h2>{{devicePlatform}}</h2>
</ion-content>
</ion-view>
[/code]
You’ll notice that when we run our test again, they still fail. This is because karma tests run in the browser – the browser doesn’t interact with Cordova plugins – there’s no platform for the browser, there’s no ‘model’, there’s no device for Cordova to plug in to. We’ll need to add a few more things to get this working. At this point, if you’re interested in continuing on under the assumption that you’ll only be unit testing and never testing in the browser (which includes automated end to end testing) prior to testing on the device, you can simply mock any calls to Cordova using spies/doubles. I think there’s value in automated end to end testing and manual browser testing prior to testing on devices. I think it’s an easy and efficient way to troubleshoot your code in an environment isolated from platform dependencies. In that case, we’ll use ngCordovaMocks (and some grunt scripting) to make our unit tests pass in our development environment, we’ll add Protractor so we can test our app end-to-end prior to running on the device, and finally, we’ll run the app in the iOS emulator to complete our validation.
ngCordovaMocks You might notice in the ngCordova Bower package that there are an additional set of files named ‘ng-cordova-mocks’. These compliment ngCordova by providing empty implementations of the services that ngCordova wraps, which can be injected in place of the standard ngCordova implementations for testing purposes. First, we’ll need to add references in two places – our application config, and our test config. For the application configuration, update our app’s module definition in /scripts/app.js:
[code language=”javascript”]
angular.module(‘GettingStartedWithIonicAndNgcordova’, [‘ionic’, ‘config’, ‘GettingStartedWithIonicAndNgcordova.controllers’, ‘GettingStartedWithIonicAndNgcordova.services’, ‘ngCordovaMocks’])
[/code]
The test config can be found in our Grunt script. In /Gruntfile.js, find the karma task. In the karma task, you should see a configuration option named ‘files’. Add a line to update it to the following:
[code language=”javascript”]
files: [
‘<%= yeoman.app %>/lib/angular/angular.js’,
‘<%= yeoman.app %>/lib/angular-animate/angular-animate.js’,
‘<%= yeoman.app %>/lib/angular-sanitize/angular-sanitize.js’,
‘<%= yeoman.app %>/lib/angular-ui-router/release/angular-ui-router.js’,
‘<%= yeoman.app %>/lib/ionic/release/js/ionic.js’,
‘<%= yeoman.app %>/lib/ionic/release/js/ionic-angular.js’,
‘<%= yeoman.app %>/lib/angular-mocks/angular-mocks.js’,
‘<%= yeoman.app %>/lib/ngCordova/dist/ng-cordova-mocks.js’,
‘<%= yeoman.app %>/<%= yeoman.scripts %>/**/*.js’,
‘test/mock/**/*.js’,
‘test/spec/**/*.js’
],
[/code]
Now, we’ll update our test to use the new ngCordovaMocks library. We’ll add a reference to the ngCordovaMocks module, we’ll inject a decorated version of our $cordovaDevice plugin into our DashCtrl, and we’ll update our test condition accordingly.
[code language=”javascript”]
describe(‘Controller: DashCtrl’, function () {
var should = chai.should(), $cordovaDevice = null, $httpBackend, DashCtrl, scope;
beforeEach(module(‘GettingStartedWithIonicAndNgcordova’));
beforeEach(module(‘ngCordovaMocks’));
beforeEach(inject(function (_$cordovaDevice_) {
$cordovaDevice = _$cordovaDevice_;
}));
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope, _$httpBackend_) {
$httpBackend = _$httpBackend_;
$httpBackend.when(‘GET’, /templatesS/).respond("");
$cordovaDevice.platform = ‘TEST VALUE’;
scope = $rootScope.$new();
DashCtrl = $controller(‘DashCtrl’, {
$scope: scope
});
$httpBackend.flush();
}));
it(‘should inspect the current deviceType’, function () {
scope.devicePlatform.should.equal(‘TEST VALUE’);
});
});
[/code]
You can see now we’re decorating $cordovaDevice, supplying it with a value for it’s platform property, and we’re asserting that the $cordovaDevice.getPlatform() method is returning the correct value via our $scope.devicePlatform variable. We’ve also added a mock $httpBackend (and subsequent flush) that will listen to and ignore any page requests triggered by our controller initializing. In this way, we can simulate a specific platform and exercise our code in unit tests, AND our app still runs in the browser. At this point, running in the browser without ngCordovaMocks would cause failures. To really see the value of ngCordovaMocks, we’ll add support for Protractor tests.
Protractor (https://github.com/angular/protractor) First, we’ll need to install two node modules that give us new grunt tasks: one to control a Selenium Webdriver (http://www.seleniumhq.org), and one to run our protractor tests.
[code language=”bash”]
npm install grunt-protractor-webdriver –save-dev
npm install grunt-protractor-runner –save-dev
[/code]
While grunt-protractor-runner installs a controller for Selenium Webdriver, we still need a Selenium server. We can install a standalone Selenium server by running the following the root of our project:
[code language=”bash”]
node_modules/protractor/bin/webdriver-manager update
[/code]
Next, we’ll update our grunt script to include configurations for the new tasks, and add a new task of our own. Include these new tasks somewhere in your grunt.initConfig object:
[code language=”javascript”]
protractor_webdriver: {
all: {
command: ‘webdriver-manager start’
}
},
protractor: {
options: {
keepAlive: true, // If false, the grunt process stops when the test fails.
noColor: false // If true, protractor will not use colors in its output.
},
all: {
options: {
configFile: ‘test/protractor-conf.js’
}
}
},
[/code]
Then register our custom task somewhere after grunt.initConfig:
[code language=”javascript”]
grunt.registerTask(‘test_e2e’, [
‘protractor_webdriver’,
‘protractor’
]);
[/code]
We’re not doing anything special in this config – we’re basically using a grunt task to control the Selenium server we could otherwise control from the CLI, and we’re offloading much of our protractor config to a properties file. Next, create the properties file at the path listed above (test/protractor-conf.js):
[code language=”javascript”]
exports.config = {
seleniumAddress: ‘http://localhost:4444/wd/hub’,
specs: [
‘e2e/**/*.js’
],
framework: ‘mocha’,
capabilities: {
‘browserName’: ‘chrome’,
‘chromeOptions’: {
args: [‘–args’,’–disable-web-security’]
}
},
/**
* This should point to your running app instance, for relative path resolution in tests.
*/
baseUrl: ‘http://localhost:8100’,
};
[/code]
Last, we’ll write a new end to end test case and execute it. Create a file at /test/e2e (which is the directory we included in our protractor configuration above). I named mine ‘tabs.js’. Add the content below to the file:
[code language=”javascript”]
var chai = require(‘chai’);
var chaiAsPromised = require(‘chai-as-promised’);
chai.use(chaiAsPromised);
var expect = chai.expect;
describe(‘Ionic Dash Tab’, function() {
var decoratedModule = function() {
var ngCordovaMocks = angular.module(‘ngCordovaMocks’);
var injector = angular.injector([‘ngCordovaMocks’, ‘ng’]);
ngCordovaMocks.service(‘$cordovaDevice’, function() {
var cordovaDevice = injector.get(‘$cordovaDevice’);
cordovaDevice.platform = ‘ios’;
return cordovaDevice;
});
};
it(‘should have the correct heading’, function() {
browser.addMockModule(‘ngCordovaMocks’, decoratedModule);
browser.get(‘http://localhost:8100’);
var heading = element(by.css(‘h2’));
expect(heading.getText()).to.eventually.equal(‘ios’);
});
});
[/code]
In the code above, we’re decorating the $cordovaDevice service (much like we were in the unit tests), by first getting a reference to the ngCordovaMocks module, then getting a handle on the injector instance from the ngCordovaMocks module, then getting the $cordovaDevice service itself, and finally decorating the service by setting the platform to our desired value. In the test itself, we’re adding our newly decorated ngCordovaMocks module to Protractor’s browser instance. At this point, running the tests should yield positive results. You can run them using the custom task we registered (assuming your server is already running), by using the following command (be sure your server is running with ‘grunt serve’:
[code language=”bash”]
grunt test_e2e
[/code]
Dynamic Configuration Since we’ve updated our app to only use ngCordovaMocks instead of ngCordova, we need the ability to switch between using the two seamlessly. Inspiration from this portion of the post comes from this post (http://www.ecofic.com/about/blog/getting-started-with-ng-cordova-mocks). To do this, we’ll use the grunt-text-replace grunt task. Install the grunt-text-replace node package using the following statement:
[code language=”bash”]
npm install grunt-text-replace –save-dev
[/code]
Next, add the following config to Gruntfile.js somewhere in your grunt.initConfig object:
[code language=”javascript”]
replace: {
production: {
src: [
‘<%= yeoman.app %>/index.html’,
‘<%= yeoman.app %>/<%= yeoman.scripts %>/app.js’
],
overwrite: true,
replacements:[
{ from: ‘lib/ngCordova/dist/ng-cordova-mocks.js’, to: ‘lib/ngCordova/dist/ng-cordova.js’ },
{ from: ”ngCordovaMocks”, to: ”ngCordova” }
]
},
development: {
src: [
‘<%= yeoman.app %>/index.html’,
‘<%= yeoman.app %>/<%= yeoman.scripts %>/app.js’
],
overwrite: true,
replacements:[
{ from: ‘lib/ngCordova/dist/ng-cordova.js’, to: ‘lib/ngCordova/dist/ng-cordova-mocks.js’ },
{ from: ”ngCordova”, to: ”ngCordovaMocks” }
]
}
},
[/code]
Now we’ll add calls to these tasks to our existing Grunt tasks, as well as create a new init-development task, as seen below:
[code language=”javascript”]
grunt.registerTask(‘test’, [
‘replace:development’,
‘clean’,
‘concurrent:test’,
‘autoprefixer’,
‘karma’,
‘karma:unit:start’,
‘watch:karma’
]);
grunt.registerTask(‘serve’, function (target) {
if (target === ‘compress’) {
return grunt.task.run([‘compress’, ‘ionic:serve’]);
}
grunt.config(‘concurrent.ionic.tasks’, [‘ionic:serve’, ‘watch’]);
grunt.task.run([‘init-development’, ‘concurrent:ionic’]);
});
grunt.registerTask(‘init’, [
‘replace:production’,
‘clean’,
‘wiredep’,
‘concurrent:server’,
‘autoprefixer’,
‘newer:copy:app’,
‘newer:copy:tmp’
]);
grunt.registerTask(‘init-development’, [
‘replace:development’,
‘clean’,
‘wiredep’,
‘concurrent:server’,
‘autoprefixer’,
‘newer:copy:app’,
‘newer:copy:tmp’
]);
grunt.registerTask(‘test_e2e’, [
‘replace:development’,
‘protractor_webdriver’,
‘protractor’
]);
[/code]
When we run ‘grunt serve’, the application will start in the web server, and will be running with ngCordovaMocks. From here, we can run our automated end to end tests using ‘grunt test_e2e’. We can also simply run the unit tests standalone using ‘grunt test’. You can see the way the tasks above changed to make that possible – calls to ‘replace:development’ prior to the tasks executing. In the case of the ‘test’ task, it was altered slightly to also include the ‘karma’ task to run the tests through after initial invocation, then followed by a test watcher. At this point, we can also run in our emulator without issue. To do that, we’ll quickly add the iOS platform to our project, and kick off the emulator to see the ‘real’ platform displayed on the screen.
[code language=”bash”]
grunt platform:add:ios
[/code]
…followed by:
[code language=”bash”]
grunt emulate:ios
[/code]
At this point, we’ll start to see a slight divergence from the way the app is functioning on the web versus in our emulator. In some cases, the device is available before all of the Cordova plugins are loaded. Furthermore, the way the screen refreshes as a result of this is also slightly different. To counter this, we’ll have to add a bit of logic to our controller to wait for the device to be ready. Update the DashCtrl with the following code:
[code language=”javascript”]
.controller(‘DashCtrl’, function($scope, $cordovaDevice, $ionicPlatform) {
$ionicPlatform.ready(function() {
$scope.devicePlatform = $cordovaDevice.getPlatform();
});
})
[/code]
Run the emulator again, and we should see the app functioning properly:
That’s it! We should now be able to run in the browser, in the emulator, and through our automated tests with consistency. This setup has paid efficiency dividends for me on my current project, and I hope it helps folks get started on the right foot. It was a lot longer than I thought it would be. *As previously stated, I ran the Yeoman generator with the ‘Tabs’ example project option. Turns out it came with a broken test. I added a question to an open issue on this at the ngCordova project’s Github (https://github.com/driftyco/ng-cordova) page. You can find the fixed test below:
[code language=”javascript”]
‘use strict’;
describe(‘Controller: FriendsCtrl’, function () {
var should = chai.should();
// load the controller’s module
beforeEach(module(‘GettingStartedWithIonicAndNgcordova’));
var FriendsCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
FriendsCtrl = $controller(‘FriendsCtrl’, {
$scope: scope
});
}));
it(‘should attach a list of pets to the scope’, function () {
scope.friends.should.have.length(4);
});
});
[/code]