4 Comments

4 Ways to Pass Rails Data to AngularJS

I’ve recently been working on a simple Ruby on Rails web app that uses AngularJS on the front end. Angular provides us with a number of nice client-side features (two-way data binding, form validations, easy to use UI widgets, etc.), but we chose to use Rails for routing the entire site. Therefore, the Angular app is embedded within a rendered Rails view. This choice raised a practical question: how do we expose the Rails data (instance variables, params, etc.) to the Angular app’s controller(s)?

I’ve seen many developers asking how to access Rails params and instance variables within their own Angular controller code (often to make calls to $resource and $http for their CRUD operations). The purpose of this blog post is to describe four approaches I found to solve this common question.

In these examples, I have a single Rails model: Item. Each item has a name, a cost, and an id value. Our Rails controller consists of the following code, which creates an instance variable for the currently shown item, @item, that is accessible in our view:

class ItemsController < ApplicationController
  def show
    @item = Item.find(params[:id])
  end
end

All HTML code below is contained in view.html.erb, while javascript is held in app/assets/javascripts/items.js.

Option 1: Parse the URL via $location

Your Angular controller can be injected with the $location service. From there, it is possible to simply parse the current URL (for example, www.example.com/items/2) to get the id of the resource you wish to GET, PATCH, DELETE, etc.

<div class="container" ng-app="itemsApp">
  <div ng-controller="itemsParseIdController">
    <h3>id is {{id}}</h3>
  </div>
</div>

The Angular controller pulls the id out of the url using a regexp:

var itemsApp = angular.module('itemsApp', []);
 
function itemsParseIdController($scope, $location) {
  $scope.id=(/items\/(\d+)/.exec($location.absUrl())[1]);
}

This approach will work for getting the id of the resource. However, it’s ugly; and in this canned example, the name and cost of an item are not exposed in the URL.

Option 2: Hidden divs Containing HTML5 Custom Data Attributes

In this technique, we create a div that contains the custom attributes within our ng-controller scope.

<div ng-controller="itemsControllerHiddenDiv">
  <div id="div-item-data" data-item-name="<%= @item.name %>"
       data-item-cost="<%= @item.cost %>" data-item-id="<%= @item.id %>"></div>
  <h2> {{item.name}} ({{item.id}}) costs {{item.cost | currency}}  {{test}} </h2>
</div>

And the controller calls getElementById() and getAttribute() to set the $scope variables.

function itemsControllerHiddenDiv ($scope) {
    var div = document.getElementById('div-item-data');
 
    $scope.item = {name: div.getAttribute("data-item-name"),
                   cost: div.getAttribute("data-item-cost"),
                   id:   div.getAttribute("data-item-id")
    };
}

This approach doesn’t take advantage of any particular Angular feature.

Option 3: Use ng-init to Evaluate an Initialization Method

We can define an initialization method using ng-init in the same element as our controller:

<div ng-controller="itemsControllerNgInit" 
     ng-init="init('<%= @item.name %>', <%= @item.cost %>, <%= @item.id %>)">
  <h2> {{item.name}} ({{item.id}}) costs {{item.cost | currency}}  {{test}} </h2>
</div>

This will evaluate the ng-init expression in Angular’s pre-link function (in this case, we call a method init()). Beware: there is a caveat with this approach. Because ng-init shares an element with ng-controller, the controller will actually be instantiated before the ng-init expression is evaluated! I found two ways of handling this issue:

1. Use the Initialized Values Inside the init Function:

function itemsControllerNgInit ($scope) {
  $scope.init = function(name, cost, id)
  {
    $scope.item = {name: name,
                   cost: cost,
                   id:   id
    };
    logSomeStuff("2: ");
  }
 
  var logSomeStuff = function(call)
  {
    console.log(call + $scope.item);
  }
 
  logSomeStuff("1: ");
}
<p>

The above code will print out these lines to the console, demonstrating that $scope.item is undefined when the controller is initialized:

1: undefined
2: [object Object] 

2: Wrap it in $evalAsync():

Wrap code that depends on ng-init in $scope.$evalAsync(), which forces it to run on the next $digest() phase:

function itemsControllerNgInit ($scope) {
  $scope.init = function(name, cost, id)
  {
    $scope.item = {name: name,
                   cost: cost,
                   id:   id
    };
  }
 
  var logSomeStuff = function()
  {
    console.log($scope.item);
  }
 
  $scope.$evalAsync(logSomeStuff);
}

Option 4: Use an Angular Service to Provide the Data & Inject It

Taking advantage of an Angular service defined within our app allows us to selectively inject it into the controllers that need it.

<div class="container" ng-app="itemsApp">
  <script>
    <%= render "items_service.js.erb" %>
  </script>
 
  <div ng-controller="itemsController">   
    <h1> {{item.name}} ({{item.id}}) costs {{item.cost | currency}}  {{test}} </h1>
  </div>
 
  <div ng-controller="otherController">
    <h2> {{item.name}} ({{item.id}}) costs {{item.cost | currency}}  {{test}} </h2>
  </div>
</div>

In this example we have two controllers, each in the itemsApp. We could put the service definition within the script tags, but I feel it is cleaner to render it as a partial — the line <%= render "items_service.js.erb" %> accomplishes this — and move the service code out into a separate file (app/views/items/_items_service.js.erb):

itemsApp.service('itemsAppInitializer', function(){
  return <%= @item.to_json.html_safe %>;
});

The Angular controllers (in app/assets/javascripts/items.js) can then be injected with the service to get the Rails data:

function reverse(str) {
  return str.split("").reverse().join("");
}
 
function adjustedCost(cost) {
  return 1.2*cost;
}
 
function otherController ($scope, initializer) {
  $scope.item = {name: reverse(initializer.name),
                 cost: adjustedCost(initializer.cost),
                 id:   initializer.id  
  };
}
 
function itemsController ($scope, initializer) {
  $scope.item = initializer;
}
 
otherController.$inject = ['$scope', 'itemsAppInitializer'];
itemsController.$inject = ['$scope', 'itemsAppInitializer'];

In the above code, we tell Angular to inject the controllers with the $scope and itemsAppInitalizer services via $inject = []. Angular also supports implicit dependency injection if the names of the parameters in the controller function definition match the names of the services:

function itemsController ($scope, itemsAppInitializer) {
  $scope.item = initializer;
}

See this Angular Dev Guide entry for more details on dependency injection.

Conclusion

Of the four, the service injection approach feels most idiomatic to Angular. It is DRY — we create the service once, the code for the controller can be separated out into its own file in the form of a Rails partial, and it takes advantage of the Angular service dependency injection mechanism. Of course, any of the four solutions described above will solve the problem, but it is up to the developer to choose which is best for their application.