Yeoman

A new project with PhoneGap and AngularJS

This is part of a series posts relating to PhoneGap. PhoneGap is an open source framework for quickly building cross-platform mobile applications using HTML5 and Javascript, allowing you to make use of the many open licensed Javascript libraries available on the internet. You also get the benefits of the native features on the targeted device and the simplicity and quick development cycle of a web page. You can write the application once and deploy it to multiple targets with ease.

What is AngularJS?

AngularJS is an open source javascript framework maintained by Google. It allows you to develop static web application using a MVC software architectural pattern. It’s a perfect companion framework for building powerful PhoneGap applications quickly while maintaining organized and reusable code.

AngularJS is perfect for a PhoneGap application where the device might not always be online. In today’s article I will be demonstrating how to setup and build a new AngularJS / PhoneGap project.

Creating a new AngularJS / PhoneGap Project

We will be using Yeoman for generating our project. Yeoman provides a generator ecosystem – a plugin that when run with the `yo` command will scaffold complete projects to create a robust workflow. Read more about Yeoman.

First make sure you have the required software installed: PhoneGap, npm, Ruby, and Compass.

Install the required npm modules.

npm –g install grunt grunt-cli bower yo generator-karma generator-angular 

Use PhoneGap to create a new project and then create a new AngularJS project using Yeoman inside the same folder.

phonegap create new-project
cd new-project
yo angular "New Project"

PhoneGap expects your application to live in the www folder while AngularJS expects the application to live in the app folder. It is easier to change grunt’s application folder than it is to change PhoneGap. In Gruntfile.js, add the following variable to your appConfig declaration.

  // Configurable paths for the application
  var appConfig = {
    app: require('./bower.json').appPath || 'app',
    dist: 'dist',
    phonegap: 'www'
  };

Add a new clean task for PhoneGap:

    // Empties folders to start fresh
    clean: {
      dist: {
        files: [{
          dot: true,
          src: [
            '.tmp',
            '<%= yeoman.dist %>/{,*/}*',
            '!<%= yeoman.dist %>/.git{,*/}*'
          ]
        }]
      },
      phonegap: ['<%= yeoman.phonegap %>/*', '!<%= yeoman.phonegap %>/config.xml', '!<%= yeoman.phonegap %>/res'],
      server: '.tmp'
    },

This will clean all of the files in the PhoneGap folder but will leave the res folder with the project’s icons and splash screen images. Now modify the copy task:

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
            '.htaccess',
            '*.html',
            'views/{,*/}*.html',
            'images/{,*/}*.{webp}',
            'styles/fonts/{,*/}*.*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/images',
          dest: '<%= yeoman.dist %>/images',
          src: ['generated/*']
        }, {
          expand: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= yeoman.dist %>'
        }]
      },
      phonegap: {
        expand: true,
        cwd: '<%= yeoman.dist %>',
        dest: '<%= yeoman.phonegap %>',
        src: '**'
      },
      styles: {
        expand: true,
        cwd: '<%= yeoman.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
      }
    },

Modify the build task. We will be configuring this task in the style of ‘grunt build:target’ so that you can just run ‘grunt build:phonegap’ to build for PhoneGap.

grunt.registerTask('build', 'build task', function(target) {
      target = target || 'dev';
      if (target === 'phonegap') {
          
          grunt.task.run([
            'clean:phonegap',
            'clean:dist',
            'wiredep',
            'useminPrepare',
            'concurrent:dist',
            'autoprefixer',
            'concat',
            'ngAnnotate',
            'copy:dist',
            'preprocess:phonegap',
            'cdnify',
            'cssmin',
            'uglify',
            'filerev',
            'usemin',
            'htmlmin',
            'copy:phonegap'
          ]);
      } else {
          grunt.task.run([
            'clean:dist',
            'wiredep',
            'useminPrepare',
            'concurrent:dist',
            'autoprefixer',
            'concat',
            'ngAnnotate',
            'copy:dist',
            'preprocess:dist,
            'cdnify',
            'cssmin',
            'uglify',
            'filerev',
            'usemin',
            'htmlmin'
          ]);
      }
  });

Now we can build a web version (grunt build) or a PhoneGap version (grunt build:phonegap) from the command line.

In order for your application to make use of the device’s mobile features we will need to include PhoneGap’s cordova.js file and not bootstrap AngularJS until after the device is ready.

We will use grunt-preprocess to provide different a conditional index.html depending on if your project is built for web or for PhoneGap.

Add the following to Gruntfile.js.

    preprocess: {
        options: {
            context: {
            }
        },
        dist: {
            src: '<%= yeoman.app %>/index.html',
            dest: '<%= yeoman.dist %>/index.html'
        },
        phonegap: {
            options: {
                context: {
                    phonegap: true
                }
            },
            src: '<%= yeoman.app %>/index.html',
            dest: '<%= yeoman.dist %>/index.html'
        }
    },

In index.html, we will need to remove the ng-app directive so that we can control when AngularJS will bootstrap. In the <head> section we will load cordova.js for PhoneGap. The ng-app directly have been removed from the <body> tag and javascript has been added above the </body> closing tag that will wait for PhoneGap to report the device as ready before bootstrapping.

Make the following changes to index.html.

<!doctype html>
<html class="no-js">
  <head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    
    <!-- @ifdef phonegap -->
    <script type="text/javascript" src="cordova.js"></script>
    <meta name="format-detection" content="telephone=no" />
    <meta name="msapplication-tap-highlight" content="no" />
    <!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
    <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" />
    <!-- @endif -->
    <!-- @ifndef phonegap -->
    <meta name="viewport" content="width=device-width">
    <!-- @endif -->
    
    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
    <!-- build:css(.) styles/vendor.css -->
    <!-- bower:css -->
    <!-- endbower -->
    <!-- endbuild -->
    <!-- build:css(.tmp) styles/main.css -->
    <link rel="stylesheet" href="styles/main.css">
    <!-- endbuild -->
  </head>
  <body>
    <!--[if lt IE 7]>
      <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
    <![endif]-->

    <!-- Add your site or application content here -->
    <div class="header">
      <div class="navbar navbar-default" role="navigation">
        <div class="container">
          <div class="navbar-header">

            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>

            <a class="navbar-brand" href="#/">newProject</a>
          </div>

          <div class="collapse navbar-collapse" id="js-navbar-collapse">

            <ul class="nav navbar-nav">
              <li class="active"><a href="#/">Home</a></li>
              <li><a ng-href="#/about">About</a></li>
              <li><a ng-href="#/">Contact</a></li>
            </ul>
          </div>
        </div>
      </div>
    </div>

    <div class="container">
    <div ng-view=""></div>
    </div>

    <div class="footer">
      <div class="container">
        <p><span class="glyphicon glyphicon-heart"></span> from the Yeoman team</p>
      </div>
    </div>


    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID -->
     <script>
       !function(A,n,g,u,l,a,r){A.GoogleAnalyticsObject=l,A[l]=A[l]||function(){
       (A[l].q=A[l].q||[]).push(arguments)},A[l].l=+new Date,a=n.createElement(g),
       r=n.getElementsByTagName(g)[0],a.src=u,r.parentNode.insertBefore(a,r)
       }(window,document,'script','//www.google-analytics.com/analytics.js','ga');

       ga('create', 'UA-XXXXX-X');
       ga('send', 'pageview');
    </script>

    <!-- build:js(.) scripts/vendor.js -->
    <!-- bower:js -->
    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/bootstrap-sass-official/assets/javascripts/bootstrap.js"></script>
    <script src="bower_components/angular-animate/angular-animate.js"></script>
    <script src="bower_components/angular-cookies/angular-cookies.js"></script>
    <script src="bower_components/angular-resource/angular-resource.js"></script>
    <script src="bower_components/angular-route/angular-route.js"></script>
    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
    <script src="bower_components/angular-touch/angular-touch.js"></script>
    <!-- endbower -->
    <!-- endbuild -->

        <!-- build:js({.tmp,app}) scripts/scripts.js -->
        <script src="scripts/app.js"></script>
        <script src="scripts/controllers/main.js"></script>
        <script src="scripts/controllers/about.js"></script>
        <!-- endbuild -->
        
    <script>


    window.isPhoneGap = function() {
            return (document.URL.indexOf( 'http://' ) === -1 && document.URL.indexOf( 'https://' ) === -1) || document.location.port == 3000;
        };

        // This is a function that bootstraps AngularJS, which is called from later code
        function bootstrapAngular() {
            console.log("Bootstrapping AngularJS");
            // This assumes your app is named "app" and is on the body tag: <body ng-app="app">
            // Change the selector from "body" to whatever you need
            var domElement = document.querySelector('html');
            // Change the application name from "app" if needed
            angular.bootstrap(domElement, ['newProjectApp']);
        }

        // This method of user agent detection also works, though it means you might have to maintain this UA list
        if (window.isPhoneGap()) {
            console.log("UA: Running in Cordova/PhoneGap");
            document.addEventListener('deviceready', bootstrapAngular, false);
        } else {
            console.log("UA: Running in browser");
            jQuery(function($){
                bootstrapAngular();
            });

        }
    </script>
        
</body>
</html>

Using this workflow you can create and test your AngularJS application using your browser by running:

grunt serve

You can test your AngularJS/PhoneGap app by running:

grunt build:phonegap
phonegap build android
If everything worked as expected your device should show the Yeoman scaffold boilerplate.

PhoneGap AngularJS scaffold

Hutz Media Logo

Bryan Wiebe is a web developer and mobile developer located in the Okanagan, British Columbia. He works for Hutz Media Ltd. This post is an entry in a blog series covering development with PhoneGap.