AngularJS, Browserify and Grunt

August 29th 2013

UPDATE: There is an official angular package now on npm. Also @bclinkinbeard has been working and writing some great stuff combining Angular with Browserify, go check out his site as well: http://benclinkinbeard.com.

Let's setup a new project that uses AngularJS, Browserify and Grunt.

This tutorial assumes you have Node.js installed and are familiar with it and Grunt.
If not please read using npm on the client side.

Create a Project

Create a folder to contain the project and init your package.json file within it:

mkdir mysite && cd mysite
npm init

Start by installing the development dependencies you'll need to run Grunt and Browserify through Grunt:

npm i grunt grunt-browserify grunt-contrib-copy --save-dev

At the time of this writing, AngularJS does not have an official package on npm and that is too bad. But! That isn't going to stop us. You can easily install arbitrary packages not setup for npm using napa:

npm i napa --save-dev

Then in your package.json add the following install script:

{
  "scripts": {
    "install": "napa"
  },
  "napa": {
    "angular": "angular/bower-angular",
    "angular-route": "angular/bower-angular-route"
  }
}

Now any time you run npm install it will also install AngularJS into the node_modules/angular/ folder and Angular Route into the node_modules/angular-route/ from their official releases.

Folder Structure

Let's setup a folder structure based upon the angular-seed example. Create the files and folders to make your app look like the following (don't worry about what's in them right now):

|- app/
|--- css/
|----- app.css
|--- js/
|----- app.js
|--- partials/
|----- partial1.html
|----- partial2.html
|--- index.html
|- Gruntfile.js
|- package.json

Gruntfile

Next let's setup your Gruntfile.js to build your site upon typing the grunt command within the project folder:

module.exports = function(grunt) {
  grunt.initConfig({
    browserify: {
      js: {
        // A single entry point for our app
        src: 'app/js/app.js',
        // Compile to a single file to add a script tag for in your HTML
        dest: 'dist/js/app.js',
      },
    },
    copy: {
      all: {
        // This copies all the html and css into the dist/ folder
        expand: true,
        cwd: 'app/',
        src: ['**/*.html', '**/*.css'],
        dest: 'dist/',
      },
    },
  });

  // Load the npm installed tasks
  grunt.loadNpmTasks('grunt-browserify');
  grunt.loadNpmTasks('grunt-contrib-copy');

  // The default tasks to run when you type: grunt
  grunt.registerTask('default', ['browserify', 'copy']);
};

Building Your App

Now you're all ready to begin building your app. Let's start with the app/index.html:

HTML

<!doctype html>
<!-- Specify the AngularJS app to use -->
<html lang="en" ng-app="myApp">
<head>
  <meta charset="utf-8">
  <title>My AngularJS App</title>
  <!-- The css is copied into dist/css/app.css by the copy task -->
  <link rel="stylesheet" href="css/app.css"/>
</head>
<body>

  <ul class="menu">
    <li><a href="#/view1">view1</a></li>
    <li><a href="#/view2">view2</a></li>
  </ul>

  <!-- A simple AngularJS view -->
  <div ng-view></div>

  <!-- This script will be bundled by the browserify task into dist/js/app.js -->
  <!-- You should only ever really need this one script tag -->
  <script src="js/app.js"></script>
</body>
</html>

JavaScript

Getting Angular to play nice with Browserify is incredibly simple. In your app/js/app.js file add the following:

// This will include ./node_modules/angular/angular.js
// and give us access to the `angular` global object.
require('angular/angular');
require('angular-route/angular-route');

// Create your app
angular.module('myApp', ['ngRoute']).config(['$routeProvider', function($routeProvider) {
  // Specify routes to load our partials upon the given URLs
  $routeProvider.when('/view1', {templateUrl: 'partials/partial1.html'});
  $routeProvider.when('/view2', {templateUrl: 'partials/partial2.html'});
  $routeProvider.otherwise({redirectTo: '/view1'});
}]);

That is it! Now when you click on or navigate to #/view1 and #/view2 it will load the contents of your app/partials/partial1.html into your ng-view.

Continuing with AngularJS

Let's create a simple value service, filter and use it in one of your partials.

First create a app/js/services.js file with the following contents:

angular.module('myApp.services', []).
  value('version', '0.1');

Next create a app/js/filters.js file with the following contents:

angular.module('myApp.filters', []).
  filter('interpolate', ['version', function(version) {
    return function(text) {
      return String(text).replace(/\%VERSION\%/mg, version);
    }
  }]);

Now you can include these in your bundle by requiring them in your app/js/app.js file:

require('angular/angular');

// Relative paths to include services.js and filters.js into your bundle
require('./services');
require('./filters');

// then include them into your app
angular.module('myApp', ['myApp.filters', 'myApp.services'])
  .config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/view1', {templateUrl: 'partials/partial1.html'});
    $routeProvider.when('/view2', {templateUrl: 'partials/partial2.html'});
    $routeProvider.otherwise({redirectTo: '/view1'});
  }]);

Now you can use the filter in your HTML. Edit your app/partials/partial1.html file with the following:

<p>This is the partial for view 2.</p>
<p>
  Showing of 'interpolate' filter:
  {{ 'Current version is v%VERSION%.' | interpolate }}
</p>

Globals

Binding a global function to a controller isn't a good idea, IMO, as you're cluttering the global namespace but is possible using Browserify. Simply make sure to add it to the window global object. Create a app/js/HelloController.js file with the following:

window.HelloController = function($scope) {
  $scope.dude = 'AngularJS';
};

In your app/index.html use the controller:

<div ng-controller="HelloController">
  Hi {{dude}}!
</div>

and finally bundle your controller by adding it to your app/js/app.js file:

require('./HelloController');

Conclusion

These are fairly basic examples of using AngularJS with Grunt and Browserify. They can easily be extended to use grunt-contrib-connect and grunt-contrib-watch to build your app upon file changes and serve the dist/ folder on a local server. As well as all the other Grunt tasks available, eg.: testing, templates pre-processing, etc.

The goal here is to keep it as simple as possible with AngularJS while still automating the build process.