AngularJS and Rails Tutorial: part 3 a basic list using $resource for a restful query from rails

Part 3 of a tutorial that connects AngularJS to a Rails backend. This post focuses on tailoring ng-boilerplate, using ngResource to obtain data from rails, and introducing karma unit testing.  The previous post was Installing ng-boilerplate, and serving up via Rails, the next post is Using ngGrid and building an edit popup.  You can also find the index of the posts in the tutorial, or hit the tutorials menu at the top and select the Rails 3 tutorial.

There is a newer, rails 4 and newer angularJS, version of this tutorial.  It is also more complete and has a nicer UI that doesn’t use modal windows, which is probably a better choice for anyone starting fresh today.  The first page in that tutorial is here, and the index here.

In this post, I’m going to talk a bit more about what angular is actually doing, and how it connects to rails.  Hopefully it will be semi-understandable, but I’ll note that it’s taken me a few weeks to bend my brain around what angular is doing – it’s quite different than rails.

The boilerplate app that we created has a bunch of functionality in it, but most of that functionality we don’t really want.  So step one is going to be to go through it all and firstly to rename everything to our new app name (league), and secondly to clean out the bits that we aren’t really using.  If you haven’t followed the two previous tutorials, you can download the ending point code from github: tutorial_2.  At the end of this tutorial section you should have an application that looks something like:

Tutorial_3 clubs from server

Let’s start off in the package.json file in the root directory.  This content looks like it’s getting packaged up and made available somewhere (not clear yet where).  Anyway, change the information at the top of this file to reflect our new app:

{
  "author": "",
  "name": "league",
  "version": "0.0.1",
  "homepage": "localhost",
  "bugs": "localhost",
  "repository": {
    "type": "git",
    "url": "localhost"
  },

Next, we move on to the src directory. This directory is where all the code lives, the code from here is built into the build directory by the grunt build command (and, for production usage, into the bin directory by the grunt compile command).

Let’s start by editing src/index.html. This is a standard web page, but has some extra angular directives in it. The most important is the ng-app, which tells the angular scripts what your app name is. Let’s change it:

<html ng-app="league" ng-controller="AppCtrl">

We delete all the twitter tags (no use for them at the moment), and put some information in:

<!-- social media tags -->
<meta property="og:title" content="league" />
<meta property="og:type" content="website" />
<meta property="og:url" content="localhost" />
<meta property="og:description" content="A sample angular/rails app that provides a sports league">

We’ll leave the body alone for now, later we’re going to repurpose some of Josh’s layout to our purposes, for now we want to get the renaming out of the way and prove the app still works.

We tidy up the footer, we’ll still reference Josh’s good work, but make it a bit more specific to us:

<footer class="footer">
  <div class="container">
    <div class="footer-inner">
      <p>
        Based on:
        <a href="http://github.com/joshdmiller/ng-boilerplate">ngBoilerplate by Josh Miller</a>,
        <a href="http://www.angularjs.org">AngularJS</a>,
        <a href="http://getbootstrap.com">Bootstrap</a>,
        <a href="http://angular-ui.github.com/bootstrap">UI Bootstrap</a>,
        and
        <a href="http://fortawesome.github.com/Font-Awesome">Font Awesome</a>.
      </p>
    </div>
  </div>
</footer>

That leaves us with a web page that declares it’s an angular app called “league”, with the main client-side controller called “AppCtrl”. It has sub-urls of /home and /about, which angular will handle for us through calling the uiRoute function, which we’ll come to later.

Next, we move into src/app/app.js. In here we simply want to rename everything to league, we end up with a javascript model called ‘league’, this module has a couple of dependencies called league.home and league.about:

angular.module( 'league', [
  'templates-app',
  'templates-common',
  'league.home',
  'league.about',
  'ui.state',
  'ui.route'
])

.config( function myAppConfig ( $stateProvider, $urlRouterProvider ) {
  $urlRouterProvider.otherwise( '/home' );
})

.run( function run () {
})

.controller( 'AppCtrl', function AppCtrl ( $scope, $location ) {
})
;

We then need to edit src/app/home/home.js

angular.module( 'league.home', [

And similarly in src/app/about/about.js

angular.module( 'league.about', [

We edit the test scripts, starting with src/app/app.spec.js

    beforeEach( module( 'league' ) );

And src/app/home/home.spec.js

  beforeEach( module( 'league.home' ) );

We now run a build, by running

grunt build

This should push everything into build/, and therefore also into public/UI. So try going to the index page again on your rails app. It should show you almost exactly what we had before, except behind the scenes our app is now called “league”, and the footer has changed slightly.

It’s still not connecting to our rails back end yet, but we’re sneaking up on it. Let’s create a clubs entity. Create a directory src/app/club. Create a file within it src/app/club/club.tpl.html. This is the page that the uiRoute will take us to when someone goes to the /club route. For now, let’s make this pretty simple:

<div>
  <h1>Club functions</h1>
  <p>Soon this will show a list of all the clubs, based on information from the server</p>
</div>

<div class="body">
  <table>
    <thead>
      <td>Name</td>
      <td>Contact Officer</td>
    </thead>
    <tr>
      <td>sample club</td>
      <td>sample officer</td>
    </tr>
  </table>
</div>

Next, we create src/app/club/club.js, which is heavily based on home.js. What this provides is:

  • a module, with a set of dependencies that are needed for the page to work properly. The only dependencies at present is ui.state. There was a plusOne service, but this was doing some work with google plus, we don’t need it.
  • a route, which tells angular that when someone goes to the url /club, that we want to use the html template club.tpl.html, and that template has a controller ClubCtrl
  • the controller itself, which is a simple function that currently only sets the tab title

Note that we’re using the stateProvider routing functionality, otherwise known as ui-router, which is an alternate router to the default angular router. This router is used to associate URLs with templates and controllers, and here we’re only scratching the surface of what it can do, there’s a lot of good starter documentation for it, with more detail around how to use it for stateful routing. Go ahead and create src/app/club/club.js:

/**
 * Club module
 */
angular.module( 'league.club', [
  'ui.state'
])

/**
 * Define the route that this module relates to, and the page template and controller that is tied to that route
 */
.config(function config( $stateProvider ) {
  $stateProvider.state( 'club', {
    url: '/club',
    views: {
      "main": {
        controller: 'ClubCtrl',
        templateUrl: 'club/club.tpl.html'
      }
    },
    data:{ pageTitle: 'Club' }
  });
})

/**
 * And of course we define a controller for our route.
 */
.controller( 'ClubCtrl', function ClubController( $scope ) {
});

Open up src/index.html again, and change the last link (issues) to take us to clubs instead.

  <li ui-route="/club" ng-class="{active:$uiRoute}">
    <a href="#/club">
      <i class="icon-github-alt"></i>
      Clubs
    </a>
  </li>

And change src/app/app.js to refer to the new module:

  'templates-app',
  'templates-common',
  'league.home',
  'league.about',
  'league.club',
  'ui.state',
  'ui.route'
])

Run grunt build again and see what we have. You should get a page with a clubs link on it, and when you click that link, it should take you to a clubs page. If it doesn’t, check the developer tools on your browser, look at the javascript console, and see whether you have the right source showing up in your browser (if you don’t, perhaps your grunt build didn’t work).

We’d like to create automated tests as we go.  So far all we’ve created is a state provider, which is effectively navigation.  I’m struggling to find examples where navigation is tested in a unit test, but it’s definitely tested in end to end tests.  This is “coming soon” to ng-boilerplate according to the ng-boilerplate issues list., so we’re not going to do it right now.   When we do implement this, we’re going to use information from this page from the artlogic blog, and in particular the e2e test example given there.  For now, therefore, we’re not testing the navigation.  :-(.  We’ll do tests when we get a bit more logic in the controller.

Next, we want to put some data into that page based on a client-side model. Models in angular are just variables that are created within the controller. So let’s create a clubs variable within the controller in src/app/club/club.js. Angular makes all variables attributes on the $scope variable – this is basically trickiness to provide variable scoping in a language that doesn’t really support scoping. There’s also magic running that makes all $scope variables that are declared in the controller available to the associated view:

.controller( 'ClubCtrl', function ClubController( $scope ) {
  $scope.clubs = [
    {name: "hard coded club 1", contact_officer: "hard coded officer 1"},
    {name: "hard coded club 2", contact_officer: "hard coded officer 2"}
  ];
});

We’ll then use data binding to put this information into the view. What we want to do is to iterate through all the clubs, creating a row in the table for each club within the clubs list. The syntax for this is "ng-repeat="club in clubs". The route setup we did earlier means that this view has access to the data within the ClubCtrl controller, so it can get to the variable called $scope.clubs. Angular will iterate through the array of clubs, duplicating the html block that we attached the ng-repeat to, and filling in data. To fill in data we can use the {{club.variable}} notation. Make the following changes to club.tpl.html:

  <table>
    <thead>
      <td>Name</td>
      <td>Contact Officer</td>
    </thead>
    <tr ng-repeat="club in clubs">
      <td>{{club.name}}</td>
      <td>{{club.contact_officer}}</td>
    </tr>
  </table>

Run grunt build, and go back to your page to see what you have.  You should get a list of two clubs in your page, which looks something like the following:

Tutorial_3 hard coded clubs

This is now the point where we can add tests.  We’re going to add a test of our controller to verify that it’s correctly creating 2 items in the list (I know, that’s not much of a test, but it’s the logic we have now, and we’re staying away from e2e tests).

Create the file src/app/club/club.spec.js, which will be the unit test for our club logic.  Into this file add:

/**
 * Unit tests for the club functionality
 */
describe( 'Club controller', function() {
  //mock Application to allow us to inject our own dependencies
  beforeEach(angular.mock.module('league'));

  //mock the controller for the same reason and include $rootScope and $controller
  beforeEach(angular.mock.inject(function($rootScope, $controller){
    //create an empty scope
    scope = $rootScope.$new();

    //declare the controller and inject our empty scope
    $controller('ClubCtrl', {$scope: scope});
  }));

  // tests start here
  it('Has two clubs defined', function(){
    expect(scope.clubs.length).toEqual(2);
  });  
});

This is following a template from tuesdaydeveloper, and is doing a few things:

  • Declaring that we’re testing the club controller
  • Before each test, mocking out the league module (todo: why is this a mock and not an include?)
  • Before each test, creating a new scope and injecting it into the test.  This is creating a scope variable in the testing code and pushing that scope variable into the controller, the effect of this is that the unit testing code gets access to all the variables within the controller and can verify their values in the local scope variable
  • Defines the controller that we’re testing, and calls new on it
  • Note that tuesdaydeveloper uses toBe to test the result, the clear recommendation is to use toEqual unless you really mean “exact same object”.  For scalar variables like integers both will work, but toEqual is better practice

Run grunt build to see what happens, and try changing the toEqual from 2 to 3 and back again.

As our final step in this post, we’re going to tie the data to the rails server instead of hard coded information.  To do that we need to install ng-resource, which provides the functionality to connect to our rails server. We use bower to install it, and we save that into the bower.json file.

bower install angular-resource --save-dev

We are also keen to have a later version of angular, we want at least 1.2. There is new functionality in angular-resource that we want in that version. If your version isn’t at least that new, run an upgrade:

bower update

This will hopefully give you options for upgrading your functionality. If it doesn’t, you may need to edit bower.json, and change the version number for both angular and angular-resource to ~1.2 then run bower update to force this upgrade. If it gives you an option to do so, prefix your selection with ! to persist it to bower.json.

Edit build.config.js to make angular-resource available (note the addition of a comma on the line above):

      'vendor/angular-ui-utils/modules/route/route.js',
      'vendor/angular-resource/angular-resource.js'

The magic in the build scripts will automatically include all these into the html, and when you compile your application for web deployment (into the bin directory) the scripts will automatically include only the single concatenated javascript file.

Make two edits in the src/app/club/club.js file, one creates a resource, which connects to the rails backend.  The second tells the controller to call this resource to get the clubs, rather than hard coding them.  Don’t miss that we added “ClubRes” to the function definition for ClubController itself.

/**
 * And of course we define a controller for our route.
 */
.controller( 'ClubCtrl', function ClubController( $scope, ClubRes ) {
  $scope.clubs = ClubRes.query();
})

/**
 * Add a resource to allow us to get at the server
 */
.factory( 'ClubRes', function ( $resource )  {
  return $resource('../clubs.json');
})
;

We also need to modify the dependencies for this module – it now has a dependency on ngResource:

angular.module( 'league.club', [
  'ui.state',
  'ngResource'
])

Run grunt build again (it will give errors on the unit tests – ignore those for now), and then look on the UI to see whether you are now getting data from the server. Again, the developer tools on your browser can help diagnose any problems with the UI. I did occasionally get problems where my sqlite database had been clobbered, perhaps when we merged ng-boilerplate in.  If that happens, try running rake db:migrate again, and restart your rails server.

You should have a page like:

Tutorial_3 clubs from server

Now we have some more interesting logic to write unit tests on.  What we’re going to do is to write a mock, which replaces the ngResource call with a pre-canned result.  In this way we can test the Angular logic on it’s own without relying on the server.  Let’s start off by creating that mock, again we’re relying on the template from tuesdaydeveloper, with a bit of help from arsuceno in the angular documentation comments (why is this so hard, I’d sort of expected there to be lots of sample code doing this stuff….):

/**
 * Unit tests for the club functionality
 */
describe( 'Club controller', function() {
  var scope, httpBackend;

  //mock Application to allow us to inject our own dependencies
  beforeEach(angular.mock.module('league'));

  //mock the controller for the same reason and include $rootScope and $controller
  beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_ ){
    //create an empty scope
    scope = $rootScope.$new();

    scope.httpBackend = _$httpBackend_;
    scope.httpBackend.expect('GET', '../clubs.json').respond([
      {"contact_officer":"Officer 1","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":1,"name":"Club 1","updated_at":"2012-03-03T00:00:00Z"},
      {"contact_officer":"Officer 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Club 2","updated_at":"2012-03-03T00:00:00Z"}]);

    //declare the controller and inject our empty scope
    $controller('ClubCtrl', {$scope: scope});
  }));

  // tests start here
  it('Has two clubs defined', function(){
    scope.$digest();
    scope.httpBackend.flush();
    expect(scope.clubs.length).toEqual(2);
  });

  it('First club\'s contact officer is as expected', function(){
    scope.$digest();
    scope.httpBackend.flush();
    expect(scope.clubs[0].contact_officer).toEqual('Officer 1');
  });

});

This code introduces a mock backend for http, which at the moment has a single repeatable response that is given whenever it’s asked for clubs.json.  We define that mock up front, and inject the mock $httpBackend into the controller.  We also configure our httpBackend mock to expect a call to clubs.json, and tell it what we want it to return when that call comes.

On entering the controller we call $digest, which triggers the angular asynch processing.  You could alternatively wrap you call to the controller in a $apply, I found that less intuitive.  We then call flush on httpBackend, which causes it to call our mock (the angularJS docs describe this process, but they ignore the $digest bit).  The flush tells httpBackend to check the expected calls, and return the values asked for.  We  should get our results as expected.  In a later tutorial we refactor some of this into a beforeEach block to make it more reusable, but this is working and is clear for now.

When you run grunt build you should now have 4 unit tests that are all passing.

The final code position for this tutorial is on github for tutorial_3.

In the next tutorial, we’ll look at modifying our layout to use ngGrid instead of a table, giving us better formatting, and providing a modal popup for editing the clubs.

Advertisements

6 thoughts on “AngularJS and Rails Tutorial: part 3 a basic list using $resource for a restful query from rails

  1. Pingback: CRUD application with AngularJS, Rails, twitter-bootstrap and ng-boilerplate: part 2 boilerplate served by rails | technpol

  2. Pingback: CRUD application with AngularJS, Rails, twitter-bootstrap and ng-boilerplate: part 4 grid and CRUD | technpol

  3. Pingback: Rails app for Angularjs, building the rails league application: part 1 | technpol

  4. Pingback: CRUD application with AngularJS, Rails, twitter-bootstrap and ng-boilerplate: part 5 New and Delete | technpol

  5. Pingback: AngularJS and Rails: Tutorial Index | technpol

  6. Pingback: AngularJS and Rails CRUD application using ng-boilerplate and twitter bootstrap | Gatelockservice

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s