Article summary
- Option 1: Parse the URL via $location
- Option 2: Hidden divs Containing HTML5 Custom Data Attributes
- {{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
- Option 3: Use ng-init to Evaluate an Initialization Method
- {{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
- Option 4: Use an Angular Service to Provide the Data & Inject It
- {{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
- Conclusion
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.
id is {{id}}
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.
{{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
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:
{{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
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: ");
}
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.
{{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
{{item.name}} ({{item.id}}) costs {{item.cost | currency}} {{test}}
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.
Thanks John, very helpful article, but I thought “itemsApp” in _items_service.js.erb will be undefined since it will be happened before Angular bootstrap. Karl
Nope, works as advertised.
You could always declare it explicitly like so:
angular.module(‘itemsApp’).service……
This solution worked great, thank you very much!
nice!