AngularJS and Turbolinks in Rails 4.1

04 August 2014

This post is for anyone trying to get AngularJS and Turbolinks to play well together.

Why would anyone want to do this you ask? Well, I can only speak for myself, but I think that AngularJS can be used within a traditional Rails app to enhance UX and create some highly interactive pages. The thing is, I wanted to be able to do this while still enjoying the snappy page loading from Turbolinks.

So, here goes nothing:


Download and install AngularJS

  1. Download AngularJS from angularjs.org (version 1.2.21 at time of writing)
  2. Add the files to vendor/assets/javascripts/ in your rails application.

Include AngularJS in your App

Modify app/assets/javascripts/application.js to include Angular

//= require jquery
//= require jquery_ujs
//= require angular.min
//= require turbolinks

Create the AngularJS application

Create the file my_app.js in app/assets/javascripts/

angular.module('myApp', []);

// This will cause your app to compile when Turbolinks loads a new page
// and removes the need for ng-app in the DOM
$(document).on('ready page:load', function(arguments) {
  angular.bootstrap(document.body, ['myApp'])
});

The Turbolinks custom event 'ready page:load' will cause your app to be bootstrapped and compiled. Read more about the custom events in the Turbolinks documentation.

“Call angular.bootstrap to compile the element into an executable, bi-directionally bound application. Note: You should not use the ng-app directive when manually bootstrapping your app.” https://docs.angularjs.org/guide/bootstrap


Create the AngularJS file structure

app/
  assets/
    javascripts/
      controllers/
      directives/
      filters/
      resources/
      my_app.js

This can be a nice and simple way to sort your Angular files. Feel free to use whichever directory structure you prefer.


Include the files/folders in your application

Edit app/assets/javascripts/application.js to include files & folders.

//= require ./my_app
//= require_tree .

It’s important that my_app.js is loaded before the rest of the Angular files. Require tree will include all files/folders in the javascripts directory. You don’t have to worry about files being loaded twice, as the asset pipeline is smart enough to avoid that.


Configure the CSRF Token

Modify the myApp config to setup the CSRF Token for valid $http requests.

angular.module('myApp', [])
.config(["$httpProvider", function(provider) {
  provider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
}]);

Without this you cannot make valid requests to your Rails application.


Create a controller to test it out

app/assets/javascripts/controllers/test_ctrl.js

angular.module('myApp')
.controller('TestCtrl',['$scope', function($scope) {
  $scope.alertMe = function(arguments) {
    alert('you\'re a genius!');
  };
}]);

Add the controller and button to any erb template you’re viewing.

<div ng-controller="TestCtrl">
  <button ng-click="alertMe()">Click me</button>
</div>

Click the button. Voila! You’re using Angular with Turbolinks!


Pass variables to your angular controller

Now, because you’re not building a single page application, you’re probably going to need a way to pass variables from your backend to your angular app.

Without Turbolinks, I would normally use ng-init to initialize these variables, and I’d use the angular.element().ready() function to know when they had been set.

Unfortunately, with Turbolinks, you cannot rely on angular’s ready() method. In my testing, the event would fire before the variables had actually been set.

Luckily, there are a few work arounds!

A) Use $watch with ng-init: This is a quick & easy way to initialize variables in your controllers. By passing variables with ng-init, you can simply watch for a change in your controller, and then perform your controller initialization.

I like this for simple controllers, but it has a few downsides:

  • You should destroy your watcher after the variable is set. Otherwise it impact performance, as $watch will continue to check for changes on your variable.
  • It’s not obvious which variables are required for your controller to initialize
  • It can get a bit complicated if your controller relies on multiple values
<div ng-controller="TestCtrl" ng-init="myName='kurt';">
  <!-- etc -->
</div>
angular.module('myApp')
.controller('TestCtrl', function($scope) {
  var stopWatching = $scope.$watch('myName', function(myName){
    if (myName) {
      alert('hello ' + myName);
      stopWatching(); // this will destroy the watcher
    }
  });
});

B) Set required values on your module: You can set values on your module, and inject them into your controllers. This completely avoids the need for ng-init at all. It will even throw errors in the event that you forget to set the value.

Here’s an example:

<script>
angular.module('myApp').value('joke', 'Isn\'t Jolt cola basically a fizz buzz?\n - Tenderlove');
</script>

<div ng-controller="TestCtrl">
  <button ng-click="tellJoke()">Tell me a joke!</button>
</div>
angular.module('myApp')
.controller('TestCtrl', function($scope, joke) {
  $scope.tellJoke = function() {
    alert(joke);
  };
});

Credit goes to Court Ewing for showing me this method.

Try it out:

Well, that’s it for the tutorial! Hopefully this gives enough information for you to get started.

I realize most people will probably never want to mix AngularJS/Turbolinks, but I just wanted to go against the common advice of “Remove Turbolinks”, and this seemed like a fun thing to try.

If you’ve got any feedback or comments, please let me know!

blog comments powered by Disqus