8: Deletes and resource error handling

Part 8 of the tutorial focuses on adding a delete button to our ngGrid, and adding error handling in case our rails application rejects our updates.  You can find the tutoral index, or hit the tutorials menu at the top and select the Rails 4 tutorial.

If you haven’t completed the earlier tutorials, you can get the code at github:PaulL tutorial_7.

At the end of this post we’ll have an extra button on our list page for delete, and we’ll have error handling using a directive:

club detail with more errors

We’ll start by adding a delete button. The button itself is easy – change the grid configuration in the controller to put in a delete button:

    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Club Name'},
      {field: 'contact_officer', displayName: 'Contact Officer'},
      {displayName: 'Edit', cellTemplate: 'Edit '},
      {displayName: 'Delete', cellTemplate: 'Delete '}
    ],

We need a delete method on our resource so that we can pass that delete through to the server. Angular provides both a delete and a remove method on the resource. Apparently the delete method can give problems in some browsers, as delete can be a reserved word. We’re using the remove method to be safe.

Add a delete method on the controller:

  $scope.deleteClub = function(club) {
    club.$remove();
    $scope.clubs = ClubRes.query();
  };

The delete/remove has some trickiness on the resource. Rails seems to get confused when we call the delete without doing anything to the resource, and the solution I’ve found so far is to set the headers to tell rails that we’re giving it JSON. So update the resource to look like:

/**
 * And a resource to allow us to get at the server
 */
.factory( 'ClubRes', function ( $resource )  {
  return $resource("../clubs/:id.json", {id:'@id'}, {'update': {method:'PUT'}, 'remove': {method: 'DELETE', headers: {'Content-Type': 'application/json'}}});
})

Save that and grunt build – you should now have a delete.  Be careful here, we’re running our end-2-end tests against our development rails instance.  If you delete the items from the database that your end-2-end tests expect, your end-2-end tests will fail.  I’d recommend running instead against your test rails instance and with a known good set of database records, doing that is outside the scope of this tutorial.

Add the unit test for the delete method:

      it('Calls delete on first row', function() {
        scope.httpBackend.expect('DELETE', '../clubs/1.json').respond();
        scope.httpBackend.expect('GET', '../clubs.json').respond([
          {"contact_officer":"Contact 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":"Contact 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"}
        ]);

        // call edit
        scope.deleteClub(scope.clubs[0]);

        scope.$digest();
        scope.httpBackend.flush();
      });

Finally for this post, we’re going to put some error handling in. Plausibly some of our calls to the rails backend might not work, instead of staying on the detail page with no indication of the problem we’d going to have some error handling.

Angular provides error handling as part of the server calls on the resource, we currently have a success response callback, we need to also add an error response callback:

  $scope.submit = function() {
    if ($scope.clubId) {
      $scope.club.$update(function(response) {
        $state.transitionTo('clubs');
      }, function(error) {
        $scope.error = error.data;
      });
    }

This is tied into the asynchronous processing (promises). Basically this says “call the save method, if it’s successful call the first function, if it’s not successful call the second function”. In our first function we close the dialog, in our second function we set the error variable. We do the same thing again with the update function:

    else {
      $scope.club.$save(function(response) {
        $state.transitionTo('clubs');
      }, function(error) {
        $scope.error = error.data;
      });
    }
  };

We then need to put that error somewhere. As a starting point we’ll just put the error block onto the top of club_edit.tpl.html, we’ll only display that block if there’s something in the error object.

<div>
  <div ng-show="error">
    <p>Error: {{error.data}}</p>
  </div>
  <p>Name: <input ng-model="club.name" /></p>
  <p>Contact Officer: <input ng-model="club.contact_officer" /></p>
</div>

This says “if the error variable has a value, then show the below div”. The div itself just shows the data from the error variable that came back from rails. We want an error that we can use to see this, so let’s modify app/models/club.rb in our rails app to make the name field mandatory:

class Club < ActiveRecord::Base
  validates_presence_of :name
end

Try creating a new club, or editing a club, and leaving the name blank. You should see an error (albeit a rather ugly one, we’ll pretty it up soon after we do our unit testing – no dessert until you eat your vegetables).

club detail with error

We need to include the error handling in the unit tests.  What we need is to test updates and saves with error, and without error.  Amend src/app/club/club_spec.js to add a failure test to the update tests:

    describe( 'Club detail controller update method tests', function() {
      beforeEach(angular.mock.inject(function($controller){
        $controller('ClubCtrl', {$scope: scope, $stateParams: scope.fakeStateParams});

        // The initial render triggers a get, drain that before we start the test proper
        scope.httpBackend.expectGET('../clubs/2.json').respond({"contact_officer":"Contact 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"});

        scope.$digest();
        scope.httpBackend.flush();
       }));

      it('Submit with clubId calls put on server, put succeeds', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPUT('../clubs/2.json').respond({});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('clubs');
      });

      it('Submit with clubId calls put on server, put fails', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPUT('../clubs/2.json').respond(422, {"name":["can't be blank"]});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).not.toHaveBeenCalledWith('clubs');
        expect(scope.error).toEqual( { name: [ "can't be blank" ] });
      });      
    });

Similarly, add a failure test to the save tests:

    describe( 'Club detail controller save method tests', function() {
      beforeEach(angular.mock.inject(function($controller){
        scope.fakeStateParams.clubId = null;
        $controller('ClubCtrl', {$scope: scope, $stateParams: scope.fakeStateParams});
       }));

      it('Submit with clubId calls post on server, post succeeds', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPOST('../clubs.json').respond({});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).toHaveBeenCalledWith('clubs');
      });

      it('Submit with clubId calls post on server, post fails', function(){
        spyOn(scope.$state, "transitionTo").andCallThrough();

        scope.club.name = 'Changed name';     
        scope.submit();

        scope.httpBackend.expectPOST('../clubs.json').respond(422, {"name":["can't be blank"]});
        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.$state.transitionTo).not.toHaveBeenCalledWith('clubs');
        expect(scope.error).toEqual( { name: [ "can't be blank" ] });
      });      
    });

Finally, add error handling to the delete method as well. This follows a similar pattern:

  $scope.deleteClub = function(club) {
    club.$remove(function(response) {
      $scope.clubs = ClubRes.query();
    }, function(error) {
      $scope.error = error.data;
    });
  };

You also need to add the error display to clubs.tpl.html:

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

<div ng-show="error">
  <p>Error: {{error.data}}</p>
</div>

<div>
  <div class="gridStyle" ng-grid="gridOptions"></div>
  <button ng-click="newClub()" class="btn btn-primary" >New</button>
</div>

You can trigger an error to see the behaviour by opening the clubs page, then killing your rails server. When you click delete you’ll get an error because your server isn’t there.

Add the delete unit test:

      it('Calls delete on first row, success', function() {
        scope.httpBackend.expect('DELETE', '../clubs/1.json').respond();
        scope.httpBackend.expect('GET', '../clubs.json').respond([
          {"contact_officer":"Contact 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":"Contact 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"}
        ]);

        // call edit
        scope.deleteClub(scope.clubs[0]);

        scope.$digest();
        scope.httpBackend.flush();
      });

      it('Calls delete on first row, success', function() {
        scope.httpBackend.expect('DELETE', '../clubs/1.json').respond(409, { error: "You can't do that"});

        // call edit
        scope.deleteClub(scope.clubs[0]);

        scope.$digest();
        scope.httpBackend.flush();
        expect(scope.error).toEqual({ error: "You can't do that" });
      });

Next, we’d like to make our error handling a bit more pretty. Ideally, our errors on the edit page would be associated with the field that was in error, and we’d make the field coloured to show that it’s in error.

The bootstrap styles provide ways to do this, but we’re not overly keen to go writing all the logic on each individual field. AngularJS provides a way to deal with this, which is to write a directive. Think of a directive as a way to extend HTML – so we’re thinking “I wish there was just an HTML element that displays errors in a sensible way, based on my error object.” And that’s what we can write.

Create a new folder src/common/errorHandling, we’re choosing to make this a common service because we’re going to share it across all our pages. In that folder create a new javascript file src/common/errorHandling/errorHandling.js:

/**
 * Common error handling module, provides a directive for displaying field errors
 */

angular.module( 'common.errorHandling', [])

.directive('fieldError', function () {
  return {
    restrict: 'A',
    scope: {
      errors: '=',
      field: '@'
    },
    templateUrl: 'errorHandling/fieldError.tpl.html'
  };  
})
;

This declares a directive called fieldError. The restrict clauses says that this directive can only be used as an attribute on an html object (e.g. <div fieldError>). The scope clause says that we’re going to have a model variable passed in that we want to share with our parent scope. We’re going to have a field variable passed in that we won’t share with the parent scope, which is known as an isolate scope.  An isolate scope lets you use the same directive multiple times on a page, each time with different parameters, without those parameters overwriting each other.

The operation of a directive is a bit esoteric, and the documentation a bit obscure. But once you get one working you can play with it to learn what the other options do – from a working base.

Having declared the directive itself, we now need the template that we described in the directive.  Create a file src/common/errorHandling/fieldError.tpl.html:

<div id="{{field}}.field_errors" ng-repeat="error in errors[field]"> {{error}}</div>

Add this new module as a dependency into app.js:

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

Finally, update club.tpl.html to use this new directive:

<div class="body">
  <div class="form-horizontal">
    <div class="control-group">
      <label class="control-label">Club name:</label>
      <div class="controls">
        <input type="text" autofocus ng-model="club.name" />
      </div>
      <div field_error errors="error" field="name"></div>
    </div>

    <div class="control-group">
      <label class="control-label">Contact officer:</label>
      <div class="controls">
        <input type="text" ng-model="club.contact_officer" />
      </div>
      <div field_error errors="error" field="contact_officer"></div>
    </div>

    <div class="controls">
      <button ng-click="submit()" class="btn btn-primary" >Save</button>
      <button ng-click="cancel()" class="btn btn-primary" >Cancel</button>
    </div>
  </div>
</div>

Grunt build and run the club detail page, and try creating a club without a name again.  You should now see an error directly below the field.

Our last item is to deal with the colours on the field – we want the field and the error to be red if there is an error present for the field.  Edit src/less/main.less to add an error class:

.error {
  color: @errorText;    
}

And add some code to club.tpl.html to set the error class if there are any error messages on the field:

<div class="body">
  <div class="form-horizontal">
    <div class="control-group" ng-class="error.name ? 'error' : ''">
      <label class="control-label">Club name:</label>
      <div class="controls">
        <input type="text" autofocus ng-model="club.name" />
      </div>
      <div field_error errors="error" field="name"></div>
    </div>

    <div class="control-group" ng-class="error.contact_officer ? 'error' : ''">
      <label class="control-label">Contact officer:</label>
      <div class="controls">
        <input type="text" ng-model="club.contact_officer" />
      </div>
      <div field_error errors="error" field="contact_officer"></div>
    </div>

    <div class="controls">
      <button ng-click="submit()" class="btn btn-primary" >Save</button>
      <button ng-click="cancel()" class="btn btn-primary" >Cancel</button>
    </div>
  </div>
</div>

You should end up with a page somewhat like this:

club detail with formatted error

We also note that we can’t just press save on a blank page.  This is due to the new strong parameters functionality in rails 4, and in particular to the following method in our controller:

    def club_params
      params.require(:club).permit(:name, :contact_officer, :date_created)
end

This method says that we must have a club, and on that club we permit only the name, contact_officer and date_created fields.  It replaces the fields_accessible logic in rails 3.

The trick is that when we leave both fields blank then rails ends up with club: {} as the input parameter, and that doesn’t pass the rule that we require a club.  Change the line instead to:

    def club_params
      params[:club].permit(:name, :contact_officer, :date_created)
    end

Do the same in the teams controller.

    def team_params
      params[:team].permit(:club_id, :name, :captain, :date_created)
    end

While you’re at it, amend the club model to have more (and more interesting) validation so that you can see more error messages:

class Club < ActiveRecord::Base
  validates_presence_of :name, :contact_officer
  validates_length_of :name, :in => 5..30
  validates_length_of :contact_officer, :in => 9..25
end

club detail with more errors

The final code for tutorial 8 can be found at github: PaulL : tutorial_8.  Next post: do the same thing all over again with the team entity, and building a link between clubs and teams.

Advertisements

2 thoughts on “8: Deletes and resource error handling

  1. Pingback: 7: Adding unit testing using karma | technpol

  2. Pingback: AngularJS and Rails 4 CRUD application using ng-boilerplate and twitter bootstrap 3: Tutorial Index | technpol

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