Getting Started with AngularJS and Rails 4

This article will teach you how to plug AngularJS into Rails 4.

Getting started with AngularJS isn't hard. The documentation is some of the best out there and it's tutorials are simple enough.

But things get tricky when you start combining technologies.

If you're using CoffeeScript instead of straight JavaScript, you know have preprocessing concerns to take into account - as well as the obvious syntax difference. These are minor issues by themselves, but what if you throw Ruby on Rails, Jasmine and Karma into the mix? It gets surprisingly trickier.

This is exactly the stack we're going to use in this tutorial. Not because we're gluttons for punishment, but because this is the kind of setup you'll see in the real world.

This tutorial assumes that you're comfortable with Rails, but not necessarily AngularJS.

Creating a base Rails app

Since there are so many technology layers involved, I'm going to build a simple application that barely does anything. We'll be setting up CRUD functionality for restaurants - actually, just the CR part. The -UD is left as an exercise for the reader. ;-)

We'll call the application Restauranteur.

I'm using PostgreSQL and RSpec here, but the DBMS and server-side testing framework are not significant. You can use whatever you want.

Initial setup

First create the project:

$ rails new restauranteur --database=postgresql --skip-test-unit

If you're using Pow, add your project to Pow:

$ ln -s /Users/jasonswett/projects/restauranteur ~/.pow/restauranteur

Create the PostgreSQL database user:

$ createuser -P -s -e restauranteur

Add RSpec to your Gemfile:

# Gemfile
gem "rspec-rails", "~> 2.14.0"

Install RSpec:

$ bundle install
$ rails g rspec:install

Create the database:

$ rake db:create

Creating the Restaurant model

Now that we have our project and database created, let's create our first resource. The Restaurant resource will have only one attribute: name, which is a string.

$ rails generate scaffold restaurant name:string

Now, just to be OCD about it, we'll make sure restaurant names are unique.

# db/migrate/[timestamp]_create_restaurants.rb

class CreateRestaurants < ActiveRecord::Migration
  def change
    create_table :restaurants do |t|
      t.string :name

      t.timestamps
    end

    # Add the following line
    add_index :restaurants, :name, unique: true
  end
end

Run the migration:

$ rake db:migrate

Let's add some specs to verify that we can't create invalid restaurants. Notice that unique failure gives raw error.

require 'spec_helper'

describe Restaurant do
  before do
    @restaurant = Restaurant.new(name: "Momofuku")
  end

  subject { @restaurant }

  it { should respond_to(:name) }
  it { should be_valid }

  describe "when name is not present" do
    before { @restaurant.name = " " }
    it { should_not be_valid }
  end

  describe "when name is already taken" do
    before do
      restaurant_with_same_name = @restaurant.dup
      restaurant_with_same_name.name = @restaurant.name.upcase
      restaurant_with_same_name.save
    end

    it { should_not be_valid }
  end
end

Adding these validators will make the specs pass:

class Restaurant < ActiveRecord::Base 
  validates :name, presence: true, uniqueness: { case_sensitive: false }
end

We're now good to move on.

Bringing AngularJS into the mix

Rather than dump everything on you at once, I'd first like to demonstrate the simplest "Hello, world" version of an AngularJS-Rails application and then build our restaurant CRUD functionality onto that.

There's no reason our "Hello, world" page must or should be tied to any particular Rails resource. For this reason, we'll create a StaticPagesController to serve up our AngularJS home page.

Create the controller

$ rails generate controller static_pages index

Our root route right now is just the "Welcome to Rails" page. Let's set it to the indexaction of our new StaticPagesController:

# config/routes.rb

Restauranteur::Application.routes.draw do
  # Add the following line
  root 'static_pages#index'
end

Download Angular

  1. In order to get our tests to work properly later, we'll need a file calledangular-mocks.js. I don't think there's any mention of this in the Angular docs anywhere, but it's necessary.
  2. In the AngularJS tutorial, the docs list the latest bleeding-edge version, but if I recall correctly, I had problems with compatibility betweenangular.js and angular-mocks.js for the latest version. I know that versions 1.1.5 worked together, so even though that's not the latest stable version, that's the version I'm listing here. Of course as time goes on the compatibility situation will probably improve.

Download angular.js and angular-mocks.js from code.angularjs.org and move the files into app/assets/javascripts.

$ wget http://code.angularjs.org/1.1.5/angular.js \
http://code.angularjs.org/1.1.5/angular-mocks.js
$ mv angular* app/assets/javascripts

Add it to the asset pipeline

Now we want to tell our application to require the AngularJS file, and we want to make sure it gets loaded before other files that depend on it. (We could use something like RequireJS to manage these dependencies, and that's probably what I would do on a production product, but for the purposes of this tutorial I want to keep the technology stack as thin as possible.)

Note: Angular and Turbolinks can conflict with one another, so we disable them here

// app/assets/javascripts/application.js

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require jquery_ujs

// Add the following two lines
//= require angular
//= require main

//= require_tree .

Set up the layout

We'll add ng-app and ng-view, which signal that we have an Angular app in our page. Also notice that mentions of Turbolinks have been removed.

  <%= yield %>

Creating an Angular controller

First let's create a directory for our controllers. You can name it whatever you want.

$ mkdir -p app/assets/javascripts/angular/controllers

Now let's create the controller file itself. I'm calling this controller the "home controller," and the convention in Angular is to append your controller filenames withCtrl. Thus our filename will beapp/assets/javascripts/angular/controllers/HomeCtrl.js.coffee:

# app/assets/javascripts/angular/controllers/HomeCtrl.js.coffee

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) ->
  # Notice how this controller body is empty
]

Add an Angular route

Now we'll add a routing directive in order to make our HomeCtrl be our "default page." Here I'm defining my routing in app/assets/javascripts/main.js.coffee, but again I don't think the filename matters.

# app/assets/javascripts/main.js.coffee

# This line is related to our Angular app, not to our
# HomeCtrl specifically. This is basically how we tell
# Angular about the existence of our application.
@restauranteur = angular.module('restauranteur', [])

# This routing directive tells Angular about the default
# route for our application. The term "otherwise" here
# might seem somewhat awkward, but it will make more
# sense as we add more routes to our application.
@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    }) 
])

Add an Angular template

We'll also want a place to keep our Angular templates. I decided to put mine inpublic/templates. Again, you can place them wherever you like.

mkdir public/templates

If we create a file public/templates/home.html with some arbitrary content, we should be able to see it in the browser.


This is the home page.

Now, if you go to http://restauranteur.dev/ (or http://localhost:3000/ if you're not using Pow) and you should see the contents of home.html.

An example of data binding

That's kind of interesting, but probably not very impressive. Let's actually send something across the wire. Edit yourapp/assets/angular/controllers/HomeCtrl.js.coffee like this:

# app/assets/angular/controllers/HomeCtrl.js.coffee 

@restauranteur.controller 'HomeCtrl', ['$scope', ($scope) -> 
  $scope.foo = 'bar'
]

This is kind of analagous to saying @foo = "bar" in a Rails controller. We can plugfoo into the template by using the double-brace syntax like this:

Value of "foo": {{foo}}  

Doing it for real this time

We've already built a simple hello world app. Creating a full blown CRUD application isn't much harder.

Seed the database

Working with our restaurant CRUD will be a little more meaningful if we start with some restaurants in the database. Here's a seed file you can use.

# db/seeds.rb

Restaurant.create([
  { name: "The French Laundry" },
  { name: "Chez Panisse" },
  { name: "Bouchon" },
  { name: "Noma" },
  { name: "Taco Bell" },
])
rake db:seed

Creating a restaurant index page

First let's create a template folder for restaurants:

mkdir public/templates/restaurants

The first template we'll create is the index page:

[index](/#)

  * {{ restaurant.name }}

I'll explain in a moment what these things mean. First let's create the controller:

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )
]

Lastly, we'll adjust our routing configuration:

# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])

Now, finally, we can go to the URI /#/restaurants and we should be able to see our list of restaurants. Before we move on let's add a test.

Adding our first test

Add JS test folder:

mkdir spec/javascripts

Write test:

# spec/javascripts/controllers_spec.js.coffee

describe "Restauranteur controllers", ->
  beforeEach module("restauranteur")

  describe "RestaurantIndexCtrl", ->
    it "should set restaurants to an empty array", inject(($controller) ->
      scope = {}
      ctrl = $controller("RestaurantIndexCtrl",
        $scope: scope
      )
      expect(scope.restaurants.length).toBe 0
    )

Add config:

// spec/javascripts/restauranteur.conf.js

module.exports = function(config) {
  config.set({
    basePath: '../..',

    frameworks: ['jasmine'],

    autoWatch: true,

    preprocessors: {
      '**/*.coffee': 'coffee'
    }, 

    files: [
      'app/assets/javascripts/angular.js',
      'app/assets/javascripts/angular-mocks.js',
      'app/assets/javascripts/main.js.coffee',
      'app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee',
      'app/assets/javascripts/angular/*',
      'spec/javascripts/*_spec.js.coffee'
    ]  
  });
};

Install Karma and start the server:

sudo npm install -g karma
sudo npm install -g karma-ng-scenario
karma start spec/javascripts/restauranteur.conf.js

If you go to http://localhost:9876/, our test will run and be successful. If you'd like to see the test fail, change expect(scope.restaurants.length).toBe 0 toexpect(scope.restaurants.length).toBe 1 and run the test again.

The meaningfulness of this test we just added is obviously questionable, but my intention here is to save you the work of figuring out how to get your Angular code into a test harness. There are certain things, like the CoffeeScript preprocessor andangular-mocks.js inclusion that are totally not obvious and took me several hours of head-scratching to get right.

Building out the restaurants page

Let's now make a temporary adjustment to our restaurant index template:

  * {{restaurant.name}} ({{restaurant.id}})

If you now revisit /#/restaurants, you'll notice that none of the restaurants have their IDs. Why are they blank?

When you generate scaffolding in Rails 4, it gives you some .jbuilder files:

$ ls -1 app/views/restaurants/*.jbuilder
app/views/restaurants/index.json.jbuilder
app/views/restaurants/show.json.jbuilder

If you open up app/views/restaurants/index.json.jbuilder, you'll see this:

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :name
  json.url restaurant_url(restaurant, format: :json)
end

As you can see, it's including :name but not :id. Let's add it:

# app/views/restaurants/index.json.jbuilder

json.array!(@restaurants) do |restaurant|
  json.extract! restaurant, :id, :name
  json.url restaurant_url(restaurant, format: :json)
end

If you save the file and refresh /#/restaurants, you should see the IDs appear.

Now let's change the template back to the way it originally was:

[index](/#)

  * {{ restaurant.name }}

You may have noticed at some point that we're pointing these things at something called viewRestaurant() but we never actually defined anything calledviewRestaurant(). Let's do that now:

# app/assets/javascripts/angular/controllers/RestaurantIndexCtrl.js.coffee

@restauranteur.controller 'RestaurantIndexCtrl', ['$scope', '$location', '$http', ($scope, $location, $http) ->
  $scope.restaurants = []
  $http.get('./restaurants.json').success((data) ->
    $scope.restaurants = data
  )

  # Add the following lines
  $scope.viewRestaurant = (id) ->
    $location.url "/restaurants/#{id}"
]

The convention in Rails is that resource_name/:id maps to a "show" page, and that's what we'll do here. Let's create a show template, route and controller.

# {{restaurant.name}}
# app/assets/javascripts/main.js.coffee

@restauranteur = angular.module('restauranteur', [])

@restauranteur.config(['$routeProvider', ($routeProvider) ->
  $routeProvider.
    when('/restaurants', {
      templateUrl: '../templates/restaurants/index.html',
      controller: 'RestaurantIndexCtrl'
    }).
    when('/restaurants/:id', {
      templateUrl: '../templates/restaurants/show.html',
      controller: 'RestaurantShowCtrl'
    }).
    otherwise({
      templateUrl: '../templates/home.html',
      controller: 'HomeCtrl'
    })
])
# app/assets/javascripts/angular/controllers/RestaurantShowCtrl.js.coffee

@restauranteur.controller 'RestaurantShowCtrl', ['$scope', '$http', '$routeParams', ($scope, $http, $routeParams) ->
  $http.get("./restaurants/#{$routeParams.id}.json").success((data) ->
    $scope.restaurant = data
  )
]

Now if you refresh /#/restaurants and click on a restaurant, you should find yourself at that restaurant's show page. Yay!

That's all for now

We may not have seen particularly impressive results, but I hope I've saved you some time plugging AngularJS into Rails 4. Next I might recommend looking intongResource, which can help make CRUD functionality more DRY.

Interested in learning more?

Check out the great post by Adam Anderson, whose Bootstrapping an AngularJS app in Rails 4.0 series helped me get started with AngularJS and Rails. You might like to go through his tutorial as well, but this tutorial is different in the sense that I try to _really_spoon-feed you all the details, minimizing the chances you'll get stuck in the weeds.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Starr Horne

    Starr Horne is a Rubyist and Chief JavaScripter at Honeybadger.io. When she's not neck-deep in other people's bugs, she enjoys making furniture with traditional hand-tools, reading history and brewing beer in her garage in Seattle.

    More articles by Starr Horne
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial