04 Dec 2013, 09:30

Sailing App - part 1 - Intro to Yeoman, Grunt, Bower and AngularJS

I'm fan of sailing and the magasine "Voile Magasine" provides some doc / pdf / excel file with the index of all their issues and the boat they tested. Purpose of our app wil be to render the excel file in a user friendly manner to allow user to filter depending on different criterion.

As you can see, the file has a few columns :

  • Boat model
  • Boat type
  • Test type
  • Issue Number
  • Month
  • Year

So you quickly can imagine on what we'll do in our app.

So let's start !

Disclaimer : I'll discover the tools on my way to build the app ; if you have best practices & so, please share and improve my writings :-)

Installing Yeoman / Grunt / Bower

So in this first part, the point is to define how we will work and get the " develoment tooling" chain. I decided to use Yeoman, which stands for a bunch of tools to allow the development of modern web apps. It includes :

  • yo - the scaffolding tool from Yeoman
  • bower - the package management tool
  • grunt - the build tool

First, it requires you to have a working instance of Node.js installted on your computer. I used the 0.10.22 version of Node. I'll not cover the node installation, on my mac, I just made a :

brew install node

Check that npm (the Node Package Manager) is installed and available.

So to install Yeoman, this should be sufficient :

npm -g install yo

If it clainms about bower version, just do first :

nrpm install -g bower grunt-cli

The -g installs the software globally for your node environment and not in the current directoty in a node_modules directory.

And we're done for yeoman :-)

Let's initiate our project ; we want to build it with AngularJS, the MVC Javascript framework, and fortunately Yeoman has a genreator for AngularJS, meaning that Yeoman provides some integration with AngularJS.

Let's install the generator :

npm install -g generator-karma
npm install -g generator-angular

The Karma generator was required by angular's one ; Karma is a tester runner in Javascript.

Project initalisation

So now we can initiate our projet, we'll call "vm-collection" :

mkdir vm-collection && cd $_
yo angular vm-collection --minsafe
[?] Would you like to include Twitter Bootstrap? (Y/n)Y
[?] Would you like to use the SCSS version of Twitter Bootstrap with the Compass CSS Authoring Framework? Yesn) Y
[?] Which modules would you like to include? (Press <space> to select)
angular-resource.js
angular-cookies.js
angular-sanitize.js
> angular-route.js

Select all modules (especially route at least) so far ; and npm will work for a few minutes to retrive all the required stuff. The --minsafe argument is to make you app ready for minification when it will be about to be deployed.

You did not see but the bower tool was used to retrived some libs. Bower is a package manager for frontend libs like jquery and others. It's well explained on the Yeoman page decidated to Bower. Libraries installed with Bower will be in the app/bower_components folder and their list will be in the bower.json file in your project root folder.

For example, if we want to use the Modernizr lib for HTML5/CSS3 detection in our project, just type :

bower install modernizr --save

The --save is useful to store the information you installed Modernizr in bower.json. When you share your project with others, you no longer need to save your lib in your source control system oher whatever. Just provide your bower.json file with all required libs (and versions if necessary) and your peers will have just to type bower install to fetch all the required libs.

At this step, if you run grunt server, it will open your default browser and you will see the demo page of the angular generator. The server has some watch/auto-reload mechanism which will reload the latest files when changed/saved and reload your current page. grunt test will launch the tests with karma but we'll see this later.

First Angular controller / route / template and test

I will not introduce AngularJS by itself and would recommend you to go through the tutorial at least to understand what will be done. Nevertheless, I'll give some explaination on what I'll do.

First of all:

  • I transformed the xls file from Voile Magasine into a CSV one I put in app/data/index-essais-215.csv
  • I transformed it then into a JSON document as even if I found a CSV to JSON on the fly library with  jquery-csv ; I had some issue in tests. So I converted it once for all in JSON ; File is app/data/index-essais-215.json 
  • For this tutorial, I have no admin interface nor backend storage to keep it as simple as possible.

Let's create our first "route" in Angular, which would results in :

  • a added route in app/scripts/app.js,
  • a new controller app/scripts/controllers/index.js
  • and a new view in app/views/index.html.
  • The controller being added to app/index.html

Without the yeoman generator, we should have done it manually, which is time saving.

yo angular:route index --minsafe
invoke angular:controller:/usr/local/lib/node_modules/generator-angular/route/index.js
create app/scripts/controllers/index.js
create test/spec/controllers/index.js
invoke angular:view:/usr/local/lib/node_modules/generator-angular/route/index.js
create app/views/index.html

As we saw previously, we added the --minsafe argument to be minification ready when necessary.

In app/scripts/controllers/index.js

'use strict';

angular.module('vmCollectionApp')
.controller('IndexCtrl', ['$scope', '$http', function ($scope, $http) {
$http.get('/data/index-essais-215.json')
.success(function(data) {
$scope.issuelist = data;
})
}]);

What we have and added :

  • The definition of the IndexCtrl controller as part of the vmcollectionApp.
  • The use of the $http service of AngularJS to access some resources through HTTP. We'll use it to access our json file.
  • Once the file is parsed, content is stored in a "issuelist" variable which we link to $scope to make it accessible from the template.

In app/views/index.html ; let's start with a basic rendering of our JSON file

<p>We have {{ issuelist.length }} entries</p>
<table>
<thead>
<td>Model</td>
<td>Boat type</td>
<td>Category</td>
<td>Month</td>
<td>Year</td>
</thead>
<tr ng-repeat="item in issuelist">
<td>{{ item.model }}</td>
<td>{{ item.boat_type }}</td>
<td>{{ item.category }}</td>
<td>{{ item.month }}</td>
<td>{{ item.year }}</td>
</tr>
</table>

What we have and added :

  • First line allow us to know how many entries do we have in the issuelist variable.
  • Then we build a table and thus use the ng-repeat directive to populate the lines of the table.
  • As we built an object, for each line, we access the value by its property name ; property names are defined in the first line of the json file.

In your console, run grunt server which will open a new browser ; then go to http://127.0.0.1:9000/#/index to see the basic rendering we made. The "/index" is not defined by magic, routing system is defined in app/scripts/app.js, where for an url the controller and the template to be used are defined.

You should see something like :

vmcollection_app_basic_rendering_table.png

We could say we are finished but we're not as there is not tests so far. Let'd do the related test !

Related unit is in test/spec/controllers/index.js :

'use strict';

describe('Controller: IndexCtrl', function () {

  // load the controller's module
  beforeEach(module('vmCollectionApp'));

  var IndexCtrl, $httpBackend,
    scope;

  // Initialize the controller and a mock scope
  beforeEach(inject(function (_$httpBackend_, $controller, $rootScope) {
    // Define the expect answer from the http request and content of the json file supposed to be fetched
    var issuelist = [{
      "model": "420",
      "boat_type": "dériveur",
      "category": "présentation",
      "issue": "11",
      "month": "novembre",
      "year": "1996"
      }];
    // Initiate httpbackend to fake the $http service
    $httpBackend = _$httpBackend_;
    $httpBackend.expectGET('/data/index-essais-215.json').respond(issuelist);
   
    scope = $rootScope.$new();
    IndexCtrl = $controller('IndexCtrl', {
      $scope: scope
    });
  }));

  it('issuelist should be non empty', function () {
    // JSON file is not yet retrieved so variable should be still undefined
    expect(scope.issuelist).toBeUndefined();
    // Get JSON data
    $httpBackend.flush();
// JSON file is retrieved so issuelist should be defined and have one entry ; cf issuelist content above, in the beforeEach section
expect(scope.issuelist.length).toBe(1);
// Go deeper, let's check properties
expect(scope.issuelist[0].model).toBeTruthy();
expect(scope.issuelist[0].boat_type).toBeTruthy();
expect(scope.issuelist[0].category).toBeTruthy();
expect(scope.issuelist[0].issue).toBeTruthy();
expect(scope.issuelist[0].month).toBeTruthy();
expect(scope.issuelist[0].year).toBeTruthy();
  });
});

What we did :

  • First, tests follow the Jasmine syntax.
  • the beforeEach section allow us to provide some fake content with the simulation of the $http service and providing a dummy content for our json file.
  • the test by itself will check if the issuelist is not defined before the test (it should not) ; then simulate the call to the JSON file and then will do what is defined in the controller and check that there is one entry.

And the answer when you run  grunt test :

[...]
Running "karma:unit" (karma) task
INFO [karma]: Karma v0.10.7 server started at http://localhost:8080/
INFO [launcher]: Starting browser Chrome
WARN [watcher]: Pattern "/Users/nsteinmetz/Documents/Projets/angular/vmcollection/test/mock/**/*.js" does not match any file.
INFO [Chrome 31.0.1650 (Mac OS X 10.7.5)]: Connected on socket I2qIcTpftG6bH-Z0UtQZ
Chrome 31.0.1650 (Mac OS X 10.7.5): Executed 2 of 2 SUCCESS (0.473 secs / 0.052 secs)

Let's stop here for this week with all what we did so far :

  • We installed our workflow chain with Yeoman, Grunt and Bower
  • We initiated our project and use the AngularJS generator from Yeoman to scaffold our application
  • We create a first full and complete Angular controller / route / view and unit test

See you next week (hopefully) !

[edit] : You can see the results and get the code.