Angular with Rails, Part III

 

angular-ruby-on-rails-logo
In Part 2 of Angular with Rails series, we covered creating a Rails API with tests in RSpec and setting up an AngularJS app that reads from the API.

In Part 3 we will cover:

  • Adding CSS
  • Adding editing functionality
  • Creating a custom filter
  • Setting up JavaScript tests

Setup Bootstrap Sass

In order to support editing functionality we’ll add some CSS to the UI. Following the README at bootstrap-sass, we’ll add to our Gemfile:

gem 'bootstrap-sass', '~> 3.3.0' 

Add to our app/assets/javascripts/application.js:

//= require bootstrap-sprockets 

Rename app/assets/stylesheets/application.css to application.css.sass (if you prefer sass over scss) and add:

@import "bootstrap-sprockets" @import "bootstrap" 

Then we’ll add some classes to our employee index in app/views/employees/index.html.erb:

<div ng-app='app.employeeApp' ng-controller='EmployeeListCtrl' class="col-xs-8">   <h1>Employees List</h1>   <table ng-if="employees" class="table table-hover table-striped">     <thead>       <th>Name</th>       <th>Email</th>       <th>SSN</th>       <th>Salary</th>     </thead>     <tbody>       <tr ng-repeat="employee in employees">         <td>{{employee.name}}</td>         <td>{{employee.email}}</td>         <td>{{employee.ssn}}</td>         <td>{{employee.salary | currency}}</td>       </tr>     </tbody>   </table> </div> 

Add the Edit API

Next, we’ll update our Rails API to support editing of employee records. Let’s start by adding some tests for the update method in spec/controllers/api/employees_controller_spec.rb:

require 'spec_helper'  describe Api::EmployeesController do   before(:each) do     @employee = create(:employee, name: 'Calhoun Tubbs', salary: 50000)   end    describe '#index' do     it 'should return a json array of users' do       get :index       result = JSON.parse(response.body)        expect(result[0]['name']).to eq('Calhoun Tubbs')     end   end    describe "#update" do     it 'should successfully respond to edits' do       put :update, id: @employee.id, employee: {             id: @employee.id,             salary: 60000           }        expect(response).to be_success     end      it "should change the employee's salary" do       @employee.update_attribute(:salary, 50000)        put :update, id: @employee.id, employee: {             id: @employee.id,             salary: 60000           }        expect(@employee.reload.salary).to eq(60000)     end   end end 

At this point, these tests will fail. We need to create an update method in our API controller, but first we must specify what params are allowed on update (Rails’ strong parameters) at app/controllers/api/employees_controller.rb:

class Api::EmployeesController < ApplicationController # section omitted   private    def employee_params     attributes = [       :salary,       :name,       :email,       :ssn     ]      params.require(:employee).permit(attributes)   end end 

Now, our update action is simple. The code will try to update the employee object and render the errors if there’s a problem.

class Api::EmployeesController < ApplicationController   # section omitted   def update     employee = Employee.find(params[:id])      if employee.update(employee_params)       render json: employee     else       render json: employee.errors.messages, status: :bad_request     end   end   # section omitted end 

Next run the tests to make sure they pass. For a more complex application there would be more tests, but in our example these will suffice.

Add the Edit Front End

For our employee edit functionality we’ll define a modal dialog containing a form and a submit button. The Angular-bootstrap package contains a modal dialog tool. We’ll add the angular-bootstrap package to our bower.json:

{   "lib": {     "name": "bower-rails generated lib assets",     "dependencies": {       "angular": "v1.2.25",       "restangular": "v1.4.0",       "angular-bootstrap": "v0.11.0"     }   },   "vendor": {     "name": "bower-rails generated vendor assets",     "dependencies": {     }   } } 

And bundle exec rake bower:install

Next, require angular-bootstrap in app/assets/javascripts/application.js:

//= require jquery //= require jquery_ujs //= require angular //= require angular-rails-templates //= require lodash //= require restangular  //= require angular-bootstrap //= require bootstrap-sprockets  //= require angular-app/app // rest of the file omitted 

Finally, we’ll add it as a dependency to our angular app in app/assets/javascripts/angular-app/modules/employee.js.coffee.erb:

@employeeApp = angular   .module('app.employeeApp', [     'restangular',     'ui.bootstrap'     'templates'   ])   .run(->     console.log 'employeeApp running'   ) 

Now, we need to add a modal controller to our AngularJS app. This controller will handle popping up a modal editable form and submitting it to our Rails API. In app/assets/javascripts/angular-app/controllers/employee/EmployeeListCtrl.js.coffee:

angular.module('app.employeeApp').controller("EmployeeListCtrl", [   '$scope', 'EmployeeService', '$modal',   ($scope, EmployeeService, $modal)->      $scope.editEmployee = (employee) ->       modalInstance = $modal.open({         templateUrl: 'employee/edit.html',         controller: 'EmployeeEditModalCtrl'         size: 'lg'         resolve:           employee: ->             employee       })        modalInstance.result.then(->         console.log 'edit closed'       ) # rest of file omitted 

Next, we’ll create the modal controller at app/assets/javascripts/angular-app/controllers/employee/EmployeeEditModalCtrl.js.coffee:

angular.module('app.employeeApp').controller('EmployeeEditModalCtrl', [   '$scope', '$modalInstance', 'employee', ($scope, $modalInstance, employee)->     $scope.submitEmployee = ->       console.log 'submit employee'      $scope.cancel = ->       $modalInstance.dismiss('cancel')  ]) 

In the above code we’re defining a modal controller that receives the employee object we passed in the prior controller, along with function stubs for the save / cancel buttons in the dialog. Next, we need to define the edit form template in app/assets/javascripts/angular-app/templates/employee/edit.html.erb:

<div class="modal-header">   <h4 class="modal-title">Edit Employee</h4> </div>  <div class="modal-body">   <form name="" ng-submit="submitEmployee(employee)">     <div class="form-group">       <div class="row">         <div class="col-xs-12">           <label>Name<span class="required"> required</span></label>           <input class="form-control" ng-model="employee.name">         </div>       </div>     </div>      <div class="form-group">       <div class="row">         <div class="col-xs-12">           <label>Email<span class="required"> required</span></label>           <input class="form-control" ng-model="employee.email">         </div>       </div>     </div>      <div class="form-group">       <div class="row">         <div class="col-xs-12">           <label>SSN<span class="required"> required</span></label>           <input class="form-control" ng-model="employee.ssn">         </div>       </div>     </div>      <div class="form-group">       <div class="row">         <div class="col-xs-12">           <label>Salary<span class="required"> required</span></label>           <input class="form-control" ng-model="employee.salary">         </div>       </div>     </div>      <div class="modal-footer">       <button type="button" class="btn btn-default" data-dismiss="modal" ng-click="cancel()">Cancel</button>       <button type="submit" class="btn btn-primary">Save Changes</button>     </div>   </form> </div> 

Finally, if you run the code now you’ll get an error loading the template. We need to tell our angular-rails-templates gem what the base path for our templates is. This is necessary because we are using the rails asset pipeline to combine our javascript files. In config/application.rb:

require File.expand_path('../boot', __FILE__) require 'rails/all'  Bundler.require(*Rails.groups)  module AngularExample   class Application < Rails::Application     # the path relative to app/assets/javascripts     config.angular_templates.ignore_prefix  = 'angular-app/templates/'   end end 

Next, we can return to our employees index and provide an ng-click attribute to launch the edit form. We’ll add this link on the employee name cell in the table in app/views/employees/index.html.erb:

# rest of the file omitted <td><a ng-click="editEmployee(employee)">{{employee.name}}</a></td> 

Now, clicking an employee’s name will launch a modal form with fields containing that employee’s data. However, we still need to hook up the Save Changes button to our Rails API. In app/assets/javascripts/angular-app/controllers/employee/EmployeeEditModalCtrl.js.coffee:

angular.module('app.employeeApp').controller('EmployeeEditModalCtrl', [   '$scope', '$modalInstance', 'employee', ($scope, $modalInstance, employee)->     $scope.employee = employee      $scope.submitEmployee = (employee)->       employee.put().then(         $modalInstance.close('saved')       )     # section omitted ]) 

We can simply say put() above because we are using Restangular to manage that object.

One problem with the above code is that while the server is applying validation to the object, the client side AngularJS portion is not. One downside of using client-side MVC is the fact that validation from the server must often be duplicated on the client. The subject of AngularJS form validation could be a blog post on its own. I’ll leave that as an exercise for the reader.

Secure the UI

Though the editing functionality now works, notice the page still shows each employee’s social security number. In order to mask the SSN and only reveal it in edit form, we can create an AngularJS filter. In app/assets/javascripts/angular-app/filters/employee/empMaskNumber.js.coffee

angular.module('app.employeeApp').filter('empMaskNumber', ()->   (value, nonMaskLength) ->     if value?       maskLength = value.length - nonMaskLength - 1       if maskLength > 0         v = value.split("")         for i in [0..maskLength] by 1           if v[i] != '-'             v[i] = '*'         v = v.join('')       else         value     else       value ) 

It’s recommended that you name filters with an app prefix to avoid colliding with AngularJS’s built-in filters. Let’s edit our employee table to make use of the filter in app/views/employees/index.html.erb:

# section omitted <tr ng-repeat="employee in employees">   <td><a ng-click="editEmployee(employee)">{{employee.name}}</a></td>   <td>{{employee.email}}</td>   <td>{{employee.ssn | empMaskNumber: 4 }}</td>   <td>{{employee.salary | currency}}</td> </tr> # section omitted 

Viewing the employee table again you should see the SSN field being masked with asterisks except the last 4 digits. While this filter is simple and apparently works, when building more complex filters we will need javascript tests.

Setup JavaScript Tests

Next, we’ll setup jasmine-rails. Add jasmine-rails to the Gemfile:

group :test, :development do   gem 'jasmine-rails' end 

And add angular-mocks to the bower.json:

{   "lib": {     "name": "bower-rails generated lib assets",     "dependencies": {       "angular": "v1.2.25",       "restangular": "v1.4.0",       "angular-bootstrap": "v0.11.0",       "angular-mocks": "v1.3.2"     }   },   "vendor": {     "name": "bower-rails generated vendor assets",     "dependencies": {     }   } } 

Then run:

bundle install rails generate jasmine_rails:install bundle exec rake bower:install 

Next, we need to setup our spec_helper. In app/assets/javascripts/spec_helper.coffee:

#= require application #= require angular-mocks #= require sinon  beforeEach(module('app', 'app.employeeApp'))  beforeEach inject (_$httpBackend_, _$compile_, $rootScope, $controller, $location, $injector, $timeout, $filter) ->   @scope = $rootScope.$new()   @compile = _$compile_   @location = $location   @controller = $controller   @injector = $injector   @http = @injector.get('$httpBackend')   @timeout = $timeout   @model = (name) =>     @injector.get(name)   @filter = $filter   @eventLoop =     flush: =>       @scope.$digest()  afterEach ->   @http.resetExpectations()   @http.verifyNoOutstandingExpectation() 

Note the beforeEach line above. As your application grows and more angular apps are added, they must also be added here to be available for testing. The above spec helper also requires SinonJS. Download the file and place it at vendor/assets/javascripts/sinon.js .

Now you should be able to run an empty test suite:

RAILS_ENV=test bundle exec rake spec:javascript 

Which should return 0 specs and 0 failures. We are now ready to write our test for our filter defined above. In spec/javascripts/filters/emp_mask_number_spec.js.coffee:

#= require spec_helper  describe 'Filters', ->   describe 'EmpMaskNumber', ->     beforeEach ->       @maskFilter = @filter('empMaskNumber')      it 'should mask the value with asterisks', ->       @value = "123"       @nonMaskLength = 0        expect(@maskFilter(@value, @nonMaskLength)).toEqual "***" 

In a real application expect to have more specs than just the above. At this point though, you have everything you need to continue building your application and expanding. You can find the code for this example on Github.

Need to catch up on the Angular with Rails series? Check these out:

  • Angular with Rails Part I
  • Angular with Rails, Part II

Resources: