AngularJS and Rails Donuts

Interested in mocking up a modern-looking interface to some data? Using AngularJS to interact with an existing Rails project makes it easy to go from a plain old CRUD app to a modern, surprisingly full-featured UI.

CRUDdy Angular Donuts

For instructive purposes, we’ve spun up a Rails application that displays a variety of information about donuts. You can check out the source on github and the (read-only) demo on heroku.

It's mission-critical that our designers, developers, and project managers have a modern, full-featured "CRUD" UI to interact with the pastry information.

But, our existing Rails app was built using simple scaffold generators and has not received any modernizing TLC in a few years. The donut app is looking a little stale.

Check out the source on github or the (read-only) demo on Heroku to follow along on our delicious journey from plainish to danish.

Crumb-y old Rails Scaffold UI

AngularJS Front-End Setup

Enter AngularJS and UI Grid (née ng-grid). We're also using Restangular, which provides an easy way for our Angular front-end to consume RESTful Rails resources.

Rails Assets

We use Rails Assets to load these libraries into our asset pipeline. Rails Assets converts Bower packages into Bundler gems that slot into your Gemfile.

# Gemfile
source 'https://rails-assets.org' do
  gem 'rails-assets-angular-ui-grid'
  gem 'rails-assets-restangular'
  # Additional rails-assets gems go here...
end

Working with the Rails asset pipeline

We can then load the packages we installed using Rails Assets into the asset pipeline by requiring them in application.js:

// application.js
//= require angular
//= require lodash
//= require angular-ui-grid
//= require restangular

And the corresponding additions in application.css:

/* application.css */
/**
 *= require angular-ui-grid
**/

Now our javascript and css dependencies will be loaded automatically — no extra javascript_include_tags or stylesheet_link_tags required.

Displaying the AngularJS App in the Rails View

The template for our new UI is fairly minimal; we will define donutApp, indexController, and gridOptions later in javascript.

<!-- view/donuts/index.html.erb -->
<div ng-app="donutApp">
  <div ng-controller="indexController">
    <div ui-grid="gridOptions" class="donutGrid">
    </div>
  </div>
</div>

Turbolinks creates problems for our embedded javascript, so we turn it off in the body tag of our application layout.

<!-- layouts/application.html.erb -->
...
<body data-no-turbolink>
...

Alternatively, you can completely remove Turbolinks with a few extra steps.

The Database Schema

Our basic Rails CRUD interface is already up and running, but you can easily replicate it using Rails' scaffold generator:

rails g scaffold donut flavor:string calories:float brand:string shape:integer title:string country:string

And adding null constraints to the generated migration:

# db/migrate/your_migration.rb
class CreateDonuts < ActiveRecord::Migration
  def change
    create_table :donuts do |t|
      t.string  :flavor,   null: false
      t.float   :calories, null: false
      t.string  :brand
      t.integer :shape,    null: false
      t.string  :title,    null: false
      t.string  :country

      t.timestamps null: false
    end
  end
end

You may have noticed that shape is stored as an integer in the database. This is because we are using ActiveRecord's Enum attribute to expose the field as a string to the outside world and attach user friendly helper methods. Internally, it’s represented as an integer for efficiency.

To expose the shape field as an enum, add the following code to the Donut model:

# app/models/donut.rb
class Donut < ActiveRecord::Base
  enum shape: %i{donut filled hole fritter}
end

In our repository, we've included a seed file with predefined donuts if you want to test-drive the code.

Using Restangular to call the API

Restangular gives us a back-end agnostic interface for consuming APIs that conform to RESTful design principles. Fortunately, the controllers created by Rails' generators follow these principles, exposing a fully featured JSON API for our donuts resource that we can use in our shiny new Angular interface.

Restangular calls will return an object with the data from the server and then decorate it with methods that can be used to operate on the resource, a process known as "restangularizing". For example, Restangular.one('donuts', 1) will fetch donut 1 from the server and return an object with its data and methods such as save, which can be called to persist any changes you make back to the server.

Our ui-grid component will look in gridOptions.data for a list of resources to display. You can either load restangularized resources directly into this array and tell ui-grid which ones to display, or manually define which fields get loaded on your own. Since we'll be doing some custom processing of the data later, we'll just extract the fields we're interested in manually for now.

// donuts.js
Restangular.all('donuts').getList().then(function(donuts) {
    $scope.gridOptions.data = _.map(donuts, function(d) {
        return {
          title: d.title,
          flavor: d.flavor,
          ...
        };
    });
});

beautiful new angularjs ui-grid hotness fresh out of the fryolator

Customization

Custom columnDefs

Angular's ui.grid package provides some standard filtering and sorting behavior out of the box. Sorting is enabled by default, and to turn filters on we add enableFiltering: true to $scope.gridOptions.

We apply three types of customizations to the column definitions: links to resources, custom filters, and in-place editing.

We use a customized cellTemplate in the "Title" column to provide a link to an individual donut's "show" page.

// donuts.js
cellTemplate: '<a href="{{row.entity.url}}">{{row.entity.title}}</a>'

Custom filters

We allow users to filter within a range of calories, using "greater than" and "less than" fields.

// donuts.js
...
{
  name: 'calories',
  type: 'number',
  enableCellEdit: true,
  filters: [
    {
      condition: uiGridConstants.filter.GREATER_THAN,
      placeholder: 'Greater Than'
    },
    {
      condition: uiGridConstants.filter.LESS_THAN,
      placeholder: 'Less Than'
    }
  ],
},
...

Custom filtering for those with many donuts

In-place editing

We can easily allow users to edit individual cells in the grid by injecting the ui.grid.edit module into the angular application. Columns are editable by default. The type of input element depends on the type declared in the columnDefs.

Since we've restricted donut shape to a few specific shapes by using a Rails enum, we use a select box for editing the column:

// donuts.js
{
  name: 'shape',
  type: 'string',
  editableCellTemplate: 'ui-grid/dropdownEditor',
  editDropdownValueLabel: 'shape',
  editDropdownOptionsArray: [
  {id: 'donut', shape: 'donut'},
  {id: 'filled', shape: 'filled'},
  {id: 'hole', shape: 'hole'},
  {id: 'fritter', shape: 'fritter'}]
}

We pass a reference to the original "restangularized" object into the Angular version of each donut object so that we can use the reference to update the resource after editing.

Now we simply implement an afterCellEdit callback that PUTs the edited data to the server and it works!

// donuts.js
$scope.gridOptions.onRegisterApi = function(gridApi) {
  $scope.gridApi = gridApi;
  gridApi.edit.on.afterCellEdit($scope, function(rowEntity, colDef, newValue, oldValue) {
    rowEntity.resource[colDef.name] = rowEntity[colDef.name];
    rowEntity.resource.put();
  });
};

Update your donuts so your database doesn't get stale

This approach has some tradeoffs:

Pros: it is nice and RESTful!

Cons: interacting with Rails' CSRF tokens from embedded JavaScript can be difficult.

AngularJS plays well with Rails' CSRF tokens

Luckily there's a Bower package that elegantly handles Rails' CSRF dance!

Just add this to the Gemfile and require it in application.js:

# Gemfile
source 'https://rails-assets.org' do
  ...
  gem 'rails-assets-ng-rails-csrf'
end

And ng-rails-csrf will keep track of the CSRF token and ensure it’s attached to all AJAX requests.

Donut creation

We decided to use a modal dialog to display the form for donut creation. The ngModal plugin made this process straightforward. Once again, all we have to do is drop its rails-assets gem into our gemfile:

# Gemfile
source 'https://rails-assets.org' do
  ...
  gem 'rails-assets-ngModal'
end

And add it to the asset pipeline:

// application.js
...
//= require ngModal
...
/* application.css */
...
*= require ngModal
*= ...

NgModal gives us a modal-dialog component that can be dropped into our template. We'll add a simple form to the model and have it build a new "donut" resource for us using ng-model directives:

<!-- index.html.erb -->
<modal-dialog show='dialogShown' dialog-title='New Donut'>
  <form>
    Title: <input type="text" ng-model="donut.title" />
    ...
    <input type="submit" ng-click="update(donut)" value="Save" />
  </form>
</modal-dialog>

<a href="" ng-click="toggleDialog()">New Donut</a>

Then, we add a few more functions to our controller:

// donuts.js
$scope.dialogShown = false;

$scope.update = function(donut) {
  Restangular.all('donuts').post(donut).then(function(d) {
    $scope.gridOptions.data.push({
       title: d.title,
       ...
    });
    $scope.dialogShown = false;
  });
};

$scope.reset = function() {
  $scope.donut = angular.copy({});
};

$scope.toggleDialog = function() {
  $scope.reset();
  $scope.dialogShown = !$scope.dialogShown;
};

Using $scope.gridOptions.data.push allows us to directly push the data into the ui-grid, so it will update without requiring users to reload the page.

Displaying errors

Since Rails already has validations defined for the Donut model, we'll skip doing client side validation of new donuts and just let the server handle it.

// donuts.js
$scope.error = '';

$scope.update = function(donut) {
  Restangular.all('donuts').post(donut).then(function(d) {
    // success function
  }, function(err) {
    $scope.error = [err.statusText];
  });
};

In the cases where a validation fails, Rails will return a "422 Unprocessable Entity" status code and a list of the failed validations in JSON. We can take advantage of this to display errors in a more user friendly way by slightly modifying our error handling code.

// donuts.js
function(err) {
  if (err.status === 422) {
    $scope.error = _.map(err.data, function(messages, field) {
      return _.capitalize(field) + ' ' + messages.join(', ');
    });
  }
  else {
    $scope.error = [err.statusText];
  }
}

And then update the index template to display errors inside our modal dialog:

<!-- index.html.erb -->
<!-- inside the modal -->
<ul>
  <div ng-repeat="e in error" style="color: red; font-weight: bold;">
    <li>{{e}}</li>
  </div>
</ul>
...

Gotchas

Browser back button caching

A number of popular browsers attempt to cache the last page a user visited so it can be loaded quickly when the back button is pressed. This can sometimes cause trouble when the cached page contains an AJAX heavy javascript app.

In our case, we encountered a few different problems in Chrome and Safari.

In Chrome, navigating to our app using the back button displayed a JSON list of donut resources, caching the JSON data instead of the HTML page. This was caused by Chrome not respecting the standard hypermedia API practice of using an Accept header to access different content types from the same URL; we initially load the "/donuts" page with an Accept: text/html header, then fetch the list of donuts from the same url using an Accept: application/json header (the default behavior for Restangular). Chrome ignored the header change and just cached the result of the last request to "/donuts".

We fixed this bug by having Restangular set a .json suffix for its requests:

// donuts.js
Restangular.setRequestSuffix('.json');

Safari had a similar problem, although the underlying cause wasn't as clear. Fortunately with a little help from Stack Overflow, we found a sufficient workaround.

// donuts.js
window.onunload = function() { }; // Really Safari?  Really?

Setting an initial element in the modal select box

Setting ng-init is not the optimal "Angular" way to do this. But, it's an acceptable work-around for now. It's documented in an issue on the AngularJS github page.

The Filling

We’ve come to the center of our donut saga. What have we learned?

The JSON endpoints generated by the current version of Rails are a huge win for easy APIs. However, Rails can be a bit inconsistent in its adherence to RESTful principles, such as not returning the URL to the resource upon a PUT request.

Using Rails Assets for embedding AngularJS in a Rails application is super smooth. We will definitely use this in future projects.

Homework Problems

We’ve restricted our scope here to the basics of getting AngularJS to interact with an existing Rails API, so there’s a lot we haven’t covered. Here are some directions you could take to further develop your understanding:

  • D is for Donuts in our book, but you can implement the other commonly used D in CRUD (deletion).
  • We left our Angular code in one monolithic file (donuts.js) for simplicity's sake. Refactor the code to use a more idiomatic Angular style.
  • In-place editing does not display errors. Refactor our code so the main page shows errors from the PUT when an in-place edit results in an invalid donut.
  • Extra credit: refactor the shape editDropdownOptionsArray to use the data from the Rails Donut.shape enum. This would improve our code by making sure that the AngularJS front-end and the Rails back-end are always consistent.

Bendyworks can Angular-ize your Rails app!

Is there a hole in your developer life? Does your Rails app look a little crumb-y? Bendyworks is happy to rise to the challenge. Throw some dough our way and we can help.