AngularJS and Rails Tutorial: part 4 ngGrid and edit in a modal popup

Part 4 of a tutorial that connects AngularJS to a Rails backend. This post focuses on extending our club list view to use a grid instead of a basic list, and providing a modal form to edit the clubs.  The previous post was Adding a list (query) page to the AngularJS app, the next post is New and Delete.  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.

In the third post we hooked our AngularJS app to our rails app, and provided a list of clubs from the rails server.  If you haven’t completed the earlier posts in this tutorial series, download the code from github: tutorial_3.  In this post we’re going to extend in two ways – using the ngGrid control to provide a nicer looking list that has a lot of tailoring options, and we’ll be creating modal popups for the editing functions.

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.

At the end of this section of the tutorial, you should have a page that looks somewhat like this:

Tutorial_4 club grid

Tutorial_4 club edit

First, we’re going to change to using an angular grid called ngGrid.  This is similar to a datatable in many ways, but it’s baked into the Angular framework, and is very easy to use.  It’s easy to add, so we’re going to change all our list pages to use this.

We need to install angular-grid, which we use bower to do and again ask it to save it into the bower.json file:

bower install angular-grid --save-dev

We then edit build.config.js to make this available:

   'vendor/angular-resource/angular-resource.js',
   'vendor/jquery/jquery.js',
   'vendor/angular-grid/build/ng-grid.js'
   ],

We’re also making jquery.js available, as the grid uses it internally. The recommendations out there are that we shouldn’t ever use jquery directly, and I have a feeling that we maybe could get away without including it, but for the purposes of this tutorial we’re including it for now (and the installation process seems to have included it automatically).

You also need to include the ng-grid stylesheet so that everything looks pretty. Unfortunately, this is a .css file, and the boilerplate build scheme uses less. It’s hard to import css into a less-based build – the way boilerplate works it expects to concatenate all the less files into a single stylesheet, but less expects to send any import of .css files up to the client for processing.

Unfortunately the client processing doesn’t work, because the build process doesn’t copy the style sheet into the build/bin directory. To get around this we can use the fact that .css is also valid .less, so we can simply link the .css file as a .less, and then include it. Another option is to just rename the file .css, but that’s less likely to keep working when we do upgrades:

 cd vendor/angular-grid
 ln -s ng-grid.css ng-grid.less

We then edit <code>src/less/main.less</code>, and add another import at the bottom of the angular imports so that the style sheet is included:

  @import '../../vendor/angular-grid/ng-grid.less';

According to this post, at some point in the near future instead of doing the symlink we’ll be able to instead have:

  @import (less) '../../vendor/angular-grid/ng-grid.css';

Next, we replace the entirety of the table we created in <code>clubs.tpl.html</code> with an ng-grid.  The grid will have the gridStyle formatting applied to it, and gets it’s configuration options from a variable/model called gridOptions, which it will expect to find in the controller.

<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">
  <div class="gridStyle" ng-grid="gridOptions"></div>
</div>

We edit clubs.js to tell Angular that we have a dependency on ngGrid:

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

Finally, we edit the clubs controller to provide the gridOptions variable that the grid is using as it’s configuration.

.controller( 'ClubCtrl', function ClubController( $scope, ClubRes ) {
  $scope.clubs = ClubRes.query();
  $scope.gridOptions = {
    data: 'clubs',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Club Name'},
      {field: 'contact_officer', displayName: 'Contact Officer'}
    ],
    multiSelect: false
  };
})

What this is doing is telling the grid to get it’s data from the clubs variable (which is also our model), and telling it which columns to display, along with what title to give that column.  We’re also turning off multi-select.  The options for the ng-grid, and a bunch of really good examples, can be found in the documentation.

Check that’s working correctly – grunt build, then go to the page.  You should see a grid now, you’ll find that the grid is showing only one line when you’d really like it larger.  The reason for this is that ngGrid needs a specified height at all times, and we haven’t put a height on it.  Create the src/app/club/club.less stylesheet, add the gridstyle in:

.gridStyle {
  border: 1px solid rgb(212,212,212);
  height: 300px; 
}

We also need to tell the build system to pull in club.less, so edit src/app/less/main.less to import club.less (this goes right at the bottom, next to home.less):

  @import '../app/club/club.less';

Run grunt build again, and see if your grid is prettier.

So far, we’ve created nothing that’s easily unit testable (although I suspect there are ways to unit test the grid – haven’t found those yet), so we’re still ignoring karma.

We’d like to be able to edit the items in the list.  To do this we need:

  1. An edit button against each item in the grid.  This will be call a method on our controller when clicked
  2. A modal popup that is displayed, and allows you to edit that data.  We’re going to use the modal widgets to display this, and based on some reading we’re going to use a separate controller and a partial html template to handle this
  3. Updates to our ng-resource to call the server with updates

First, let’s put a button into the grid.  We want this button to be present in each row, and we want it to call a method that opens the modal dialog.  We do this in the src/app/club/club.js file as follows:

    {field: 'contact_officer', displayName: 'Contact Officer'},
    {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" class="btn btn-primary" ng-click="editClub(row.entity)" >Edit</button> '}
  ],
  multiSelect: false

What this is doing is creating a column with a title of Edit, and no data field as source.  In the cell itself, it’s putting the html for a button, and telling it that when the click event happens we want to call a method called editClub, passing in the entity that relates to the current row.

Next, we need to create this editClub method within our existing controller.  What it’ll do is to define a dialog that we want to open, this code goes within the controller, after the gridOptions:

/**
 * And of course we define a controller for our route.
 */
.controller( 'ClubCtrl', function ClubController( $scope, ClubRes, dialog ) {
  $scope.clubs = ClubRes.query();
  $scope.gridOptions = {
    data: 'clubs',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Club Name'},
      {field: 'contact_officer', displayName: 'Contact Officer'},
      {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" class="btn btn-primary" ng-click="editClub(row.entity)" >Edit</button> '}
    ],
    multiSelect: false
  };

  $scope.editClub = function(club) {
    $scope.myDialog = $dialog.dialog({dialogFade: false, resolve: {club: function(){return angular.copy(club);}}});
    $scope.myDialog.open('club/club_edit.tpl.html', 'ClubEditCtrl').then(function(result){
      if (result === 'cancel'){}
      else {
        $scope.clubs = ClubRes.query();
      }
    });  
  };
})

This defines a function within our controller called editClub. This function takes in a club, and creates a dialog, which creates a dependency on the dialog library (we’ll come to that). This dialog has passed in to it a variable called club, and we pass it in using a tricky angular thing called resolve. Basically this is a very complex way of passing data into a dialog, the main advantage of it is that it allows late binding (it can take a promise which will later turn into data once an asynch call has completed). The main disadvantage is that it has to be a function, not just a straight variable.

Anyway, what we’re passing in to the club variable is a copy of the club we got. The reason for using a copy rather than the entity itself is that if we passed in the entity itself (the row from our clubs model), then the two way data binding would update that entity directly. Which would make it hard to have a cancel button, as we’d have overwritten the data already.

Then, we’re opening the dialog, we’re expecting a partial html called club_edit.tpl.html, and a new controller called ClubEditCtrl. If the result is a cancel then we’re going to do nothing, if the result isn’t a cancel then we’ll assume the record was updated, and we’ll requery the server to get a new list. We’ll also set the title back to “Club” after we’ve been off to the modal.

Next, let’s create the modal popup itself.  This gives us a new template src/app/club/club_edit.tpl.html, and we’ll add an edit controller to club.js:

Start with the club_edit.tpl.html file:

<div class="modal-header">
  <h3>Edit Club</h3>
</div>

<div class="modal-body">
  <p>Name: <input ng-model="club.name" /></p>
  <p>Contact Officer: <input ng-model="club.contact_officer" /></p>
</div>

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

This defines a modal box, it has two fields (tied to the model – which will be in the controller we haven’t written yet, but expects to be $scope.entity with at least two fields – name and contact_officer), and two buttons on the bottom – save and cancel.  Save calls a method on the controller called submit, cancel calls a method called cancel.

We’ll now write the controller that provides that functionality.  This goes into the src/app/club/club.js file, it can go down the bottom after the factory but before the last semicolon (maybe it’d be better after the first controller, who knows.  Probably not important).

/**
 * We define a controller for the edit action
 */
.controller('ClubEditCtrl', function ClubEditController($scope, ClubRes, dialog, club) {
  $scope.club = club;
  $scope.submit = function() {
    $scope.club.$update(function(data) {
                            dialog.close($scope.club);      
                          });
   };

  $scope.cancel = function() {
    dialog.close('cancel');
  };
})

This controller uses $scopeand ClubRes the same as the previous controller.  It uses dialog, but this is without the $ sign, it looks like when you create a dialog the angular-bootstrap code is automatically adding a variable called ‘dialog’ to the local scope, and that variable is then available to be passed into the edit controller.  Refer this google groups discussion for more detail.  This is rather obscure behaviour, so I hope it’s tidied up in future versions.

It also expects a variable called club to be passed in.  This is the variable that we passed in through the “resolve” process earlier.  It sets the title to “Edit Club”, and copies the passed in club into the $scope.club variable, which also makes it available as a model in the view. It then defines two methods on the controller – submit and cancel. Cancel is pretty simple – it closes the dialog without saving anything.

Submit is making use of the ngResource to do some trickiness. When we first queried the clubs we got an array, with each item in that array being an entity. ngResource did some magic, and put a set of methods against each of those entities – so we can call club.method to interact with the server. In this case we’re doing an update, so we want to call club.$update to save it to the server. However, by default the ngResource gives us query, get, save, delete and remove. Remove is just an alias for delete (some browsers think that delete is a keyword). The trick is that save always creates a new entity in rails, we need an update method, and ngResource doesn’t have one automatically. Our submit method is calling this new $update method, and telling it that once the update is successful, it’d like it to call a function, that function closes the dialog.

So, now we have some tidyup of dependencies that we didn’t get to.

Change the ClubCtrl definition in club.js to tell it that we’re now using the dialog module as well:

.controller( 'ClubCtrl', function ClubController( $scope, ClubRes, $dialog ) {

Change the resource definition to tell it we’d like an update method:

  return $resource("../clubs/:id.json", {id:'@id'}, {'update': {method:'PUT'}});

The documentation for ngResource is pretty good, and worth a look.  But what we’re doing here is telling it a few things different than what we had before:

  • Our URL for interacting with clubs is in the format clubs/1.json.  ngResource is clever enough (if you have a recent version) that if a method doesn’t provide an id (e.g. the query method), then it doesn’t give us clubs/.json, it collapses the / out to give us clubs.json.  This is the feature we had to upgrade to 1.2 to get…
  • If our method requires an id, you can get it from the entity itself, look for a field called id.  (id: ‘@id’ – the @ tells it to get the data from the entity itself)
  • We’d like an update method, it uses the http verb PUT

We need to include the stylesheet for the bootstrap modal stuff, so back to src/app/less/main.less, and with the rest of the angular includes add in:

@import '../../vendor/bootstrap/less/modals.less';

Try a grunt build, and go to your browser to see if the edit button works, and the save correctly saves to the server.

Other than basic copy paste and typing errors, if things have gone wrong you may get an error on your browser console like “http://localhost:3000/clubs/.json not found”.  This is most likely going to be a problem with your ngResource version, you can tell this version by looking in bower.json to see what version of angular-resource you have.  You may need to force a minimum of version ~1.2 in bower.json.

All going well, you now have something that looks like this:

Tutorial_4 club grid

Tutorial_4 club edit

The ending point for the code for this tutorial can be found on github: tutorial_4.

I’ve created a separate post for the unit testing on this item, which focuses on using karma to test bootstrap modals, because there’s a lot of content in there, and some people might want to skip the testing and keep on with the next post, in which we’ll modify this functionality so that we can do new as well as edit, and we’ll add a delete button.  And perhaps apply the same functionality into the teams area of the application.

Advertisements

16 thoughts on “AngularJS and Rails Tutorial: part 4 ngGrid and edit in a modal popup

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

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

  3. Pingback: CRUD application with AngularJS, Rails, twitter-bootstrap and ng-boilerplate: part 3 List | 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 Tutorial: part 4.5 unit testing bootstrap modals with Karma | technpol

  7. Hi, Thanks for this tuto, it’s perfect !
    However I have an issue, when I press save, the answer is an 500 from the server.
    ActiveModel::ForbiddenAttributesError

    I think this is because the json sended contains the id and the created_at, and updated_at, and those attribute are not permitted in the controller.

    Do you have any idea, to solve this issue ?

    Cheers

  8. So, a couple of possible links to solutions (and you might want to glance at my startings of a Rails 4 tutorial, as I think I’ve gotten mostly this far):
    http://stackoverflow.com/questions/17335329/activemodelforbiddenattributeserror-when-creating-new-user
    http://stackoverflow.com/questions/18679758/activemodelforbiddenattributeserror-rails-4

    The short answer is you need a params.require block I think. If it’s more than this, provide a bit more code detail and I may be able to help more. I am part-way through the Rails 4 tutorial and hope to resolve this as part of that.

  9. Hey, Thanks for your answer, I have figured out the issue.

    In Rails 4, In the controler, I had to use ‘club_params’ which is
    def club_params
    params.require(:club).permit(…..)
    end

    Instead of params(:club).

    Great Tuto,
    Thanks for your answer

    I’m now at the last tuto, and I think, I ready to use Rails and Bakbone thinks to you.

    Cheers Mate

  10. For anyone using a slightly more modern version of ui.bootcamp you’ll note that $dialog is no longer available. The code below does the same thing (hopefully without too many spelling mistakes as I’m not using an IDE):

    .controller(‘VentureCtrl’, [‘$scope’, ‘ClubRes’, ‘$modal’, ‘$log’, function ($scope, ClubRes, $modal, $log) {
    $scope.clubs = ClubsRes.query();
    $scope.gridOptions = {
    data: ‘clubs’,
    columnDefs: [
    {field: ‘id’, displayName: ‘Id’},
    {field: ‘name’, displayName: ‘Club Name’},
    {field: ‘contact_officer’, displayName: ‘Contact Officer’},
    {displayName: ‘Edit’, cellTemplate: ‘Edit ‘}
    ],
    multiSelect: false
    };

    $scope.editVenture = function (club) {
    var modalInstance = $modal.open({
    templateUrl: ‘club/club_edit.tpl.html’,
    controller: ‘ClubEditCtrl’,
    resolve: {
    club: function () {
    return angular.copy(club);
    }
    }
    });

    modalInstance.result.then(function (result) {
    if (result === ‘cancel’) {
    }
    else {
    $scope.clubs = ClubsRes.query();
    }
    });
    };
    }])
    .controller(‘ClubEditCtrl’, [ ‘$scope’, ‘$modalInstance’, ‘club’, function ($scope, $modalInstance, club) {
    $scope.club = club;
    $scope.submit = function () {
    $scope.club.$update(function (data) {
    $modalInstance($scope.club);
    });
    };
    $scope.cancel = function () {
    $modalInstance(‘cancel’);
    };
    }])

  11. There’s a later version of the tutorial for Rails4, and it omits the popup entirely, choosing to navigate between pages. That’s worth a look as well

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

  13. Pingback: twitter bootstrap listview - Search Yours

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