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.

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:

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.

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.

Links to resources

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

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:

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:

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.

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.jsRestangular.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.jswindow.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.