« Home

Getting AngularJS and Rails talking

In this post, I want to explore a few of the ways to exchange data between a Rails app and an AngularJS module—looking at the pros and cons of each approach. While Angular sees a lot of usage in single-page applications (SPAs), I’ve found it just as useful for enhancing certain pages of a Rails application with more dynamic interfaces, while sticking to plain, scaffoldable CRUD screens for the rest of the app.

With this approach, it doesn’t make sense to kick HAML to the curb, turn your Rails app into a JSON API, and render everything on the client side. Rails may not be the new kid on the block anymore, but it has a lot of mature gems that make putting together a complex site a breeze.

With that in mind, it’s not always clear how best to combine the two technologies:

JSON endpoints in the Rails controller

The first approach is to provide a JSON format for the resource in your controller. Imagine we wanted to implement client-side filtering of a short list of items in a store’s inventory. We can easily add a JSON format to the index action:

Then from our HAML template, we’ll provide the Angular templating to dynamically render the items:

# app/views/items/index.html.haml
%input(type="text" ng-model="search")

%table(ng-controller="ItemsController")
  %thead
    %tr
      %th Name
      %th Price
  %tbody
    %tr(ng-repeat="item in items | filter : search")
      %td {{ item.name }}
      %td {{ item.price | currency }}

Lastly, we’ll initialize the scope’s items array by requesting the data as JSON from the Rails server using the $http service.

I like this approach, but it has a few issues:

ngInit (and ngInitial)

When building forms, it’s common to want to show or hide an option or when something else is selected. Say our inventory app collects some basic information about how the item is taxed when an item is added. If the item is marked as taxable, the “tax rate” field should appear. If it is not taxable, the field should be hidden.

The is definitely a job for Angular, but I’m not willing to give up simple_form in the process. Let’s see if we can make them play together.

The item form partial will be used for both the new and edit actions. The show/hide behavior must work for both a new item as well as for an existing item that has already been marked as taxable.

The ngInit directive evaluates an expression in the current scope. After assigning the taxable checkbox to the $scope.taxable via ng-model, we can use ng-init it to set its initial value:

# app/views/items/_form.html.haml
%div(ng-controller="ItemFormController")
  = simple_form_for @item do |f|
    = f.input :taxable, 'ng-model' => 'taxable', 'ng-init' => "taxable = #{@item.taxable.to_json}"
    = f.input :tax_rate, 'ng-if' => 'taxable'

Then from the Angular controller, there’s not much left to do:

While this a decent strategy for simple use cases like above, it’s easy to abuse and mixing Ruby and Angular templates can get ugly. A slightly cleaner approach is to use something like the ngInitial directive from this StackOverflow answer. This directive sets the initial value of the $scope variable using the value attribute, which will be set automatically by the form helper. I’ve slightly modified the directive below to also handle checkbox inputs:

It can then be used in the above HAML view like so:

# app/views/items/_form.html.haml
%div(ng-controller="ItemFormController")
  = simple_form_for @item do |f|
    = f.input :taxable, input_html: { 'ng-model' => 'taxable', 'ng-initial' => '' }
    = f.input :tax_rate, input_html: { 'ng-if' => 'taxable' }

(Unfortunately, I don’t know of a cleaner way to define valueless attributes.)

Using script tags

When mixing Backbone.js and Rails, it’s common to bootstrap Backbone models and collections from a script tag at the bottom of the page. It’s possible to do something similar in Angular by putting the data somewhere in the global scope and accessing it from the Angular controller.

# app/views/items/index.html.haml
:javascript
  window.items = #{@items.to_json};

While this is a bit simpler than requesting the data with the $http service and does work quite well, using global variables means the angular app loses its nice encapsulation, which can make testing problematic. Global variables also of course come with problems of their own.

JSON data attributes

Another approach is to render the data as JSON to data attributes. By adding a data-items attribute to the element with the ng-controller attribute, you can easily access the data with an injected $element dependency.

# app/views/items/index.html.haml
%ul{ 'ng-controller' => 'ItemsController', 'data-items' => items.to_json }
  %li(ng-repeat="item in items") {{ item.name }}

This is my favorite approach in cases where a JSON endpoint doesn’t make sense. The $element dependency can be mocked, so the code is easily-testable. It doesn’t rely on global variables, so the app is still nicely encapsulated. On top of that, we also avoid making another request.