AngularJS and Rails Tutorial: part 6 the team entity, grid filtering, links between pages

Part 6 of a tutorial that connects AngularJS to a Rails backend. This post focuses on updating our modal form to support the “new” function, adding a delete button to our ngGrid, and adding error handling in case our rails application rejects our updates. We also add filters to our grid and automatically populate the team filter when we navigate from a specific club.  The previous post was New and Delete, the next post is Tidyup.  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.

If you haven’t followed the earlier tutorials, the code can be found on github: tutorial 5.

At the end of this post you’ll expect to have updated the app to look like this:

Clubs offer a link to related teams:

Tutorial_6 clubs with team link

Teams has filtering based on that link:

Tutorial_6 teams with filter

New team defaults based on filter:

Tutorial_6 teams with default in dropdown

You’ll recall back in the first post we created a teams entity at the same time as we created the clubs entity.  The definition for this entity in rails looked like:

rails generate scaffold team club:references name:string captain:string date_created:datetime

This entity has a handful of fields on it:

  • club_id
  • name
  • captain
  • date_created

Initially we’d like to create a team entity in angular that is like the club entity, that is it is standalone.  We can do this by copying the entire club directory, and renaming everything in it from club to team, both the filenames, and everything inside the files.  When running search and replace, make sure to be careful on capitalisation – Club->Team, club->team.  Things to watch out for are:

    1. You’ll also need to change the contact_officer field in the html templates to be captain – teams have a captain, clubs have a contact_officer
    2. You’ll need to update the src/less/main.less to include team.less.  Strictly speaking we’re using the same styles at the moment, so once we tidy up the app we’ll aim to put all the less into a more sensible structure with a set of application-wide styles, and only styles specific to a particular entity turning up in that entity’s less
    3. You need to change app.js to include your new module – league.team
    4. You need to change index.html to include a block for teams.  I deleted the github block, and created a new block after club similar to the one we already included for clubs
                <li ui-route="/team" ng-class="{active:$uiRoute}">
                   <a href="#/team">
                     <i class="icon-github-alt"></i>
                     Teams
                   </a>
                 </li>
    1. The unit test will mostly work once you search and replace club/team, but remember the JSON response on a server query needs to be updated (I got mine by just querying the rails server directly and looking at the response)
        scope.httpBackend.expect('GET', '../teams.json').respond([
          {"captain":"Captain 1","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":1,"name":"Team 1","updated_at":"2012-03-03T00:00:00Z"},
          {"captain":"Captain 2","created_at":"2012-02-02T00:00:00Z","date_created":"2012-01-01T00:00:00Z","id":2,"name":"Team 2","updated_at":"2012-03-03T00:00:00Z"}
        ]);

Run grunt build and check that you now have a teams section of your app that behaves a lot like the clubs section did.

Next, we’d like to have our application create a link between clubs and teams.  The way we’re going to do this initially is to use a drop-down on the teams modal to allow you to select one of the clubs.

First, we need access to a list of clubs within our edit team controller.  We’re going to do that by using the ClubRes resource that we already defined in the clubs entity.  In our teams.js we make the following changes.  First, modify the module definition at the very top of your team.js file to include league.club as a dependency:

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

Next, modify the edit controller definition to include the TeamRes as a dependency,:

.controller('TeamEditCtrl', function TeamEditController($scope, TeamRes, ClubRes, dialog, team, isNew) {

Finally, when we enter the edit controller, we want to go and get the list of clubs from the ClubRes:

  $scope.team = team;
  $scope.clubs = ClubRes.query();

Then we need to do the html template updates. We’re going to follow the advice we found from IEnableMuch, which suggests a tidy way to implement this. In the team_edit.tpl.html add in club as a editable field. We’re using a select box, refer to the Angular documentation for information on settings you can use with a select dropdown.

  <p>Captain: <input ng-model="team.captain" /></p>
  <p>Club: <select ng-model="team.club_id" ng-options="club.id as club.name for club in clubs"></select></p>
</div>

Run grunt build, and see if you get a select dropdown working properly. You’ll probably get a unit test failure, as we have a new http request that the unit test isn’t expecting, so update the edit controller portion of our unit test to expect this query to happen:

    beforeEach(angular.mock.inject(function($controller){
      // setup a mock for the isNew flag
      scope.isNew = false;

      //declare the controller and inject our parameters
      $controller('TeamEditCtrl', {$scope: scope, dialog: scope.fakeDialog, team: scope.fakeTeam, isNew: scope.isNew});

      // edit controller gets a list of clubs to populate dropdown
      scope.httpBackend.expect('GET', '../clubs.json').respond([]);
      scope.$digest();
      scope.httpBackend.flush();        
    }));

This should look broadly as follows:

Tutorial_6 dropdown

What you will probably find is that you’re getting a dropdown that defaults properly and lets you select values, but doesn’t save any changes.  This is because we haven’t set club_id as an accessible field in our rails model, so go to your rails project and edit app/models/team.rb, and change the attributes accessible:

  attr_accessible :captain, :date_created, :name, :club_id

Try again, you should find your selected club being saved.

Next, we’d like a way to come from an individual club to see all the teams for the club.  There are a lot of ways to put this together, but for now we’re going to put a “show teams” button on the clubs list.  This will take us to the teams list view, and show the teams for just that club through using a filter.  We want that filter to be pushed all the way down to the server, and processed on the server.

First, we’re going to add the “show teams” button to the clubs controller.  Edit src/app/club/clubs.js to add the following code to the gridOptions configuration:

    {displayName: 'Delete', cellTemplate: '<button id="deleteBtn" type="button" class="btn btn-primary" ng-click="deleteClub(row.entity)" >Delete</button> '},
    {displayName: 'Show Teams', cellTemplate: '<button id="showBtn" type="button" class="btn btn-primary" ng-click="showTeams(row.entity)" >Show Teams</button> '}
  ],

Then we’re going to create the showTeams method on the controller, at the moment this just takes us to the teams page, it doesn’t tell teams that we want to see only a specific club:

  $scope.showTeams = function(club) {
    $location.path("/team");
  };

We also need to update our controller to have a dependency on $location as follows:

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

Run grunt build and see if this method drives us to the teams page (albeit it isn’t showing us the clubs for our team yet).

Next, we want to add a filter capability to the teams list. The ngGrid documentation describes a filter function, but that is basically a search box.  We want something that is more a set of selections per column – sort of like a query builder.  From this page I can see at very bottom a plunker that creates a standalone filter box, and in that filter box we can put column names with values.  So, for example, we could put “name:John” and it will filter on the column name for all Johns.  We’re going to have a go at using something like that for now – although later we would prefer to shift this to the server side.

First, add a filter box to src/app/team/team.tpl.html, this is a text box that is bound to the “filterOptions.filterText” model variable:

<div class="body">
  <strong>Filter:</strong><input type="text" ng-model="filterOptions.filterText" />
  <div class="gridStyle" ng-grid="gridOptions"></div>
  <button ng-click="newTeam()" class="btn btn-primary" >New Team</button>
</div>

Then, add a binding to the grid filtering using the filterOptions.  There are two additions here – we create a filterOptions variable right up front so the control has something to bind to, then we use that in the gridOptions configuration:

.controller( 'TeamCtrl', function TeamController( $scope, TeamRes, $dialog ) {
  $scope.teams = TeamRes.query();
  $scope.filterOptions = {
    filterText: ''
  };
  $scope.gridOptions = {
    data: 'teams',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Team Name'},
      {field: 'captain', displayName: 'Captain'},
      {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" class="btn btn-primary" ng-click="editTeam(row.entity)" >Edit</button> '},
      {displayName: 'Delete', cellTemplate: '<button id="deleteBtn" type="button" class="btn btn-primary" ng-click="deleteTeam(row.entity)" >Delete</button> '}
    ],
    multiSelect: false,
    filterOptions: $scope.filterOptions
  };

Run grunt build to see how we’re going on the teams page. This should now allow filtering on the teams page, for example you can put “captain:John” into the filter box, it should show just captains with name John.

Our next extension is that we haven’t shown the club names or ids in the list, and while we’re at it we’d like to show and format the create date. Start back in the rails app/controllers/teams_controller.rb – we want to pull back the club name when we bring back the list of teams:

  # GET /teams.json
  def index
    @teams = Team.joins('LEFT OUTER JOIN clubs on teams.club_id = clubs.id').select('teams.*, clubs.name as club_name')

    render json: @teams
  end

Call the rails service directly to verify the json is now showing the club_name in the result (http://localhost:3000/teams.json). We then edit our grid options in teams.js to show these extra columns, and to add some extra features to our grid, namely the ability for a user to decide which columns to display (a dropdown top right of the grid), and the ability to group by one or more columns.

I note that dragging and dropping columns into the grouping isn’t yet working for me, I think this is because of not (yet) setting the app to be html5, but the default columns do work:

  $scope.gridOptions = {
    data: 'teams',
    columnDefs: [
      {field: 'id', displayName: 'Id'},
      {field: 'name', displayName: 'Team Name'},
      {field: 'captain', displayName: 'Captain'},
      {field: 'club_id', displayName: 'Club Id', groupable: true, visible: false},
      {field: 'club_name', displayName: 'Club Name', groupable: true},
      {field: 'date_created', displayName: 'Date Created'},
      {displayName: 'Edit', cellTemplate: '<button id="editBtn" type="button" class="btn btn-primary" ng-click="editTeam(row.entity)" >Edit</button> '},
      {displayName: 'Delete', cellTemplate: '<button id="deleteBtn" type="button" class="btn btn-primary" ng-click="deleteTeam(row.entity)" >Delete</button> '}
    ],
    multiSelect: false,
    filterOptions: $scope.filterOptions,
    showColumnMenu: true,
    showGroupPanel: true,
    groups: ["club_name"]
  };

Run grunt build again, and see whether this now shows the additional columns.

Next, we don’t like the date formatting all that much, so we’re going to use a filter to change the date to a prettier format:

      {field: 'date_created', displayName: 'Date Created', cellFilter: "date:mediumDate"},

Run grunt build and check the date formatting.

We want to allow the club id to be a parameter on the URL, and to then put that parameter into the filter box automatically.  We can get at parameters that are passed in to us by using the $stateParams, which is part of the $stateProvider module. This replaces the standard $routeProvider module, and is the magic that allows us to move between the pages of our application. We tell our team controller that we have a dependency on $stateParams:

.controller( 'TeamCtrl', function TeamController( $scope, TeamRes, $dialog, $stateParams ) {

And we tell our state setup that we are able to accept a parameter on the URL, and to please put that into the $stateParams for us:

.config(function config( $stateProvider ) {
  $stateProvider.state( 'team', {
    url: '/team?club_id',
    views: {
      "main": {
        controller: 'TeamCtrl',
        templateUrl: 'team/team.tpl.html'
      }
    },
    data:{ pageTitle: 'Team' }
  });
})

And in the controller changes we need to make use of the club_id that we then get:

  $scope.teams = TeamRes.query();

  $scope.club_id = $stateParams.club_id;

  if($scope.club_id) {
    $scope.filterOptions = {
      filterText: 'club_id:' + $scope.club_id
    };
  }
  else {
    $scope.filterOptions = {
      filterText: ''
    };
  }

Run grunt build and try modifying the URL on the teams page to provide a club_id, and see it update the filter: http://localhost:3000/teams?club_id=1, change it to http://localhost:3000/teams?club_id=2 and see the filter change.

Modify the club to pass in the id on the url, finishing this part of the integration:

  $scope.showTeams = function(club) {
    $location.path("/team").search({club_id: club.id});
  };

Run grunt build, and check that using the “show teams” button on the club page takes you to a filtered list of the teams on the team page.

Finally, when a filter is set on the teams page, we want to apply that filter as a default value when we create a new team. To do this, we’re going to set the club_id in the newTeam method on the controller if there is a club_id in the controller.

  $scope.newTeam = function() {
    $scope.myDialog = $dialog.dialog({dialogFade: false, resolve: {team: function(){
                                                                     var team = new TeamRes();
                                                                     if ($scope.club_id) {
                                                                       team.club_id = $scope.club_id;
                                                                     }
                                                                     return team; 
                                                                   }, 
                                                                   isNew: function() {return true;}}});
    $scope.myDialog.open('team/team_edit.tpl.html', 'TeamEditCtrl').then(function(result){

This won’t fully work however, because club_id needs to be an integer, and the club_id we have came from the URL, so it’s a string. So we also update our controller to convert to an integer first:

.controller( 'TeamCtrl', function TeamController( $scope, TeamRes, $dialog, $stateParams ) {
  $scope.teams = TeamRes.query();

  $scope.club_id = parseInt($stateParams.club_id, 10);

And that should do it – when we create a new team it should use the current club if we passed one in on the URL.

You should have an application that has a teams UI, and linkages from the clubs to the teams including a dropdown.  It should default the filtered teams into a new team created.  It should broadly look like:

Tutorial_6 clubs with team link

Tutorial_6 teams with filter

Tutorial_6 teams with default in dropdown

The code for this post in the tutorial series can be found at github: tutorial 6.

The next post in the series focuses on better error handling, adding a datepicker, and general tidyup.

Advertisements

5 thoughts on “AngularJS and Rails Tutorial: part 6 the team entity, grid filtering, links between pages

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

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

  3. Pingback: CRUD application with AngularJS, Rails, twitter-bootstrap and ng-boilerplate: part 7 tidyup | technpol

  4. 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