How to Wire Up Ruby on Rails and AngularJS as a Single-Page Application

This post is now somewhat outdated. If you’re interested in Angular/Rails/Grunt/JavaScript/HTML, read on. If you’d prefer Angular/Rails/Gulp/CoffeeScript/Jade, check out this newer post.

Why this tutorial exists

I wrote this tutorial because I had a pretty tough time getting Rails and Angular to talk to each other as an SPA. The best resource I could find out there was Ari Lerner’s Riding Rails with AngularJS. I did find that book very helpful and I thought it was really well-done, but it seems to be a little bit out-of-date by now and I couldn’t just plug in its code and have everything work. I had to do a lot of extra Googling and head-scratching to get all the way there. This tutorial is meant to be a supplement to Ari’s book, not a replacement for it. I definitely ecommend buying the book because it really is very helpful also if you are low on cash and can’t afford one, then you can take a loan up to $1500 online and buy it anyway.

The sample app

There’s a certain sample app I plan to use throughout AngularOnRails.com called Lunch Hub. The idea with Lunch Hub is that office workers can announce in the AM where they’d like to go for lunch rather than deciding as they gather around the door and waste half their lunch break. Since Lunch Hub is a real project with its own actual production code, I use a different project here called “Fake Lunch Hub.” You can see the Fake Lunch Hub repo here. I also created a branch specifically to match up with this tutorial here.

Setting up our Rails project

Instead of regular Rails we’re going to use Rails::API. I’ve tried to do Angular projects with full-blown Rails, but I end up with a bunch of unused views, which feels weird. First, if you haven’t already, install Rails::API.

$ gem install rails-api

Creating a new Rails::API project works the same as creating a regular Rails project.

$ rails-api new fake_lunch_hub -T -d postgresql

Get into our project directory.

$ cd fake_lunch_hub

Create our PostgreSQL user.

$ createuser -P -s -e fake_lunch_hub

Create the database.

$ rake db:create

Now we’ll create a resource so we have something to look at through our AngularJS app. (This might be a good time to commit this project to version control.)

Creating our first resource

Add gem 'rspec-rails' to your Gemfile (in the test group) and run:

$ bundle install
$ rails g rspec:install

When you generate scaffolds from now on, RSpec will want to create all kinds of spec files for you automatically, including some kinds of specs (like view specs) that in my opinion are kind of nutty and really shouldn’t be there. We can tell RSpec not to create these spec files:

# config/application.rb

require File.expand_path('../boot', __FILE__)

# Pick the frameworks you want:
require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
require "action_view/railtie"
require "sprockets/railtie"
# require "rails/test_unit/railtie"

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

module FakeLunchHub
  class Application < Rails::Application
    # Settings in config/environments/* take precedence over those specified here.
    # Application configuration should go into files in config/initializers
    # -- all .rb files in that directory are automatically loaded.

    # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
    # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
    # config.time_zone = 'Central Time (US & Canada)'

    # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
    # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
    # config.i18n.default_locale = :de

    config.generators do |g|
      g.test_framework :rspec,
        fixtures: false,
        view_specs: false,
        helper_specs: false,
        routing_specs: false,
        request_specs: false,
        controller_specs: true
    end
  end
end

(Now might be another good time to make a commit.)

In Lunch Hub, I want everybody’s lunch announcements to be visible only to other people in the office where they work, not the whole world. And there’s actually a good chance a person might want to belong not only to a group tied to his or her current workplace, but perhaps a former workplace or totally arbitrary group of friends. So I decided to create the concept of a Group in Lunch Hub. Let’s create a Group resource that, for simplicity, has only one attribute: name.

$ rails g scaffold group name:string

Since groups have to have names, let’s set null: false in the migration. We’ll also include a uniqueness index.

# db/migrate/<timestamp>_create_groups.rb

class CreateGroups < ActiveRecord::Migration
  def change
    create_table :groups do |t|
      t.string :name, null: false

      t.timestamps
    end

    add_index :groups, :name, unique: true
  end
end
$ rake db:migrate

Now, if you run rails server and go to http://localhost:3000/groups, you should see empty brackets ([]). We actually want to be able to do http://localhost:3000/api/groups instead.

# config/routes.rb

Rails.application.routes.draw do
  scope '/api' do
    resources :groups, except: [:new, :edit]
  end
end

At the risk of being annoying, I wanted to include a realistic level of testing in the tutorial, at least on the server side. (I don’t have the client-side testing part 100% figured out yet.)

# spec/models/group_spec.rb

require 'rails_helper'

RSpec.describe Group, :type => :model do
  before do
    @group = Group.new(name: "Ben Franklin Labs")
  end

  subject { @group }

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

To make this spec pass you’ll of course need to add a validation:

# app/models/group.rb

class Group < ActiveRecord::Base
  validates :name, presence: true
end

We also have to adjust the controller spec RSpec spit out for us because RSpec’s generators are evidently not yet fully compatible with Rails::API. I suggest you just copy my file and replace yours wholesale.

# spec/controllers/groups_controller_spec.rb

require 'rails_helper'

# This spec was generated by rspec-rails when you ran the scaffold generator.
# It demonstrates how one might use RSpec to specify the controller code that
# was generated by Rails when you ran the scaffold generator.
#
# It assumes that the implementation code is generated by the rails scaffold
# generator.  If you are using any extension libraries to generate different
# controller code, this generated spec may or may not pass.
#
# It only uses APIs available in rails and/or rspec-rails.  There are a number
# of tools you can use to make these specs even more expressive, but we're
# sticking to rails and rspec-rails APIs to keep things simple and stable.
#
# Compared to earlier versions of this generator, there is very limited use of
# stubs and message expectations in this spec.  Stubs are only used when there
# is no simpler way to get a handle on the object needed for the example.
# Message expectations are only used when there is no simpler way to specify
# that an instance is receiving a specific message.

RSpec.describe GroupsController, :type => :controller do

  # This should return the minimal set of attributes required to create a valid
  # Group. As you add validations to Group, be sure to
  # adjust the attributes here as well.
  let(:valid_attributes) {
    skip("Add a hash of attributes valid for your model")
  }

  let(:invalid_attributes) {
    skip("Add a hash of attributes invalid for your model")
  }

  # This should return the minimal set of values that should be in the session
  # in order to pass any filters (e.g. authentication) defined in
  # GroupsController. Be sure to keep this updated too.
  let(:valid_session) { {} }

  describe "GET index" do
    it "assigns all groups as @groups" do
      group = Group.create! valid_attributes
      get :index, {}, valid_session
      expect(assigns(:groups)).to eq([group])
    end
  end

  describe "GET show" do
    it "assigns the requested group as @group" do
      group = Group.create! valid_attributes
      get :show, {:id => group.to_param}, valid_session
      expect(assigns(:group)).to eq(group)
    end
  end

  describe "POST create" do
    describe "with valid params" do
      it "creates a new Group" do
        expect {
          post :create, {:group => valid_attributes}, valid_session
        }.to change(Group, :count).by(1)
      end

      it "assigns a newly created group as @group" do
        post :create, {:group => valid_attributes}, valid_session
        expect(assigns(:group)).to be_a(Group)
        expect(assigns(:group)).to be_persisted
      end

      it "redirects to the created group" do
        post :create, {:group => valid_attributes}, valid_session
        expect(response).to redirect_to(Group.last)
      end
    end

    describe "with invalid params" do
      it "assigns a newly created but unsaved group as @group" do
        post :create, {:group => invalid_attributes}, valid_session
        expect(assigns(:group)).to be_a_new(Group)
      end

      it "re-renders the 'new' template" do
        post :create, {:group => invalid_attributes}, valid_session
        expect(response).to render_template("new")
      end
    end
  end

  describe "PUT update" do
    describe "with valid params" do
      let(:new_attributes) {
        skip("Add a hash of attributes valid for your model")
      }

      it "updates the requested group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => new_attributes}, valid_session
        group.reload
        skip("Add assertions for updated state")
      end

      it "assigns the requested group as @group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session
        expect(assigns(:group)).to eq(group)
      end

      it "redirects to the group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session
        expect(response).to redirect_to(group)
      end
    end

    describe "with invalid params" do
      it "assigns the group as @group" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session
        expect(assigns(:group)).to eq(group)
      end

      it "re-renders the 'edit' template" do
        group = Group.create! valid_attributes
        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session
        expect(response).to render_template("edit")
      end
    end
  end

  describe "DELETE destroy" do
    it "destroys the requested group" do
      group = Group.create! valid_attributes
      expect {
        delete :destroy, {:id => group.to_param}, valid_session
      }.to change(Group, :count).by(-1)
    end

    it "redirects to the groups list" do
      group = Group.create! valid_attributes
      delete :destroy, {:id => group.to_param}, valid_session
      expect(response).to redirect_to(groups_url)
    end
  end

end

Now if you run all specs on the command line ($ rspec), they should all pass. We don’t have anything interesting to look at yet but our Rails API is now good to go.

Adding the client side

On the client side we’ll be using Yeoman, a front-end scaffolding tool. First, install Yeoman itself as well as Yeoman’s Angular generator. (If you don’t already have npm installed, you’ll need to do that. If you’re using Mac OS with Homebrew, run brew install npm.)

$ npm install -g yo
$ npm install -g generator-angular

We’ll keep our client-side code in a directory called client. (This is an arbitrary naming choice and you could call it anything.)

$ mkdir client
$ cd client

Now we’ll generate the Angular app itself. Just accept all the defaults.

$ yo angular fake_lunch_hub

Start Grunt:

$ grunt serve

It’s likely that you’ll the error “invalid option: –fonts-dir”. The solution (or at least solution) to this problem is to remove the following line from your Gruntfile.js (line 186 for me):

fontsDir: '<%= yeoman.app %>/styles/fonts',

When it spins up free of errors or warnings, Grunt should open a new browser tab for you at http://localhost:9000/#/ where you see the “‘Allo, ‘Allo” thing. Our Angular app is now in place. It still doesn’t know how to talk to Rails, so we still have to make that part work.

Setting up a proxy

We’ll use something called grunt-connect-proxy to forward certain requests from our Grunt server running on port 9000 to our Rails server running on port 3000.

$ npm install --save-dev grunt-connect-proxy

Change your Gruntfile to match this:

// Gruntfile.js

// Generated on 2014-07-18 using generator-angular 0.9.5
'use strict';

// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'

module.exports = function (grunt) {

  // Load grunt tasks automatically
  require('load-grunt-tasks')(grunt);

  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);

  // Configurable paths for the application
  var appConfig = {
    app: require('./bower.json').appPath || 'app',
    dist: 'dist'
  };

  // Define the configuration for all the tasks
  grunt.initConfig({

    // Project settings
    yeoman: appConfig,

    // Watches files for changes and runs tasks based on the changed files
    watch: {
      bower: {
        files: ['bower.json'],
        tasks: ['wiredep']
      },
      js: {
        files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
        tasks: ['newer:jshint:all'],
        options: {
          livereload: '<%= connect.options.livereload %>'
        }
      },
      jsTest: {
        files: ['test/spec/{,*/}*.js'],
        tasks: ['newer:jshint:test', 'karma']
      },
      compass: {
        files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        tasks: ['compass:server', 'autoprefixer']
      },
      gruntfile: {
        files: ['Gruntfile.js']
      },
      livereload: {
        options: {
          livereload: '<%= connect.options.livereload %>'
        },
        files: [
          '<%= yeoman.app %>/{,*/}*.html',
          '.tmp/styles/{,*/}*.css',
          '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
        ]
      }
    },

    // The actual grunt server settings
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      proxies: [
        {
          context: '/api',
          host: 'localhost',
          port: 3000
        }
      ],
      livereload: {
        options: {
          open: true,
          middleware: function (connect, options) {
            if (!Array.isArray(options.base)) {
              options.base = [options.base];
            }

            // Setup the proxy
            var middlewares = [
              require('grunt-connect-proxy/lib/utils').proxyRequest,
              connect.static('.tmp'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];

            // Make directory browse-able.
            var directory = options.directory || options.base[options.base.length - 1];
            middlewares.push(connect.directory(directory));

            return middlewares;
          }
        }
      },
      test: {
        options: {
          port: 9001,
          middleware: function (connect) {
            return [
              connect.static('.tmp'),
              connect.static('test'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];
          }
        }
      },
      dist: {
        options: {
          open: true,
          base: '<%= yeoman.dist %>'
        }
      }
    },

    // Make sure code styles are up to par and there are no obvious mistakes
    jshint: {
      options: {
        jshintrc: '.jshintrc',
        reporter: require('jshint-stylish')
      },
      all: {
        src: [
          'Gruntfile.js',
          '<%= yeoman.app %>/scripts/{,*/}*.js'
        ]
      },
      test: {
        options: {
          jshintrc: 'test/.jshintrc'
        },
        src: ['test/spec/{,*/}*.js']
      }
    },

    // Empties folders to start fresh
    clean: {
      dist: {
        files: [{
          dot: true,
          src: [
            '.tmp',
            '<%= yeoman.dist %>/{,*/}*',
            '!<%= yeoman.dist %>/.git*'
          ]
        }]
      },
      server: '.tmp'
    },

    // Add vendor prefixed styles
    autoprefixer: {
      options: {
        browsers: ['last 1 version']
      },
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/styles/',
          src: '{,*/}*.css',
          dest: '.tmp/styles/'
        }]
      }
    },

    // Automatically inject Bower components into the app
    wiredep: {
      options: {
        cwd: '<%= yeoman.app %>'
      },
      app: {
        src: ['<%= yeoman.app %>/index.html'],
        ignorePath:  /\.\.\//
      },
      sass: {
        src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        ignorePath: /(\.\.\/){1,2}bower_components\//
      }
    },

    // Compiles Sass to CSS and generates necessary files if requested
    compass: {
      options: {
        sassDir: '<%= yeoman.app %>/styles',
        cssDir: '.tmp/styles',
        generatedImagesDir: '.tmp/images/generated',
        imagesDir: '<%= yeoman.app %>/images',
        javascriptsDir: '<%= yeoman.app %>/scripts',
        importPath: './bower_components',
        httpImagesPath: '/images',
        httpGeneratedImagesPath: '/images/generated',
        httpFontsPath: '/styles/fonts',
        relativeAssets: false,
        assetCacheBuster: false,
        raw: 'Sass::Script::Number.precision = 10\n'
      },
      dist: {
        options: {
          generatedImagesDir: '<%= yeoman.dist %>/images/generated'
        }
      },
      server: {
        options: {
          debugInfo: true
        }
      }
    },

    // Renames files for browser caching purposes
    filerev: {
      dist: {
        src: [
          '<%= yeoman.dist %>/scripts/{,*/}*.js',
          '<%= yeoman.dist %>/styles/{,*/}*.css',
          '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
          '<%= yeoman.dist %>/styles/fonts/*'
        ]
      }
    },

    // Reads HTML for usemin blocks to enable smart builds that automatically
    // concat, minify and revision files. Creates configurations in memory so
    // additional tasks can operate on them
    useminPrepare: {
      html: '<%= yeoman.app %>/index.html',
      options: {
        dest: '<%= yeoman.dist %>',
        flow: {
          html: {
            steps: {
              js: ['concat', 'uglifyjs'],
              css: ['cssmin']
            },
            post: {}
          }
        }
      }
    },

    // Performs rewrites based on filerev and the useminPrepare configuration
    usemin: {
      html: ['<%= yeoman.dist %>/{,*/}*.html'],
      css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
      options: {
        assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
      }
    },

    // The following *-min tasks will produce minified files in the dist folder
    // By default, your `index.html`'s <!-- Usemin block --> will take care of
    // minification. These next options are pre-configured if you do not wish
    // to use the Usemin blocks.
    // cssmin: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/styles/main.css': [
    //         '.tmp/styles/{,*/}*.css'
    //       ]
    //     }
    //   }
    // },
    // uglify: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/scripts/scripts.js': [
    //         '<%= yeoman.dist %>/scripts/scripts.js'
    //       ]
    //     }
    //   }
    // },
    // concat: {
    //   dist: {}
    // },

    imagemin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.{png,jpg,jpeg,gif}',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    svgmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.svg',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    htmlmin: {
      dist: {
        options: {
          collapseWhitespace: true,
          conservativeCollapse: true,
          collapseBooleanAttributes: true,
          removeCommentsFromCDATA: true,
          removeOptionalTags: true
        },
        files: [{
          expand: true,
          cwd: '<%= yeoman.dist %>',
          src: ['*.html', 'views/{,*/}*.html'],
          dest: '<%= yeoman.dist %>'
        }]
      }
    },

    // ngmin tries to make the code safe for minification automatically by
    // using the Angular long form for dependency injection. It doesn't work on
    // things like resolve or inject so those have to be done manually.
    ngmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/concat/scripts',
          src: '*.js',
          dest: '.tmp/concat/scripts'
        }]
      }
    },

    // Replace Google CDN references
    cdnify: {
      dist: {
        html: ['<%= yeoman.dist %>/*.html']
      }
    },

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
            '.htaccess',
            '*.html',
            'views/{,*/}*.html',
            'images/{,*/}*.{webp}',
            'fonts/*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/images',
          dest: '<%= yeoman.dist %>/images',
          src: ['generated/*']
        }, {
          expand: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= yeoman.dist %>'
        }]
      },
      styles: {
        expand: true,
        cwd: '<%= yeoman.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
      }
    },

    // Run some tasks in parallel to speed up the build process
    concurrent: {
      server: [
        'compass:server'
      ],
      test: [
        'compass'
      ],
      dist: [
        'compass:dist',
        'imagemin',
        'svgmin'
      ]
    },

    // Test settings
    karma: {
      unit: {
        configFile: 'test/karma.conf.js',
        singleRun: true
      }
    }
  });


  grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'wiredep',
      'concurrent:server',
      'autoprefixer',
      'configureProxies',
      'connect:livereload',
      'watch'
    ]);
  });

  grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
    grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
    grunt.task.run(['serve:' + target]);
  });

  grunt.registerTask('test', [
    'clean:server',
    'concurrent:test',
    'autoprefixer',
    'configureProxies',
    'connect:test',
    'karma'
  ]);

  grunt.registerTask('build', [
    'clean:dist',
    'wiredep',
    'useminPrepare',
    'concurrent:dist',
    'autoprefixer',
    'concat',
    'ngmin',
    'copy:dist',
    'cdnify',
    'cssmin',
    'uglify',
    'filerev',
    'usemin',
    'htmlmin'
  ]);

  grunt.registerTask('default', [
    'newer:jshint',
    'test',
    'build'
  ]);

  grunt.loadNpmTasks('grunt-connect-proxy');
};

Now kill Grunt and again run:

grunt serve

You should now be able to go to http://localhost:9000/api/groups and get empty brackets. (Make sure your Rails server is running.) Our Angular app is now talking to Rails.

Getting Rails data to show up in our client app

Now we’ll want to get some actual data to show up in the actual HTML of our Angular app. This is a pretty easy step now that we have the plumbing taken care of. First, create some seed data:

# db/seeds.rb

# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
#
# Examples:
#
#   cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
#   Mayor.create(name: 'Emanuel', city: cities.first)

Group.create([
  { name: 'Ben Franklin Labs' },
  { name: 'Snip Salon Software' },
  { name: 'GloboChem' },
  { name: 'TechCorp' },
])

Get the data into the database:

$ rake db:seed

Now we’ll add an AngularJS resource that will allow us to conveniently perform CRUD operations on Group. AngularJS resources match pretty nicely with Rails resources, and I’ve found that my code can be a lot more DRY using Angular resources than jQuery AJAX. We won’t get into the details here of the Angular/Rails resource interaction, though. All we’ll use is the query() method, which matches up to a Rails resource’s index action. Add the Group resource to app/scripts/app.js. I changed more than just a few lines in this file, so you might want to just copy and paste the whole thing.

// app/scripts/app.js

'use strict';

/**
 * @ngdoc overview
 * @name fakeLunchHubApp
 * @description
 * # fakeLunchHubApp
 *
 * Main module of the application.
 */
var app = angular.module('fakeLunchHubApp', [
    'ngAnimate',
    'ngCookies',
    'ngResource',
    'ngRoute',
    'ngSanitize',
    'ngTouch'
  ]);

app.config(function ($routeProvider) {
  $routeProvider
    .when('/', {
      templateUrl: 'views/main.html',
      controller: 'MainCtrl'
    })
    .when('/about', {
      templateUrl: 'views/about.html',
      controller: 'AboutCtrl'
    })
    .when('/groups', {
      templateUrl: 'views/groups.html',
      controller: 'GroupsCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
});

app.factory('Group', ['$resource', function($resource) {
  return $resource('/api/groups/:id.json', null, {
    'update': { method:'PUT' }
  });
}]);

Now add a controller for the Group resource:

// app/scripts/controllers/groups.js

'use strict';

/**
 * @ngdoc function
 * @name fakeLunchHubApp.controller:GroupsCtrl
 * @description
 * # GroupsCtrl
 * Controller of the fakeLunchHubApp
 */
angular.module('fakeLunchHubApp')
  .controller('GroupsCtrl', ['$scope', 'Group', function ($scope, Group) {
    $scope.groups = Group.query();
  }]);

And add a view.

<!-- app/views/groups.html -->

<h1>Groups</h1>

<ul ng-repeat="group in groups">
  <li>{{group.name}}</li>
</ul>

Lastly, add the following line to app/index.html, near the bottom:

<!-- app/index.html -->

<script src="scripts/controllers/groups.js"></script>

If you now go to http://localhost:9000/#/groups, you should see our list of groups.

Deployment

I’ve written a separate article about deployment called How to Deploy an Angular/Rails Single-Page Application to Heroku.

This is a work in progress

This tutorial is a work in progress, and I intend to update it as a) the technology moves and b) I learn more. I’m aware that there are certain things missing, such as testing on the client side and end-to-end tests. But right now I just want to get this tutorial out into the world because I haven’t seen anything yet that spells out the Rails/Angular combo with this level of hand-holding. I hope this has been helpful. Please leave me any questions or comments you might have. Thanks.

How to Get Grunt to Start Your Rails Server

Getting Grunt to start your Rails server is extremely simple and quick if you just know how.

By the way, you might want to check out my How to Wire Up Ruby on Rails and AngularJS as a Single-Page Application tutorial before you proceed. I realize your existing Gruntfile.js setup might be different from mine, and I frankly have no idea if what I’m suggesting here will work if your setup differs.

First you’ll need to install grunt-shell-spawn:

$ npm install grunt-shell-spawn --save-dev

Next, add this to your Gruntfile.js:

grunt.loadNpmTasks('grunt-shell-spawn');

Then, inside the grunt.initConfig part, add this:

shell: {
  startRailsServer: {
    command: 'rails server',
    options: {
      // If async: true were omitted, the rails server
      // command would prevent subsequent commands
      // from running.
      async: true
    }
  }
},

Lastly, add shell:startRailsServer to the server task.

Now, when you run grunt serve, you should end up with both localhost:9000 and localhost:3000. (Again, if your Gruntfile.js setup is different from mine, I can’t guarantee that you’ll get the same result. If you know try my suggestion with a different setup, leave me a comment and let me know how it goes.)

Here’s my complete Gruntfile.js for reference.

// Generated on 2014-07-18 using generator-angular 0.9.5
'use strict';

// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'

module.exports = function (grunt) {

  // Load grunt tasks automatically
  require('load-grunt-tasks')(grunt);

  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);

  // Configurable paths for the application
  var appConfig = {
    app: require('./bower.json').appPath || 'app',
    dist: 'dist'
  };

  // Define the configuration for all the tasks
  grunt.initConfig({

    shell: {
      startRailsServer: {
        command: 'rails server',
        options: {
          async: true
        }
      }
    },

    // Project settings
    yeoman: appConfig,

    // Watches files for changes and runs tasks based on the changed files
    watch: {
      bower: {
        files: ['bower.json'],
        tasks: ['wiredep']
      },
      js: {
        files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
        tasks: ['newer:jshint:all'],
        options: {
          livereload: '<%= connect.options.livereload %>'
        }
      },
      jsTest: {
        files: ['test/spec/{,*/}*.js'],
        tasks: ['newer:jshint:test', 'karma']
      },
      compass: {
        files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        tasks: ['compass:server', 'autoprefixer']
      },
      gruntfile: {
        files: ['Gruntfile.js']
      },
      livereload: {
        options: {
          livereload: '<%= connect.options.livereload %>'
        },
        files: [
          '<%= yeoman.app %>/{,*/}*.html',
          '.tmp/styles/{,*/}*.css',
          '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
        ]
      }
    },

    // The actual grunt server settings
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      proxies: [
        {
          context: '/api',
          host: 'localhost',
          port: 3000
        }
      ],
      livereload: {
        options: {
          open: true,
          middleware: function (connect, options) {
            if (!Array.isArray(options.base)) {
              options.base = [options.base];
            }

            // Setup the proxy
            var middlewares = [
              require('grunt-connect-proxy/lib/utils').proxyRequest,
              connect.static('.tmp'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];

            // Make directory browse-able.
            var directory = options.directory || options.base[options.base.length - 1];
            middlewares.push(connect.directory(directory));

            return middlewares;
          }
        }
      },
      test: {
        options: {
          port: 9001,
          middleware: function (connect) {
            return [
              connect.static('.tmp'),
              connect.static('test'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];
          }
        }
      },
      dist: {
        options: {
          open: true,
          base: '<%= yeoman.dist %>'
        }
      }
    },

    // Make sure code styles are up to par and there are no obvious mistakes
    jshint: {
      options: {
        jshintrc: '.jshintrc',
        reporter: require('jshint-stylish')
      },
      all: {
        src: [
          'Gruntfile.js',
          '<%= yeoman.app %>/scripts/{,*/}*.js'
        ]
      },
      test: {
        options: {
          jshintrc: 'test/.jshintrc'
        },
        src: ['test/spec/{,*/}*.js']
      }
    },

    // Empties folders to start fresh
    clean: {
      dist: {
        files: [{
          dot: true,
          src: [
            '.tmp',
            '<%= yeoman.dist %>/{,*/}*',
            '!<%= yeoman.dist %>/.git*'
          ]
        }]
      },
      server: '.tmp'
    },

    // Add vendor prefixed styles
    autoprefixer: {
      options: {
        browsers: ['last 1 version']
      },
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/styles/',
          src: '{,*/}*.css',
          dest: '.tmp/styles/'
        }]
      }
    },

    // Automatically inject Bower components into the app
    wiredep: {
      options: {
        cwd: '<%= yeoman.app %>'
      },
      app: {
        src: ['<%= yeoman.app %>/index.html'],
        ignorePath:  /\.\.\//
      },
      sass: {
        src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        ignorePath: /(\.\.\/){1,2}bower_components\//
      }
    },

    // Compiles Sass to CSS and generates necessary files if requested
    compass: {
      options: {
        sassDir: '<%= yeoman.app %>/styles',
        cssDir: '.tmp/styles',
        generatedImagesDir: '.tmp/images/generated',
        imagesDir: '<%= yeoman.app %>/images',
        javascriptsDir: '<%= yeoman.app %>/scripts',
        importPath: './bower_components',
        httpImagesPath: '/images',
        httpGeneratedImagesPath: '/images/generated',
        httpFontsPath: '/styles/fonts',
        relativeAssets: false,
        assetCacheBuster: false,
        raw: 'Sass::Script::Number.precision = 10\n'
      },
      dist: {
        options: {
          generatedImagesDir: '<%= yeoman.dist %>/images/generated'
        }
      },
      server: {
        options: {
          debugInfo: true
        }
      }
    },

    // Renames files for browser caching purposes
    filerev: {
      dist: {
        src: [
          '<%= yeoman.dist %>/scripts/{,*/}*.js',
          '<%= yeoman.dist %>/styles/{,*/}*.css',
          '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
          '<%= yeoman.dist %>/styles/fonts/*'
        ]
      }
    },

    // Reads HTML for usemin blocks to enable smart builds that automatically
    // concat, minify and revision files. Creates configurations in memory so
    // additional tasks can operate on them
    useminPrepare: {
      html: '<%= yeoman.app %>/index.html',
      options: {
        dest: '<%= yeoman.dist %>',
        flow: {
          html: {
            steps: {
              js: ['concat', 'uglifyjs'],
              css: ['cssmin']
            },
            post: {}
          }
        }
      }
    },

    // Performs rewrites based on filerev and the useminPrepare configuration
    usemin: {
      html: ['<%= yeoman.dist %>/{,*/}*.html'],
      css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
      options: {
        assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
      }
    },

    // The following *-min tasks will produce minified files in the dist folder
    // By default, your `index.html`'s <!-- Usemin block --> will take care of
    // minification. These next options are pre-configured if you do not wish
    // to use the Usemin blocks.
    // cssmin: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/styles/main.css': [
    //         '.tmp/styles/{,*/}*.css'
    //       ]
    //     }
    //   }
    // },
    // uglify: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/scripts/scripts.js': [
    //         '<%= yeoman.dist %>/scripts/scripts.js'
    //       ]
    //     }
    //   }
    // },
    // concat: {
    //   dist: {}
    // },

    imagemin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.{png,jpg,jpeg,gif}',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    svgmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.svg',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    htmlmin: {
      dist: {
        options: {
          collapseWhitespace: true,
          conservativeCollapse: true,
          collapseBooleanAttributes: true,
          removeCommentsFromCDATA: true,
          removeOptionalTags: true
        },
        files: [{
          expand: true,
          cwd: '<%= yeoman.dist %>',
          src: ['*.html', 'views/{,*/}*.html'],
          dest: '<%= yeoman.dist %>'
        }]
      }
    },

    // ngmin tries to make the code safe for minification automatically by
    // using the Angular long form for dependency injection. It doesn't work on
    // things like resolve or inject so those have to be done manually.
    ngmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/concat/scripts',
          src: '*.js',
          dest: '.tmp/concat/scripts'
        }]
      }
    },

    // Replace Google CDN references
    cdnify: {
      dist: {
        html: ['<%= yeoman.dist %>/*.html']
      }
    },

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
            '.htaccess',
            '*.html',
            'views/{,*/}*.html',
            'images/{,*/}*.{webp}',
            'fonts/*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/images',
          dest: '<%= yeoman.dist %>/images',
          src: ['generated/*']
        }, {
          expand: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= yeoman.dist %>'
        }]
      },
      styles: {
        expand: true,
        cwd: '<%= yeoman.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
      }
    },

    // Run some tasks in parallel to speed up the build process
    concurrent: {
      server: [
        'compass:server'
      ],
      test: [
        'compass'
      ],
      dist: [
        'compass:dist',
        'imagemin',
        'svgmin'
      ]
    },

    // Test settings
    karma: {
      unit: {
        configFile: 'test/karma.conf.js',
        singleRun: true
      }
    }
  });


  grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'wiredep',
      'concurrent:server',
      'shell:startRailsServer',
      'autoprefixer',
      'configureProxies',
      'connect:livereload',
      'watch'
    ]);
  });

  grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
    grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
    grunt.task.run(['serve:' + target]);
  });

  grunt.registerTask('test', [
    'clean:server',
    'concurrent:test',
    'autoprefixer',
    'configureProxies',
    'connect:test',
    'karma'
  ]);

  grunt.registerTask('build', [
    'clean:dist',
    'wiredep',
    'useminPrepare',
    'concurrent:dist',
    'autoprefixer',
    'concat',
    'ngmin',
    'copy:dist',
    'cdnify',
    'cssmin',
    'uglify',
    'filerev',
    'usemin',
    'htmlmin'
  ]);

  grunt.registerTask('default', [
    'newer:jshint',
    'test',
    'build'
  ]);

  grunt.loadNpmTasks('grunt-connect-proxy');
  grunt.loadNpmTasks('grunt-shell-spawn');
};

How to Deploy an Angular/Rails Single-Page Application to Heroku

Why deploying a single-page application is different

Before I explain how to deploy an Angular/Rails application to Heroku, it might make sense to explain why deploying a single-page application (SPA) is different from deploying a “traditional” web application.

The way I chose to structure the SPA in this example is to have all the client-side code like on this website a) outside Rails’ the asset pipeline and b) inside the same Git repo as Rails. I have a directory called client that sits at the top level of my project directory.

Gemfile
Gemfile.lock
README.rdoc
Rakefile
app
bin
client <-- This is where all the client-side code lives.
config
config.ru
db
lib
log
node_modules
spec
test
tmp
vendor

When I’m in development mode, I use Grunt to spin up a) a Rails server, which simply powers an API, and b) a client-side server.

In production the arrangement is a little different. In preparation for deployment, the grunt build command poops out a version of my client-side app into public. Rails will of course check for a file at public/index.html and, if one exists, serve that as the default page. In fact, if you run grunt build locally, spin up a development server and navigate to http://localhost:3000, you’ll see your SPA served there.

But it would be pretty tedious to have to manually run grunt build before each deployment. And even if you somehow automated that process so grunt build was run before each git push heroku master, it wouldn’t be ideal to check all the code generated by grunt build into version control.

Heroku’s deployments are Git-based. The version of my client-side app that gets served will never be checked into Git. This is the challenge.

Automatically building the client-side app on deployment

Fortunately, there is a way to tell Heroku to run grunt build after each deployment.

First let’s get grunt build functioning locally so you can see how it works.

Change the line dist: 'dist' to dist: '../public' under the var appConfig section of client/Gruntfile.js. For me this is found on line 19.

Now remove the public directory from the filesystem and from version control. (Add public to .gitignore is not necessary.)

$ rm -rf public

If you now run grunt build, you’ll see the public directory populated with files. This is what we want to have happen in production each time we deploy our app.

Configuring the buildpacks

Next you’ll want to add a file at the root level of your project called .buildpacks that uses both Ruby and Node buildpacks:

https://github.com/jasonswett/heroku-buildpack-nodejs-grunt-compass
https://github.com/heroku/heroku-buildpack-ruby.git

You can see that I have my own fork of the Node buildpack.

In order to deploy this you might need to adjust your client/package.json and move all your devDependencies to just regular dependencies. I had to do this. Here’s my client/package.json:

// client/package.json

{
  "name": "lunchhub",
  "version": "0.0.0",
  "description": "The website you can literally eat.",
  "dependencies": {
    "source-map": "^0.1.37",
    "load-grunt-tasks": "^0.6.0",
    "time-grunt": "^0.3.1",
    "grunt": "^0.4.1",
    "grunt-autoprefixer": "^0.7.3",
    "grunt-concurrent": "^0.5.0",
    "grunt-connect-proxy": "^0.1.10",
    "grunt-contrib-clean": "^0.5.0",
    "grunt-contrib-compass": "^0.7.2",
    "grunt-contrib-concat": "^0.4.0",
    "grunt-contrib-connect": "^0.7.1",
    "grunt-contrib-copy": "^0.5.0",
    "grunt-contrib-cssmin": "^0.9.0",
    "grunt-contrib-htmlmin": "^0.3.0",
    "grunt-contrib-imagemin": "^0.7.0",
    "grunt-contrib-jshint": "^0.10.0",
    "grunt-contrib-uglify": "^0.4.0",
    "grunt-contrib-watch": "^0.6.1",
    "grunt-filerev": "^0.2.1",
    "grunt-google-cdn": "^0.4.0",
    "grunt-newer": "^0.7.0",
    "grunt-ngmin": "^0.0.3",
    "grunt-protractor-runner": "^1.1.0",
    "grunt-rails-server": "^0.1.0",
    "grunt-shell-spawn": "^0.3.0",
    "grunt-svgmin": "^0.4.0",
    "grunt-usemin": "^2.1.1",
    "grunt-wiredep": "^1.7.0",
    "jshint-stylish": "^0.2.0"
  },
  "engines": {
    "node": ">=0.10.0"
  },
  "scripts": {
    "test": "grunt test"
  }
}

I also registered a new task in my Gruntfile:

// client/Gruntfile.js

grunt.registerTask('heroku:production', 'build');

You can just put this near the bottom of the file next to all your other task registrations.

And since the Node buildpack will be using $NODE_ENV when it runs its commands, you need to specify the value of $NODE_ENV:

$ heroku config:set NODE_ENV=production

Lastly, tell Heroku about your custom buildpack (thanks to Sarah Vessels for catching this):

$ heroku config:add BUILDPACK_URL=https://github.com/ddollar/heroku-buildpack-multi.git

After you have all that stuff in place, you should be able to do a regular git push to Heroku and have your SPA totally work.

How To Get HTML5 pushState Working With Angular And Rails

There are three steps to getting HTML5 pushState working in an Angular/Rails SPA.

The first step is to enable pushState within Angular. This is super simple.

If you just do the first step it will appear to work, but you’ll have a subtle issue I call “the reload problem” that will need to be addressed for both the development environment and the production environment. I’ll get into the details of that shortly. For now just know that the steps are:

  1. Enable pushState in Angular
  2. Fix the reload problem for the development environment
  3. Fix the reload problem for the production environment

January 2014 update: it seems that “the reload problem” is no longer an issue. I believe you can safely ignore the steps to fix this problem.

And by the way, this post makes a few assumptions about the way your project is set up. You may want to read or at least skim How to Write Up Ruby on Rails and AngularJS as a Single-Page Application before you dive deep into this post. My advice doesn’t require your app to be set up exactly this way for my basic instructions to make sense, but reading that post might help you understand where I’m coming from.

Enabling pushState Within Angular

Enabling push state is a matter of adding one line to your Angular config: $locationProvider.html5Mode(true);

In case you’d like to know exactly where you should be adding this line, here’s my complete app.js file (which includes the relevant line and also a bunch of irrelevant crap). Notice that I had to specifically add $locationProvider as a parameter to the anonymous function I’m passing to the first app.config.

// app/scripts/app.js

'use strict';

/**
 * @ngdoc overview
 * @name lunchHubApp
 * @description
 * # lunchHubApp
 *
 * Main module of the application.
 */
var app = angular.module('lunchHubApp', [
  'ngAnimate',
  'ngCookies',
  'ngResource',
  'ngRoute',
  'ngSanitize',
  'ngTouch',
  'rails',
  'ng-token-auth'
]);

app.config(function($routeProvider, $locationProvider) {
  $locationProvider.html5Mode(true);
  $routeProvider
    .when('/', {
      templateUrl: 'views/main.html',
      controller: 'MainCtrl'
    })
    .when('/groups', {
      templateUrl: 'views/groups.html',
      controller: 'GroupsCtrl'
    })
    .when('/sign_in', {
      templateUrl: 'views/user_sessions/sign_in.html',
      controller: 'UserSessionsCtrl'
    })
    .otherwise({
      redirectTo: '/'
    });
});

app.config(function($authProvider) {
  $authProvider.configure({
    apiUrl: '/api'
  });
});

app.factory('Group', ['railsResourceFactory', function (railsResourceFactory) {
  return railsResourceFactory({ url: '/api/groups', name: 'group' });
}]);

You will of course also have to remove the #/ portion from any of your links. So if you have a link to /#/sign_in, you’ll have to change it to /sign_in. Hopefully you’re making this change early enough in your application’s life that that change is not a very big deal.

You’ll also need to change your asset references from relative to absolute. So <script src="scripts/app.js"></script> will need to become <script src="/scripts/app.js"></script>.

The Reload Problem (And How To Fix It)

Like I said, if you enable pushState in Angular and that’s all you do, it will appear to work as you click around to your different links. But if you reload any page other than the root URL, it won’t work. Let me explain that in detail.

If you navigate to http://localhost:9000/ (which is known as the root URL) and then click on a link that goes to /sign_in, that state change (not location change because this is a single-page application) will be going through Angular’s routing. We’re good so far.

But if you’re at http://localhost:9000/sign_in and you click reload, it doesn’t work. The reason is that your server is now trying to find some page that lives at http://localhost:9000/sign_in and, correctly, it’s determining that such a page does not exist. And again, correctly, it’s showing you an error.

What we need to do is kind of trick the server a little bit. When we get a request for http://localhost:9000/sign_in, we need to actually serve up http://localhost:9000/ and then tell Angular that we want to be at http://localhost:9000/sign_in.

And by the way, http://localhost:9000/ and http://localhost:9000/index.html are functionally equivalent, and in our redirections we’ll be talking about /index.html. I mention this so you don’t get confused by the /index.html references.

Development Page Reloads

The solution in both production and development is to add a little redirect that takes you from whatever URI is requested and puts you at index.html. In development we add the redirect to Grunt because Grunt is serving our client-side part of our application. In production we add it to Rack because Rack is serving the whole thing.

Before we add the rewrite to Grunt we need to install the connect-modrewrite npm module.

$ npm install --save connect-modrewrite

Here’s the rewrite rule we add. There are a couple cases where a redirect would not make sense: a) when we’re dealing with an API call (which is necessarily a Rails thing and doesn’t involve HTML5 pushState) and b) when we’re serving an actual file. So our rewrite rule says “redirect anything that’s not an API call or a file.”

We add these couple lines to Gruntfile.js. If it’s not entirely clear where these lines go, don’t worry. I included my full Gruntfile.js below so you can see exactly where they go.

// Gruntfile.js

// This goes near the beginning of Gruntfile.js.
var modRewrite = require('connect-modrewrite');

// This goes inside of your "connect" options.
var middlewares = [
  // Redirect anything that's not a file or an API call to /index.html.
  // This allows HTML5 pushState to work on page reloads.
  modRewrite(['!/api|/assets|\\..+$ /index.html']),
  // any other middlewares
];

Here’s my entire Gruntfile.js:

// Gruntfile.js

// Generated on 2014-07-18 using generator-angular 0.9.5
'use strict';

// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'

module.exports = function (grunt) {

  // Load grunt tasks automatically
  require('load-grunt-tasks')(grunt);

  // Time how long tasks take. Can help when optimizing build times
  require('time-grunt')(grunt);

  // Configurable paths for the application
  var appConfig = {
    app: require('./bower.json').appPath || 'app',
    dist: '../public'
  };

  var modRewrite = require('connect-modrewrite');

  // Define the configuration for all the tasks
  grunt.initConfig({

    protractor: {
      options: {
        keepAlive: true, // If false, the grunt process stops when the test fails.
        noColor: false, // If true, protractor will not use colors in its output.
        args: {
          // Arguments passed to the command
        }
      },
      run: {
        options: {
          configFile: 'protractor_conf.js', // Target-specific config file
          args: {} // Target-specific arguments
        }
      },
    },

    uglify: {
      options: {
        beautify: true,
        mangle: true
      }
    },

    // Project settings
    yeoman: appConfig,

    // Watches files for changes and runs tasks based on the changed files
    watch: {
      bower: {
        files: ['bower.json'],
        tasks: ['wiredep']
      },
      js: {
        files: ['<%= yeoman.app %>/scripts/{,*/}*.js'],
        tasks: ['newer:jshint:all'],
        options: {
          livereload: '<%= connect.options.livereload %>'
        }
      },
      jsTest: {
        files: ['test/spec/{,*/}*.js'],
        tasks: ['newer:jshint:test', 'karma']
      },
      compass: {
        files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        tasks: ['compass:server', 'autoprefixer']
      },
      gruntfile: {
        files: ['Gruntfile.js']
      },
      livereload: {
        options: {
          livereload: '<%= connect.options.livereload %>'
        },
        files: [
          '<%= yeoman.app %>/{,*/}*.html',
          '.tmp/styles/{,*/}*.css',
          '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
        ]
      }
    },

    // The actual grunt server settings
    connect: {
      options: {
        port: 9000,
        // Change this to '0.0.0.0' to access the server from outside.
        hostname: 'localhost',
        livereload: 35729
      },
      proxies: [
        {
          context: '/api',
          host: 'localhost',
          port: 3000
        }
      ],
      livereload: {
        options: {
          open: true,
          middleware: function (connect, options) {
            if (!Array.isArray(options.base)) {
              options.base = [options.base];
            }

            // Setup the proxy
            var middlewares = [

              // Redirect anything that's not a file or an API call to /index.html.
              // This allows HTML5 pushState to work on page reloads.
              modRewrite(['!/api|/assets|\\..+$ /index.html']),

              require('grunt-connect-proxy/lib/utils').proxyRequest,
              connect.static('.tmp'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];

            // Make directory browse-able.
            var directory = options.directory || options.base[options.base.length - 1];
            middlewares.push(connect.directory(directory));

            return middlewares;
          }
        }
      },
      test: {
        options: {
          port: 9001,
          middleware: function (connect, options) {
            if (!Array.isArray(options.base)) {
              options.base = [options.base];
            }

            // Setup the proxy
            var middlewares = [
              require('grunt-connect-proxy/lib/utils').proxyRequest,
              connect.static('.tmp'),
              connect().use(
                '/bower_components',
                connect.static('./bower_components')
              ),
              connect.static(appConfig.app)
            ];

            // Make directory browse-able.
            var directory = options.directory || options.base[options.base.length - 1];
            middlewares.push(connect.directory(directory));

            return middlewares;
          }
        },
        appendProxies: false,
        proxies: [
          {
            context: '/api',
            host: 'localhost',
            port: 3001
          }
        ]
      },
      dist: {
        options: {
          open: true,
          base: '<%= yeoman.dist %>'
        }
      }
    },

    // Make sure code styles are up to par and there are no obvious mistakes
    jshint: {
      options: {
        jshintrc: '.jshintrc',
        reporter: require('jshint-stylish')
      },
      all: {
        src: [
          'Gruntfile.js',
          '<%= yeoman.app %>/scripts/{,*/}*.js'
        ]
      },
      test: {
        options: {
          jshintrc: 'test/.jshintrc'
        },
        src: ['test/spec/{,*/}*.js']
      }
    },

    // Empties folders to start fresh
    clean: {
      dist: {
        files: [{
          dot: true,
          src: [
            '.tmp',
            '<%= yeoman.dist %>/{,*/}*',
            '!<%= yeoman.dist %>/.git*'
          ]
        }]
      },
      server: '.tmp'
    },

    // Add vendor prefixed styles
    autoprefixer: {
      options: {
        browsers: ['last 1 version']
      },
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/styles/',
          src: '{,*/}*.css',
          dest: '.tmp/styles/'
        }]
      }
    },

    // Automatically inject Bower components into the app
    wiredep: {
      options: {
      },
      app: {
        src: ['<%= yeoman.app %>/index.html'],
        ignorePath:  /\.\.\//
      },
      sass: {
        src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
        ignorePath: /(\.\.\/){1,2}bower_components\//
      }
    },

    // Compiles Sass to CSS and generates necessary files if requested
    compass: {
      options: {
        sassDir: '<%= yeoman.app %>/styles',
        cssDir: '.tmp/styles',
        generatedImagesDir: '.tmp/images/generated',
        imagesDir: '<%= yeoman.app %>/images',
        javascriptsDir: '<%= yeoman.app %>/scripts',
        importPath: './bower_components',
        httpImagesPath: '/images',
        httpGeneratedImagesPath: '/images/generated',
        httpFontsPath: '/styles/fonts',
        relativeAssets: false,
        assetCacheBuster: false,
        raw: 'Sass::Script::Number.precision = 10\n'
      },
      dist: {
        options: {
          generatedImagesDir: '<%= yeoman.dist %>/images/generated'
        }
      },
      server: {
        options: {
          debugInfo: true
        }
      }
    },

    // Renames files for browser caching purposes
    filerev: {
      dist: {
        src: [
          '<%= yeoman.dist %>/scripts/{,*/}*.js',
          '<%= yeoman.dist %>/styles/{,*/}*.css',
          '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
          '<%= yeoman.dist %>/styles/fonts/*'
        ]
      }
    },

    // Reads HTML for usemin blocks to enable smart builds that automatically
    // concat, minify and revision files. Creates configurations in memory so
    // additional tasks can operate on them
    useminPrepare: {
      html: '<%= yeoman.app %>/index.html',
      options: {
        dest: '<%= yeoman.dist %>',
        flow: {
          html: {
            steps: {
              js: ['concat', 'uglifyjs'],
              css: ['cssmin']
            },
            post: {}
          }
        }
      }
    },

    // Performs rewrites based on filerev and the useminPrepare configuration
    usemin: {
      html: ['<%= yeoman.dist %>/{,*/}*.html'],
      css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
      options: {
        assetsDirs: ['<%= yeoman.dist %>','<%= yeoman.dist %>/images']
      }
    },

    // The following *-min tasks will produce minified files in the dist folder
    // By default, your `index.html`'s <!-- Usemin block --> will take care of
    // minification. These next options are pre-configured if you do not wish
    // to use the Usemin blocks.
    // cssmin: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/styles/main.css': [
    //         '.tmp/styles/{,*/}*.css'
    //       ]
    //     }
    //   }
    // },
    // uglify: {
    //   dist: {
    //     files: {
    //       '<%= yeoman.dist %>/scripts/scripts.js': [
    //         '<%= yeoman.dist %>/scripts/scripts.js'
    //       ]
    //     }
    //   }
    // },
    // concat: {
    //   dist: {}
    // },

    imagemin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.{png,jpg,jpeg,gif}',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    svgmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '<%= yeoman.app %>/images',
          src: '{,*/}*.svg',
          dest: '<%= yeoman.dist %>/images'
        }]
      }
    },

    htmlmin: {
      dist: {
        options: {
          collapseWhitespace: true,
          conservativeCollapse: true,
          collapseBooleanAttributes: true,
          removeCommentsFromCDATA: true,
          removeOptionalTags: true
        },
        files: [{
          expand: true,
          cwd: '<%= yeoman.dist %>',
          src: ['*.html', 'views/{,*/}*.html'],
          dest: '<%= yeoman.dist %>'
        }]
      }
    },

    // ngmin tries to make the code safe for minification automatically by
    // using the Angular long form for dependency injection. It doesn't work on
    // things like resolve or inject so those have to be done manually.
    ngmin: {
      dist: {
        files: [{
          expand: true,
          cwd: '.tmp/concat/scripts',
          src: '*.js',
          dest: '.tmp/concat/scripts'
        }]
      }
    },

    // Replace Google CDN references
    cdnify: {
      dist: {
        html: ['<%= yeoman.dist %>/*.html']
      }
    },

    // Copies remaining files to places other tasks can use
    copy: {
      dist: {
        files: [{
          expand: true,
          dot: true,
          cwd: '<%= yeoman.app %>',
          dest: '<%= yeoman.dist %>',
          src: [
            '*.{ico,png,txt}',
            '.htaccess',
            '*.html',
            'views/{,*/}*.html',
            'images/{,*/}*.{webp}',
            'fonts/*'
          ]
        }, {
          expand: true,
          cwd: '.tmp/images',
          dest: '<%= yeoman.dist %>/images',
          src: ['generated/*']
        }, {
          expand: true,
          cwd: '.',
          src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*',
          dest: '<%= yeoman.dist %>'
        }]
      },
      styles: {
        expand: true,
        cwd: '<%= yeoman.app %>/styles',
        dest: '.tmp/styles/',
        src: '{,*/}*.css'
      }
    },

    // Run some tasks in parallel to speed up the build process
    concurrent: {
      server: [
        'compass:server'
      ],
      test: [
        'compass'
      ],
      dist: [
        'compass:dist',
        'imagemin',
        'svgmin'
      ]
    },

    // Test settings
    karma: {
      unit: {
        configFile: 'test/karma.conf.js',
        singleRun: true
      }
    }
  });


  grunt.registerTask('serve', 'Compile then start a connect web server', function (target) {
    if (target === 'dist') {
      return grunt.task.run(['build', 'connect:dist:keepalive']);
    }

    grunt.task.run([
      'clean:server',
      'wiredep',
      'railsServer:development',
      'concurrent:server',
      'autoprefixer',
      'configureProxies',
      'connect:livereload',
      'watch'
    ]);
  });

  grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) {
    grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
    grunt.task.run(['serve:' + target]);
  });

  grunt.registerTask('test', [
    'clean:server',
    'wiredep',
    'railsServer:test',
    'concurrent:test',
    'autoprefixer',
    'configureProxies:test',
    'connect:test',
    'protractor:run'
  ]);

  grunt.registerTask('build', [
    'clean:dist',
    'wiredep',
    'useminPrepare',
    'concurrent:dist',
    'autoprefixer',
    'concat',
    'ngmin',
    'copy:dist',
    'cdnify',
    'cssmin',
    'uglify',
    'filerev',
    'usemin',
    'htmlmin'
  ]);

  grunt.registerTask('default', [
    'newer:jshint',
    'test',
    'build'
  ]);

  grunt.registerTask('heroku:production', 'build');

  grunt.loadNpmTasks('grunt-connect-proxy');
  grunt.loadNpmTasks('grunt-protractor-runner');
  grunt.loadNpmTasks('grunt-rails-server');
};

Production Page Reloads

We do basically the same thing in production. We just achieve it in a different way. First, add the rack-rewrite gem to your Gemfile.

# Gemfile

gem 'rack-rewrite'

Then bundle install, of course.

$ bundle install

Then you’ll add a redirect rule to your config.ru that looks like this:

# config.ru

# This file is used by Rack-based servers to start the application.

use Rack::Rewrite do
  rewrite %r{^(?!.*(api|\.)).*$}, '/index.html'
end

require ::File.expand_path('../config/environment',  __FILE__)
run Rails.application

Now you’re in business. Enjoy!

How to Use RSpec to Test an Angular/Rails Single-Page Application

I struggled for some time deciding how to “properly” put together an integration test for an Angular/Rails single-page application. First I tried to drive Rails with Protractor but that felt weird. Then it dawned on me that the fact that the SPA-ness of my Rails app is just a detail. This is a Rails app that happens to be a single-page application. I can still use RSpec to test my SPA just like I would for a non-SPA.

But because my Rails API layer is (rightfully) unaware of the client layer under normal circumstances, it’s not possible to simply act as if the SPA is a non-SPA. RSpec doesn’t know anything about the UI. My solution to this problem is to simulate a deployment before my test suite runs. “Simulate a deployment” is my fancy way of saying “run a grunt build“. It’s actually super simple.

Before you implement anything described below, you’ll probably want to configure Grunt to run Rails and get HTML5 pushState working. You may also like to read my post on how to deploy an Angular/Rails single-page app, because part of what I do here overlaps with that, and might not seem to make complete sense without understanding how I do a deployment.

The Feature We’re Testing

The “feature” I’ll demonstrate testing is a really simple one: a static list of groups. This feature is unrealistically simple but we cover enough ground by testing it that it will (I think) be obvious when we’re done how you’d test something more complex.

Here’s the Angular controller I’m using:

// client/app/scripts/controllers/groups.js

'use strict';

/**
 * @ngdoc function
 * @name fakeLunchHubApp.controller:GroupsCtrl
 * @description
 * # GroupsCtrl
 * Controller of the fakeLunchHubApp
 */
angular.module('fakeLunchHubApp')
  .controller('GroupsCtrl', ['$scope', function ($scope) {
    $scope.groups = ['Group One', 'Group Two'];
  }]);

Here’s my view for it:

<!-- client/app/views/groups.html -->

Groups:

<ul ng-repeat="group in groups">
  <li>{{group}}</li>
</ul>

And here’s my route:

// client/app/scripts/app.js

.when('/groups', {
  templateUrl: 'views/groups.html',
  controller: 'GroupsCtrl'
})

Configuring Grunt’s Build Correctly

First, remove Rails’ public directory. Our public directory from now on will be replaced by the output of grunt build. (If you haven’t yet, I’d recommend reading How to Deploy an Angular/Rails Single-Page Application to Heroku.)

$ rm -rf public

In our Gruntfile we’ll change the grunt build output directory from dist to ../public – the same public we just deleted. Change this:

// client/Gruntfile

var appConfig = {
  app: require('./bower.json').appPath || 'app',
  dist: 'dist'
};

To this:

// client/Gruntfile

var appConfig = {
  app: require('./bower.json').appPath || 'app',
  dist: '../public'
};

Now if you run rails server and navigate to http://localhost:3000/, you should see your single-page app there, behaving exactly as it does when served by grunt serve.

Configuring RSpec

We’ll need to tell RSpec to run a grunt build before each integration test, and afterward we’ll want to kill the public directory we created. Add the following to spec/spec_helper.rb.

# spec/spec_helper.rb

config.before(:all, type: :feature) do
  system("grunt build --gruntfile #{Rails.configuration.gruntfile_location}")
end

config.after(:all, type: :feature) do
  FileUtils.rm_rf(Rails.root.join("public"))
end

Here’s my full spec/spec_helper.rb for reference:

# This file is copied to spec/ when you run 'rails generate rspec:install'
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'rspec/autorun'

# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }

# Checks for pending migrations before tests are run.
# If you are not using ActiveRecord, you can remove this line.
ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)

Capybara.javascript_driver = :selenium

# Includes rack-rewrite configuration so HTML5 pushState can function properly.
Capybara.app = Rack::Builder.new do
  eval File.read(Rails.root.join('config.ru'))
end 

RSpec.configure do |config|
  # ## Mock Framework
  #
  # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
  #
  # config.mock_with :mocha
  # config.mock_with :flexmock
  # config.mock_with :rr

  # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
  config.fixture_path = "#{::Rails.root}/spec/fixtures"

  # If you're not using ActiveRecord, or you'd prefer not to run each of your
  # examples within a transaction, remove the following line or assign false
  # instead of true.
  config.use_transactional_fixtures = false

  # If true, the base class of anonymous controllers will be inferred
  # automatically. This will be the default behavior in future versions of
  # rspec-rails.
  config.infer_base_class_for_anonymous_controllers = false

  # Run specs in random order to surface order dependencies. If you find an
  # order dependency and want to debug it, you can fix the order by providing
  # the seed, which is printed after each run.
  #     --seed 1234
  config.order = "random"

  config.before(:suite) do
    DatabaseCleaner.strategy = :truncation
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:all, type: :feature) do
    system("grunt build --gruntfile #{Rails.configuration.gruntfile_location}")
  end

  config.after(:all, type: :feature) do
    FileUtils.rm_rf(Rails.root.join("public"))
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.after(:each) do
    DatabaseCleaner.clean
  end
end

Notice the Rails.configuration.gruntfile_location in spec_helper.rb. This configuration setting doesn’t exist yet. We’ll need to define it. (By the way, this is just an arbitrary configuration setting I came up with. It seemed more appropriate to me to define the Gruntfile location as a config setting rather than hard-coding it in this spec_helper.rb.)

To define gruntfile_location, add this line to config/environments/test.rb:

config.gruntfile_location = "client/Gruntfile.js"

You’ll also need to install a few certain gems in order for all this to work, including capybaraselenium-webdriverdatabase_cleaner and compass. (database_cleaner is actually not strictly necessary for what we’re doing but my sample code includes it so your tests won’t run without it if you’re copying and pasting my code.)

source 'https://rubygems.org'

gem 'rails', '4.1.6'
gem 'rails-api'
gem 'spring', :group => :development

# Use PostgreSQL as the RDBMS.
gem 'pg'

# Use devise_token_auth for authentication.
gem 'devise_token_auth'

# Use rack-rewrite to allow use of HTML5 pushState.
gem 'rack-rewrite'

group :test do
  gem 'rspec-rails'
  gem 'capybara'
  gem 'selenium-webdriver'
  gem 'database_cleaner'
  gem 'compass'
end

Now you’ll need to do a bundle install, of course.

The Spec Itself

Here’s the spec I wrote to verify that the group list contains “Group One”.

# spec/features/group_spec.rb

require 'spec_helper'

feature 'Groups', js: true do
  scenario 'view' do
    visit '/groups'
    expect(page).to have_content('Group One')
  end
end

If you run rspec spec/features/group_spec.rb, it should pass.

Object-Oriented AngularJS Service Example

I’ve seen some examples of using AngularJS services online that follow the convention of FooService.someMethod() or BarService.whateverElse(). I was confused about how to use AngularJS without throwing all my useful object-oriented programming experience out the window. I’ve come up with a solution that I think I like.

Here’s the CoffeeScript class. It’s simple enough that I think it should be fairly self-explanatory. Don’t worry too much about the details of what it does. Just notice there’s a constructor and one instance method.

"use strict"

angular.module("myApp").service "Report", (Payment) ->

  class Report
    constructor: ->
      @startDate = moment().subtract(14, "days").format("MM/DD/YYYY")
      @endDate   = moment().format("MM/DD/YYYY")

    refresh: (options) ->
      Payment.query
        "between[startDate]": moment(options.startDate).format("YYYY-MM-DD")
        "between[endDate]":   moment(options.endDate).format("YYYY-MM-DD")
        "repId":              options.repId
        "issueId":            options.issueId

Here’s how I’m using my Report class in a controller:

"use strict"

angular.module("myApp").controller "PaymentsCtrl", ($scope, Report) ->

  report           = new Report()
  $scope.startDate = report.startDate
  $scope.endDate   = report.endDate
  $scope.rep       = {}
  $scope.issue     = {}

  $scope.refreshReport = ->
    report.refresh(
      startDate: $scope.startDate
      endDate:   $scope.endDate
      repId:     $scope.rep.id
      issueId:   $scope.issue.id
    ).then (rows) -> $scope.rows = rows

  $scope.refreshReport()

Notice how I’m not doing something like ReportService.refresh(). To me, that wouldn’t make complete sense. It makes a lot more sense to me to instantiate report and then say report.refresh().

By the way, I discovered this post after coming up with my idea. Ben’s way is almost identical to mine, although I think my way is ever so slightly cleaner. But the fact that someone else is doing roughly the same thing is probably evidence that this idea isn’t totally retarded.

How to Wire Up Ruby on Rails and AngularJS as a Single-Page Application (Gulp Version)

Why this tutorial exists

I wrote this tutorial because I had a pretty tough time getting Rails and Angular to talk to each other as an SPA. The best resource I could find out there was Ari Lerner’s Riding Rails with AngularJS. I did find that book very helpful and I thought it was really well-done, but it seems to be a little bit out-of-date by now and I couldn’t just plug in its code and have everything work. I had to do a lot of extra Googling and head-scratching to get all the way there. This tutorial is meant to be a supplement to Ari’s book, not a replacement for it. I definitely recommend buying the book because it really is very helpful.

Refreshed for 2015

I had written a tutorial with almost the same title last summer. In that tutorial I used Grunt instead of Gulp, HTML instead of HAML or Jade, and regular JavaScript instead of CoffeeScript or ES6. Not only do cooler alternatives to those traditional technologies exist today, they did back then, too. My tutorial was sorely in need of a reboot. So here it is.

The sample app

There’s a certain sample app I plan to use throughout AngularOnRails.com called Lunch Hub. The idea with Lunch Hub is that office workers can announce in the AM where they’d like to go for lunch rather than deciding as they gather around the door and waste half their lunch break. Since Lunch Hub is a real project with its own actual production code, I use a different project here called “Fake Lunch Hub.” You can see the Fake Lunch Hub repo here.

Setting up our Rails project

Instead of regular Rails we’re going to use Rails::API. I’ve tried to do Angular projects with full-blown Rails, but I end up with a bunch of unused views, which feels weird. First, if you haven’t already, install Rails::API.

1$ gem install rails-api

Creating a new Rails::API project works the same as creating a regular Rails project.

1$ rails-api new fake_lunch_hub -T -d postgresql

Get into our project directory.

1$ cd fake_lunch_hub

Create our PostgreSQL user.

1$ createuser -P -s -e fake_lunch_hub

Create the database.

1$ rake db:create

Now we’ll create a resource so we have something to look at through our AngularJS app. (This might be a good time to commit this project to version control.)

Creating our first resource

Add gem 'rspec-rails' to your Gemfile (in the test group) and run:

12$ bundle install$ rails g rspec:install

When you generate scaffolds from now on, RSpec will want to create all kinds of spec files for you automatically, including some kinds of specs (like view specs) that in my opinion are kind of nutty and really shouldn’t be there. We can tell RSpec not to create these spec files:

123456789101112131415161
718192021222324252
6272829303132
3334353637383940
require File.expand_path(‘../boot’, __FILE__) # Pick the frameworks you want:require “active_model/railtie”require “active_record/railtie”require “action_controller/railtie”require “action_mailer/railtie”require “action_view/railtie”require “sprockets/railtie”# require “rails/test_unit/railtie” # Require the gems listed in Gemfile, including any gems# you’ve limited to :test, :development, or :production.Bundler.require(*Rails.groups) module FakeLunchHub  class Application < Rails::Application    # Settings in config/environments/* take precedence over those specified here.    # Application configuration should go into files in config/initializers    # — all .rb files in that directory are automatically loaded.     # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.    # Run “rake -D time” for a list of tasks for finding time zone names. Default is UTC.    # config.time_zone = ‘Central Time (US & Canada)’     # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.    # config.i18n.load_path += Dir[Rails.root.join(‘my’, ‘locales’, ‘*.{rb,yml}’).to_s]    # config.i18n.default_locale = :de     config.generators do |g|      g.test_framework :rspec,        fixtures: false,        view_specs: false,        helper_specs: false,        routing_specs: false,        request_specs: false,        controller_specs: true    end  endend

(Now might be another good time to make a commit.)

In Lunch Hub, I want everybody’s lunch announcements to be visible only to other people in the office where they work, not the whole world. And there’s actually a good chance a person might want to belong not only to a group tied to his or her current workplace, but perhaps a former workplace or totally arbitrary group of friends. So I decided to create the concept of a Group in Lunch Hub. Let’s create a Group resource that, for simplicity, has only one attribute: name.

1$ rails g scaffold group name:string

Since groups have to have names, let’s set null: false in the migration. We’ll also include a uniqueness index.

1234567891011class CreateGroups < ActiveRecord::Migration  def change    create_table :groups do |t|      t.string :name, null: false       t.timestamps    end     add_index :groups, :name, unique: true  endend
1$ rake db:migrate

Now, if you run rails server and go to http://localhost:3000/groups, you should see empty brackets ([]). We actually want to be able to do http://localhost:3000/api/groups instead.

12345Rails.application.routes.draw do  scope ‘/api’ do    resources :groups, except: [:new, :edit]  endend

At the risk of being annoying, I wanted to include a realistic level of testing in the tutorial, at least on the server side.

1234567891011121314require ‘rails_helper’ RSpec.describe Group, :type => :model do  before do    @group = Group.new(name: “Ben Franklin Labs”)  end   subject { @group }   describe “when name is not present” do    before { @group.name = ” ” }    it { should_not be_valid }  endend

To make this spec pass you’ll of course need to add a validation:

123class Group < ActiveRecord::Base  validates :name, presence: trueend

We also have to adjust the controller spec RSpec spit out for us because RSpec’s generators are evidently not yet fully compatible with Rails::API. The generated spec contains an example for the new action, even though we don’t have a new action. You can remove that example yourself or you can just copy and paste my whole file.

12345678910111213141516171819202122232
42526272829303132333435363738394041424
34445464748495051525354555657585960616
26364656667686970717273747576777879808
18283848586878889909192939495969798991
00101102103104105106107108109110111112
11311411511611711811912012112212312412
51261271281291301311321331341351361371
381391401411421431441451461471481491501
51152
require ‘rails_helper’ # This spec was generated by rspec-rails when you ran the scaffold generator.# It demonstrates how one might use RSpec to specify the controller code that# was generated by Rails when you ran the scaffold generator.## It assumes that the implementation code is generated by the rails scaffold# generator.  If you are using any extension libraries to generate different# controller code, this generated spec may or may not pass.## It only uses APIs available in rails and/or rspec-rails.  There are a number# of tools you can use to make these specs even more expressive, but we’re# sticking to rails and rspec-rails APIs to keep things simple and stable.## Compared to earlier versions of this generator, there is very limited use of# stubs and message expectations in this spec.  Stubs are only used when there# is no simpler way to get a handle on the object needed for the example.# Message expectations are only used when there is no simpler way to specify# that an instance is receiving a specific message. RSpec.describe GroupsController, :type => :controller do   # This should return the minimal set of attributes required to create a valid  # Group. As you add validations to Group, be sure to  # adjust the attributes here as well.  let(:valid_attributes) {    skip(“Add a hash of attributes valid for your model”)  }   let(:invalid_attributes) {    skip(“Add a hash of attributes invalid for your model”)  }   # This should return the minimal set of values that should be in the session  # in order to pass any filters (e.g. authentication) defined in  # GroupsController. Be sure to keep this updated too.  let(:valid_session) { {} }   describe “GET index” do    it “assigns all groups as @groups” do      group = Group.create! valid_attributes      get :index, {}, valid_session      expect(assigns(:groups)).to eq([group])    end  end   describe “GET show” do    it “assigns the requested group as @group” do      group = Group.create! valid_attributes      get :show, {:id => group.to_param}, valid_session      expect(assigns(:group)).to eq(group)    end  end   describe “GET edit” do    it “assigns the requested group as @group” do      group = Group.create! valid_attributes      get :edit, {:id => group.to_param}, valid_session      expect(assigns(:group)).to eq(group)    end  end   describe “POST create” do    describe “with valid params” do      it “creates a new Group” do        expect {          post :create, {:group => valid_attributes}, valid_session        }.to change(Group, :count).by(1)      end       it “assigns a newly created group as @group” do        post :create, {:group => valid_attributes}, valid_session        expect(assigns(:group)).to be_a(Group)        expect(assigns(:group)).to be_persisted      end       it “redirects to the created group” do        post :create, {:group => valid_attributes}, valid_session        expect(response).to redirect_to(Group.last)      end    end     describe “with invalid params” do      it “assigns a newly created but unsaved group as @group” do        post :create, {:group => invalid_attributes}, valid_session        expect(assigns(:group)).to be_a_new(Group)      end       it “re-renders the ‘new’ template” do        post :create, {:group => invalid_attributes}, valid_session        expect(response).to render_template(“new”)      end    end  end   describe “PUT update” do    describe “with valid params” do      let(:new_attributes) {        skip(“Add a hash of attributes valid for your model”)      }       it “updates the requested group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => new_attributes}, valid_session        group.reload        skip(“Add assertions for updated state”)      end       it “assigns the requested group as @group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session        expect(assigns(:group)).to eq(group)      end       it “redirects to the group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => valid_attributes}, valid_session        expect(response).to redirect_to(group)      end    end     describe “with invalid params” do      it “assigns the group as @group” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session        expect(assigns(:group)).to eq(group)      end       it “re-renders the ‘edit’ template” do        group = Group.create! valid_attributes        put :update, {:id => group.to_param, :group => invalid_attributes}, valid_session        expect(response).to render_template(“edit”)      end    end  end   describe “DELETE destroy” do    it “destroys the requested group” do      group = Group.create! valid_attributes      expect {        delete :destroy, {:id => group.to_param}, valid_session      }.to change(Group, :count).by(-1)    end     it “redirects to the groups list” do      group = Group.create! valid_attributes      delete :destroy, {:id => group.to_param}, valid_session      expect(response).to redirect_to(groups_url)    end  end end

Now if you run all specs on the command line ($ rspec), they should all pass. We don’t have anything interesting to look at yet but our Rails API is now good to go.

Adding the client side

On the client side we’ll be using Yeoman, a front-end scaffolding tool. First, install Yeoman itself as well as generator-gulp-angular. (If you don’t already have npm installed, you’ll need to do that. If you’re using Mac OS with Homebrew, run brew install npm.)

12$ npm install -g yo$ npm install -g generator-gulp-angular

We’ll keep our client-side code in a directory called client. (This is an arbitrary naming choice and you could call it anything.)

1$ mkdir client && cd $_

Now we’ll generate the Angular app itself. When I ran it, I made the following selections:

  • Angular version: 1.3.x
  • Modules: all
  • jQuery: 2.x
  • REST resource library: ngResource (just because it’s the default and angularjs-rails-resource isn’t an option on the list)
  • Router: UI Router
  • UI framework: Bootstrap
  • Bootstrap component implementation: Angular UI
  • CSS preprocessor: Sass (Node)
  • JS preprocessor: CoffeeScript
  • HTML template engine: Jade
1$ yo gulp-angular fake_lunch_hub

If you have a Rails server running on port 3000, stop it for now because Gulp will also run on port 3000 by default. Start Gulp to see if it works:

1$ gulp serve

Gulp should now open a new browser tab for you at http://localhost:3000/#/ where you see the “‘Allo, ‘Allo” thing. Our Angular app is now in place. It still doesn’t know how to talk to Rails, so we still have to make that part work.

Setting up a proxy

The only way our front-end app (Angular and friends) will know about our back-end server (Rails) is if we tell our front-end app about our back-end app. The basic idea is that we want to tell our front-end app to send any requests to http://our-front-end-app/api/whatever to http://our-rails-server/api/whatever. Let’s do that now.

If you look inside client/gulp, you’ll notice there’s a file in there called proxy.js. I would like to have simply tweaked this file slightly to get our proxy working, but unfortunately I found proxy.js very confusing and difficult to work with. So I deleted it and set up the proxy a different way. Let’s delete proxy.js so it doesn’t confuse future maintainers.

1$ rm gulp/proxy.js

You’ll notice another file inside client/gulp called server.js. I found that minimal adjustment in this file was necessary in order to get the proxy working. Here’s what my server.js looks like after my modifications, which I’ll explain:

123456789101112131415161718192021222324252627282930313
233343536373839404142434445464748495051525354555657585960616263
‘use strict’; var gulp = require(‘gulp’);var browserSync = require(‘browser-sync’);var browserSyncSpa = require(‘browser-sync-spa’);var util = require(‘util’);var proxyMiddleware = require(‘http-proxy-middleware’);var exec = require(‘child_process’).exec; module.exports = function(options) {   function browserSyncInit(baseDir, browser) {    browser = browser === undefined ? ‘default’ : browser;     var routes = null;    if(baseDir === options.src || (util.isArray(baseDir) && baseDir.indexOf(options.src) !== -1)) {      routes = {        ‘/bower_components’: ‘bower_components’      };    }     var server = {      baseDir: baseDir,      routes: routes,      middleware: [        proxyMiddleware(‘/api’, { target: ‘http://localhost:3000’ })      ]    };     browserSync.instance = browserSync.init({      port: 9000,      startPath: ‘/’,      server: server,      browser: browser    });  }   browserSync.use(browserSyncSpa({    selector: ‘[ng-app]’// Only needed for angular apps  }));   gulp.task(‘rails’, function() {    exec(“rails server”);  });   gulp.task(‘serve’, [‘watch’], function () {    browserSyncInit([options.tmp + ‘/serve’, options.src]);  });   gulp.task(‘serve:full-stack’, [‘rails’, ‘serve’]);   gulp.task(‘serve:dist’, [‘build’], function () {    browserSyncInit(options.dist);  });   gulp.task(‘serve:e2e’, [‘inject’], function () {    browserSyncInit([options.tmp + ‘/serve’, options.src], []);  });   gulp.task(‘serve:e2e-dist’, [‘build’], function () {    browserSyncInit(options.dist, []);  });};

Here are the things I changed, it no particular order:

  1. Configured BrowserSync to run on port 9000 so Rails can run on its default port of 3000 without conflicts
  2. Added middleware that says “send requests to /api to http://localhost:3000
  3. Added a rails task that simply invokes the rails server command
  4. Added a serve:full-stack task that runs the regular old serve task, but first runs the rails task

You’ll have to install http-proxy-middleware before continuing:

1$ npm install –save-dev http-proxy-middleware

Now we can run our cool new task. Make sure neither Rails nor Gulp is already running somewhere.

1$ gulp serve:full-stack

Three things should now happen:

  1. The front-end server should come up on port 9000 instead of 3000.
  2. If you navigate to http://localhost:9000/api/foo, you should get a Rails page that says No route matches [GET] "/api/foo", which means
  3. Rails is running on port 3000.

Getting Rails data to show up in our client app

Now we’ll want to get some actual data to show up in the actual HTML of our Angular app. This is a pretty easy step now that we have the plumbing taken care of. First, create some seed data:

1234567891011121314# This file should contain all the record creation needed to seed the database with its default values.# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).## Examples:##   cities = City.create([{ name: ‘Chicago’ }, { name: ‘Copenhagen’ }])#   Mayor.create(name: ‘Emanuel’, city: cities.first) Group.create([  { name: ‘Ben Franklin Labs’ },  { name: ‘Snip Salon Software’ },  { name: ‘GloboChem’ },  { name: ‘TechCorp’ },])

Get the data into the database:

1$ rake db:seed

Now let’s modify src/app/index.coffee to include a state for groups:

12345678910111213angular.module ‘fakeLunchHub’, [‘ngAnimate’, ‘ngCookies’, ‘ngTouch’, ‘ngSanitize’, ‘ngResource’, ‘ui.router’, ‘ui.bootstrap’]  .config ($stateProvider, $urlRouterProvider) ->    $stateProvider      .state “home”,        url: “/”,        templateUrl: “app/main/main.html”,        controller: “MainCtrl”      .state “groups”,        url: “/groups”,        templateUrl: “app/views/groups.html”,        controller: “GroupsCtrl”     $urlRouterProvider.otherwise ‘/’

Then we add GroupsCtrl, which at this point is almost nothing:

12angular.module “fakeLunchHub”  .controller “GroupsCtrl”, ($scope) ->

(I manually created a new directory for this, src/app/controllers.)

Lastly, let’s create a view at src/app/views/groups.jade:

123div.container  div(ng-include=”‘components/navbar/navbar.html'”)  h1 Groups

If you now navigate to http://localhost:9000/#/groups, you should see a big h1 that says “Groups”. So far we’re not talking to Rails at all yet. That’s the very next step.

A good library for Angular/Rails resources is called, straightforwardly, angularjs-rails-resource. It can be installed thusly:

1$ bower install –save angularjs-rails-resource

Now let’s add two things to src/app/index.coffee: the rails module and a resource called Group.

1234567891011121314151617angular.module ‘fakeLunchHub’, [‘ngAnimate’, ‘ngCookies’, ‘ngTouch’, ‘ngSanitize’, ‘ngResource’, ‘ui.router’, ‘ui.bootstrap’, ‘rails’]  .config ($stateProvider, $urlRouterProvider) ->    $stateProvider      .state “home”,        url: “/”,        templateUrl: “app/main/main.html”,        controller: “MainCtrl”      .state “groups”,        url: “/groups”,        templateUrl: “app/views/groups.html”,        controller: “GroupsCtrl”     $urlRouterProvider.otherwise ‘/’   .factory “Group”, (RailsResource) ->    class Group extends RailsResource      @configure url: “/api/groups”, name: “group”

Now let’s add a line to our controller to make the HTTP request:

123angular.module “fakeLunchHub”  .controller “GroupsCtrl”, ($scope, Group) ->    Group.query().then (groups) -> $scope.groups = groups

And some code in our template to show the group names:

123456div.container  div(ng-include=”‘components/navbar/navbar.html'”)  h1 Groups   ul(ng-repeat=”group in groups”)    li {{ group.name }}

If you now visit http://localhost:9000/#/groups, you should see your group names there. Congratulations! You just wrote a single-page application. It’s a trivial and useless single-page application, but you’re off to a good start. In my experience the plumbing is the hardest part.

That’s all for now

I’ve heard requests for tutorials on basic CRUD operations in Angular/Rails, so keep your eye out for that in the near future. You can subscribe to my posts by leaving your email in the upper right corner.

Also, if you enjoyed this tutorial, you may also like my webinar version of this same tutorial (and then some). Thanks for reading.

Interview with Tero Parviainen, author of Build Your Own AngularJS

I talked with my friend Tero Parviainen about his outstanding Angular book, Build Your Own AngularJS, as well as what it was like to transition from Java to Rails to front-end, as well as CoffeeScript vs. ES6, Angular 2, and a few other topics. Enjoy.

Jason Swett:

Hello everyone, I’m Jason Swett, author of angularonrails.com. I’m here with my friend Tero, author of “Build Your Own AngularJS.” Tero why don’t you introduce yourself tell us who you are, what you do and that kind of thing.

Tero Parviainen:

I’m Tero Parviainen. I’m a programmer from Helsinki, Finland. And I’ve been doing web development for about 13 years professionally. I started from doing Java stuff in the early 2000s and then moved on to doing Ruby on Rails for several years and did a little bit of Clojure.

Still doing a little bit of [inaudible 0:00:50] . For the past couple of years I’ve really been doing front end development. JavaScript essentially.

Jason:

I would like to talk about those couple of transitions that you’ve done, because that’s a pretty common transition to write Java to Ruby.

Tero:

Yeah. It’s definitely seems to be.

Jason:

What prompted that transition for you?

Tero:

It was mostly that doing web applications in Java was really painful. It’s almost ten years ago now it goes only 2006 and 2007 time frame when Rails really started hitting their fees all over the place.

Back then it seemed like a lot more productive than Java which it was. I convinced my boss to try to use it with something. We failed miserably but we did not blame the framework luckily. We did second thing with Rails and that was a success.

I haven’t really looked back since to Java the language that much. I have been working with the JVA model Java virtual machine the whole time. I did a lot of JRuby at some point and turn, of course Clojure’s based on that. The language itself I really haven’t done any since Ruby.

Jason:

As far as Ruby and Rails to the front end and stuff, what prompted that transition and obviously, unlike Java and Ruby, JavaScript and Ruby can co-exist in the same application and so may be use them together. What prompted that transition and you still do the real stuff?

Tero:

At some point it became apparent that we have to do to some heavy front-end applications, because it wasn’t enough to have a little bit of jQuery sprinkled into your Rails application. I went into the rich Internet application thing quite early on when they were still called that.

Did it all off Google Web toolkit at some point which was Java. Then flex was one of those things that I did. The Flash action script thing. From there it…

I followed the industry. Backbone came out, then Angular, and now React. Pretty much following what everyone else is doing at this point. Seems like that at least.

Jason:

What did you think of Backbone?

Tero:

Backbone was really refreshing after the things I was doing before that, which was mostly Google Web toolkit, because it was so lightweight and so small. It seemed to encourage the application architecture that was also small and simple. I liked it.

Back then I didn’t really miss the things we have now with data binding and things like that that we get with newer frameworks, because I hadn’t tried that yet. Backbone seemed to be like everything you need. It doesn’t feel like that anymore, but it did then.

Jason:

How did you get into Angular from Backbone?

Tero:

It was most Angular, originally mostly because it was used in some project that I went into. I didn’t choose it. It was there. That’s how it started, but obviously since then I’ve heavily gotten into Angular.

Jason:

For me, it was as much of a career and business decision as a technical decision. I really wanted to bet on the winning horse. If you think back to when was it, mid 2000’s, the whole prototype versus jQuery thing.

Obviously, if you’ve invested your whole career in being the prototype guy, then that would be a lot of time, I don’t know if I want to say down the drain, but probably invested in jQuery, because that’s the framework out of those two that won.

I definitely didn’t want to back the loosing horse as far as the JavaScript application frameworks or whatever you want to call them goes. That was part of why I picked Angular as opposed to Ember or something like that.

I don’t really see Angular and Backbone as being really for the same thing. They do some of the same stuff, but they’re not really apples to apples I would say. What do you think about that?

Tero:

Yeah. They are not directly comparable mostly because Backbone is such a minimalist framework and it’s meant for different things essentially. Definitely it’s always a gamble what you choose especially if you want to make sure that it has longevity, which customers are very interested in.

They ask if this is something that we can maintain five years from now and looking at our track record as a front end community I don’t think we’ve done that well with that but its mostly because things are really new, still, at this point.

Jason:

And its almost like are things going to converge and get easier? Or are things going to accelerate even more? It’s not clear to me.

Tero:

Yeah. Both of those things are probably going to happen at the same time. The best bet is to really learn the standards. Like what’s underneath. What JavaScript is, the language itself and the standards around that and HTML and CSS and things like that.

Because their rate of change is also increasing, but its much slower than the frameworks on top. And the web is really pretty much the only platform that’s remained stable for about 20 years now.

Like if you open one of the first web applications that came out in the 90’s they still work when you open them in the current browsers. Can’t say the same of anything else.

Jason:

Yeah. That’s a really interesting point. I haven’t really thought about that too much. I haven’t had occasion to do that, but that would be really interesting, to open up some page from so incredibly long ago and see how it performs now.

I’ve also done the opposite a little bit where you open something in IE5 or something like that and see how bad it looks on that.

Tero:

Yeah. It may look terrible but it still usually works.

Jason:

Right. So speaking of all the different divergences and different options and accelerating rates of change and stuff like that. The JavaScript language itself is getting more splintered in that if I’m writing an Angular app today.

Let’s say, I can choose to write it in ES5, ES6, Typescript which I understand is going to be what Google wants to be the standard for Angular 2.

Or if you come from the Rails world, you’re probably familiar with CoffeeScript, you’re probably used to using that. And so its like, “What. Now do I switch from CoffeeScript to ES6 because that is where the puck is moving to?”

“Do I stick with CoffeeScript and if so what’s going to happen with that?” “Am I leaving anything on the table by not using ES6?” Its a decision that’s totally unclear to me. So what are your thoughts on all that stuff?

Tero:

Yeah. Its unclear to me too. I’m currently using ES5 in my customer project, but I definitely see myself going to ES6 for the next one.

Pretty much regardless of what framework or anything might be used there because it’s the next version of JavaScript that is going to be the most common denominator from now on and the Transclusion also get to know that. That it’s not really that painful to use either.

I did use CoffeeScript a lot at some point. It seems to me that half the good things about CoffeeScript are in ES6. Many of them are there. Not all of them like the expressions by default and the significant white space on the indentation and things like that are not there.

But those were always like the controversial features anyway. So its good that they are not in ES6 because that would have been a big fight but I don’t see myself using CoffeeScript much in the future. Probably ES6.

TypeScript I haven’t tried yet so I don’t really know what I think about that yet. Except that I’m comfortable doing untyped languages because of my history of Ruby and Clojure but now that I’ve been getting into the Angular 2 source code and reading that.

Comparing that to the Angular 1 source code, it’s a riftic difference. It’s largely because of the type of information that they have in those.

Jason:

You find Angular 2 source code easier to read than one?

Tero:

Definitely, yeah. It’s partly due to the fact that it’s new and it’s clean and it’s organized much better than Angular 1 that has this history of seven years or something like that — six years.

Behind it but also when you look at a function declaration in TypeScript you see the arguments are supposed to be of this type and it’s supposed to return this thing which you can’t see in ES5. It helps a lot when you’re trying to get into a code base and I don’t know what it’s supposed to do.

Jason:

Yeah, that’s a good point and I myself fear my background is in Ruby and before that PHP and so I don’t really know what I’m missing out on by not having that stuff but I can see how coming into a new project that I’m not familiar with…

Seeing what types of parameters certain functions expect and stuff like that, that can go a long way as in picking up that unfamiliar code base more quickly.

Tero:

Definitely, I get into trouble with my own applications as well. In ES5 because I can’t remember whether this was an array of objects or arrays of objects or whatever. Those are the things that you get over by debugging or adding console statements or whatever, to help in these situations.

The types are optional in TypeScript, I might be wrong about that. But that also helps that you don’t have to do it all the time if you don’t want to.

Jason:

Yeah, that’s my understanding too. At the very least you don’t have to, if you want to use ES6 with Angular. You don’t have to use TypeScript if you don’t want to.

Tero:

Yeah, they’re saying that very strongly that you don’t have to as an application developer use TypeScript. I don’t think you even have to use ES6, you can still do ES5 even it won’t be as pleasant but those are not requirements for us.

Jason:

How much have you gotten into Angular 2 so far. You mentioned reading the source code and there’s more and more becoming available that they’re putting out there. What are your thoughts on what’s emerged so far?

Tero:

I haven’t built anything with Angular 2 yet, I’ve gone into the source code mostly because it’s what I’ve been doing with Angular 1 for such a long time with my book project that I want to compare and contrast what they’re doing.

Jason:

Yeah and by the way that in totally been switched up, it’s in the middle of that but tell us about your book. Talk about that a little bit. What made you want to write that? How did that go? All that stuff.

Tero:

The book started from an article I wrote and that started from the fact that I wanted to learn how Angular works.

Angular seems to be one of those technologies that are a little bit difficult to learn because everyone was saying that like no one understands Transclusion or whatever it is, myths that are going around. I’ve had this experience with some technologies in the past like when I was doing Java.

There was a couple of frameworks and libraries that I never really understood. There are parts of Rails that I never really understood how they work, you have to do these things.

Then it works but you don’t really understand what it’s doing and it’s not pleasant to have to rely on, superstition when you’re using something that because you don’t know what it does. The best way to fix that is to look at the source code and see what’s in there.

I started doing that and then it wasn’t really enough to even do that, but really to understand and I started thinking maybe I should reverse engineer this and try to replicate…I started from the scopes and that changed detection system.

I wanted to understand how it works, how does it detect changes? When does it detect changes and how all that builds up to the framework. That’s what I did for the article. That was in the fall of 2013 and then I published that and it got a very good response.

People really seemed to like that approach because it seems that many others as well had the same problem as I did. I started writing that book then and it was a six month project. I started 18 months ago and I’m still doing [laughs] . It’s been very large project.

The book is 800 pages now and I’m not done yet.

Jason:

Yeah, I noticed. I bought the PDF and all these eBooks are on the short side. I looked at the number of pages and I’m like, “Is that right? It’s gigantic.”

Tero:

It is mostly because there’s a lot of code in there. I have this approach where every feature in Angular that I introduced, I write a failing test first, not the units test. Jasmine test case for it and then augment the production code base to make that test pass.

A test driven development the whole book. Those tests add up to a lot of lines and a lot of pages. In that sense the text in that book isn’t that long as it sounds when you say 800 pages. It’s a lot of code. It’s certainly longer than I had planned for it to be.

Jason:

It sounds like it. Both in terms of how many pages and how much time and effort it has taken you to put into it but you’re probably learning a lot as you do it, right?

Tero:

Yeah. The next topic I’m going into is now Transclusion and I hope to finally understand it now when I get into it because I still don’t, even though I’ve been using Angular for a couple of years. I know what it is but I don’t really understand how it works.

Jason:

There is stuff that you think you understand but you never really have to articulate it to anybody and I’ve heard things like that in the not too distant past where somebody asked me, “What is active record?” I was, “What do you mean what is active record?

It’s a thing we use every single day.” Then you try to articulate it, what it is and it’s like way, “I can’t put this into words and if I can’t put it into words maybe I don’t understand it as well as I can stand to.”

That’s definitely a valuable exercise to take those things that…you might have been using them for a while and you understand them well enough to use them, to understand it well enough to be able to teach it is sometimes a whole different thing.

Tero:

Oh definitely yeah. I totally agree with that.

[chuckling]

Jason:

Let’s talk about testing a little bit because it sounds like you’ve done quite a bit of that with Angular and that’s and that’s an area that I definitely struggle with in Angular and I think a lot of people do. A lot of people in my limited experience talk with other people about it.

They totally skip that part because they don’t understand it. If people could understand better how to get into that, maybe it wouldn’t be so intimidating and confusing and people will get into it more. For me, there seems to be a certain stress hold below which the test don’t have all that value.

Like when I’m testing for example a controller that’s a couple of lines a code maybe there’s an HTTP request happening and my test is like everything is marked out and all I’m testing is that a certain function got called.

It’s not testing for a mistake I usually make and so a test is seems to be worthless. It’s like, “Why did I even do this?” But then on the other hand, it feels totally irresponsible not to have any test at all. What are your thoughts on that?

Tero:

Yeah, I feel that same thing. Let’s see… I do skip those kinds of test these days, pretty much all the time. There was a time when I felt like I have to test everything because someone has read it from somewhere. But these days I usually don’t test drive everything at least.

I usually only write test when I feel like I’m unsure of something, which happens relatively quickly whenever there’s anything none trivial in the code. I test a lot but I don’t test everything.

Essentially things like the case that you mentioned where a controller pretty much calls a service or something and puts the results on some attribute.

That’s not something that I usually bother to test because as you said, everything the test does is expressing the same thing that’s in the code in a slightly different way and it doesn’t had much to testing.

But, as soon as there’s some logic in there’s some new statement or there’s anything none trivial than it usually becomes more impossible to do that. Also as to the challenges of testing in Angular, it always been difficult to test UIs as long as I can remember at least as long as I’ve been doing it then.

The way I usually deal with that is those parts of it that is interesting that I described. I pull out from UI code, to ordinary functions in some services or factories. Then I test them as pure logic.

Give them some input to see what the outputs are, try to pull out all the stuff that’s worth testing out from anything that’s really UI specific or even Angular specific, but you plain java script objects.

Jason:

That makes sense and it sounds like an angular too. They are making efforts to make things even more separated. You can get out that stuff even more conveniently to test it.

Tero:

Yeah, it does sound like that and I’m looking forward to working with that.

Jason:

I want to talk a little bit about directives because that’s something that took me a long time to even get to the level of understanding I’m at now. I still have a feeling that there’s something I’m not quite getting yet. Maybe you could talk a little bit about that.

What triggers you to say, “Hey, this looks like something that would be appropriate to write a directive for” that thing. When do you find yourself writing directives?

Tero:

It used to be the case when I find myself writing directives when I felt like I wish this tag or this element exist in HTML or the type bar or whatever.

Jason:

Yeah. You said that thing in your book about list. Like Paul Grimm had that thing about building a program from the bottom up with list as well as the top down and directives apply to that idea. I thought that made a lot of sense.

Tero:

Yeah, it made a lot of sense and that’s definitely one used case for directives but what I did not understand at first is that directives are really everything in Angular. You can use them for everything and you probably should because they…you can use them in so many different ways.

People often think of them as something that’s the alternative to controllers which was also what I looked at them at first because of I understood what a controller is from backbone.

And even things like Rails and thought that was an important concept in Angular and separate concept from directive but that’s not the case. The controllers in Angular are really a tiny little feature built on top of directives or at least the ng-controller are stuff.

That you use in your views. Whenever you use engine controller, you can make a directive. People have started to call these kinds of directives as component directives because they feel like components where you have a list of users or user view or whatever.

That’s exactly what Angular 2 is doing now. They’re adding this concept of components that are like web components but in the Angular framework. All they are, are most trimmed line API to the directives that we have in Angular where you have a directive that has a template, an isolate scope and controller.

For the past few months I have been writing everything from those on the UI level. So there’s no ng-controller anywhere. There’s no NG include anywhere. Everything is a component, so essentially, everything is a directive. But…

[crosstalk]

Tero:

…Directives is that there are these different used cases for them that they are not separating Angular 2, which are the components, the template directives and the decorators.

That’s what makes them difficult to think about when there are so many different used cases built into this one application service or this one concept, which they are now changing. That has been my path.

Jason:

May be we can dig a little bit into a concrete example to make that a little bit easier to understand on the example of a rapid payday loan website. Say I have a form and it has five fields or something like that. Obviously, we all know how you will rate in traditional HTML. Along what lines might that break up in terms of directives?

Tero:

That depends a bit on the application. The way I’ve done it is that you might have component called “Your user editor form” and that’s a directive essentially.

Then, when you dig into that component’s template, you see some more components like probably a text field component that consist of an input, the label for it and maybe some validation or configuration for it.

It’s the level of extraction on top of the standard HTML elements, that’s application specific. It’s really not that dissimilar from what they do in ember. Essentially, that’s what they do in Angular 2 that you can in Angular 1 as well.

Jason:

You find that this makes it easier to make you code less repetitive and stuff like that because I can imagine if you have a directive for that particular form that exist one time, but then inside of that you might have a form directive that’s generic to all your forms in that application.

It might not just be a regular form tag, there might be more to it because everywhere in your application, you want to have your forms a certain way and then same with you input fields and stuff like that. Does it take out a lot of the repetition?

Tero:

It takes out some repetition and it seems to essentially do that mostly in the sense that it makes things easier to reuse because you have these more contained things that you can put in different places.

The main problem I had with the ng-controller was mostly to do with how it makes things not in capture, like you have these inherited scopes and you start to share things via those scopes.

Then you are in trouble because of a sudden your controller depends on something magically being from the parent scope and you can see that everywhere. The way that introduces repetition is that you have a similar used case somewhere else in your app but you can’t take this code.

And put it there because you haven’t set thing up over there the right way or it’s difficult to use that same controller or the same template over there. What our components really help with that is that the components enforce this isolate scope.

Or you enforce it on yourself really but you this rule that every component has an isolate scope which means that all of its inputs are explicit. You have to pass them in as an argument. It can’t see anything else from the surrounding scope.

When you see a component used somewhere in your view, you see, “OK, it has the attributes, that’s what it needs and it doesn’t need anything else because it can’t see anything else.” Then you can say, “OK, I will use that same thing over here.” You can be relatively certain that it will work.

Jason:

OK, that makes sense. You wouldn’t want too much data to be implicitly flying around because then you’d have these hidden dependencies that would get pretty mysteriously, pretty quick, I can imagine.

Tero:

Yeah, that’s what happens to me at least. I have heard similar stories from other people.

Jason:

Taro, who would say your book, is good for? What kind of person will find it most interesting? Where can we find it? Where can we find you if you want to share your Twitter or anything like that? How can we find your book and then you?

Tero:

The book is not the first Angular book you should get if you are thinking of learning Angular. It’s probably the one you should get when you’ve built an app and felt that you don’t understand the framework as well as you would like.

You really want to get deep into the level where you understand how your framework works, so you can extend it when you need. You understand when it gives you strange areas where those come from and how you can fix them.

You have the mentor model of the tool that you’re using and that’s your application, there’s no barrier after which the code becomes mysterious, which quite often happens with frameworks. That’s really the purpose of the book.

I’m self-publishing that book which is on my website, which is Teropa, T-E-R-O-P-A.info, which is also hosting my blog where I write about Angular and other topics. You can get the book from there and you can also reach me on Twitter, which is the same handle there, Teropa, which is T-E-R-O-P-A.

Jason:

Awesome. Tero thanks a lot for talking with me, a lot of good stuff that you share. Go check out Tero’s book everybody. All right, thanks Tero.

Charles Max Wood Talks About His New Screencast Project, RailsClips

Here I talk with my friend Charles Max Wood about, among other things, his new project, RailsClips, a series of screencasts which kind of picks up where RailsCasts left off. Enjoy.

Jason Swett:

Hello everyone. I’m Jason Swett, author of angularonrails.com. I’m here with Charles Max Wood of Devchat.tv, among a number of other things and we’re here today to talk about that stuff and also RailsClips.

Chuck, how about you introduce yourself a little bit. Tell us who you are and then we can talk a little bit about your new thing, RailsClips.

Charles Max Wood:

All right. I am the host of five different podcasts about programming. I’ve been podcasting about programming for about seven years. I started out programming in Ruby on Rails about eight and a half years ago, nine years ago. That’s kind of where I got going.

We have Ruby Rogues which is the longest running show that I have currently. It’s been going for about four years. JavaScript Jabber is the largest podcast, the most popular one that I do. Adventures in Angular is the next most popular. We kind of hit both angles, Ruby and Angular, so that’s what we talk about. My other two shows are about freelancing and iFreaks.

I guess we’ll get into what RailClips is here in a moment. I’ve got a few other projects here and there that I’ve got going on. For the most part, I do consulting for various clients, building various applications, some using Angular, some not, but all of them in Rails.

Jason:

A question you’ve probably been asked before is how the heck do you do so many podcasts? That’s a lot of podcasts, and you also do the consulting, so how do you manage all of that?

Charles:

Not well. It’s hard. I think a lot of people kind of get in their heads that there’s this steady balance that you reach, so every week is the same, the same, the same, and it really isn’t that way. As far as the podcasts go, I record 3 podcasts on Tuesdays, and then 2 on Wednesdays, and then I have pretty much the rest of the week to do the rest of everything else that I’m doing.

As far as fitting in the consulting and everything else, it just depends on the week. Some weeks, I get in a whole bunch of time for clients, and some weeks, I really don’t. It just depends on what I’ve got going on.

Just as an example, we’re recording this on April 27th, 2015. Last week was RailsComp. The week before that, I was at MicroComp and New Media Expo. Basically I spent two weeks at conferences for business, podcasting and programming, and I didn’t get a whole lot of either done those weeks. For the most part, I just have to plan it out.

I schedule the podcast, we do it the same time every week, so that’s easy to make sure that it gets done every week. The rest of everything else is just a matter of good planning and prioritizing, and sometimes working a little bit late.

Jason:

I know what you mean. When I first started freelancing, I said, “I’m going to do 20 hours of billable work and spend 20 hours on my product, and I’ll just do that every week.” I quickly learned that is very unrealistic, right? It’s different every week.

Charles:

I mean a lot of it depends to on how long I think the current project is going to last with my client or clients, how much time I need to fill, how much money I need to make, and whether or not I have prospects for the next job. That 20 hours billable a week is about what I try and hit. Some weeks I spend another 20 hours trying to find my next gig.

Jason:

That definitely is the way it goes. You’re working on transitioning eventually to more product revenue, right?

Charles:

Yes.

Jason:

That’s kind of where RailsClips is part of that picture?

Charles:

Yeah, RailsClips. About 2 years ago, Ryan Bates disappeared from the Internet. He left behind about 420 episodes of RailsCasts, which is still a resource that I think a lot of Ruby on Rails developers use. Some of them are out of date. It still kind of gives you the landscape of where you’re working, even if the exact APIs or whatever aren’t the same.

A lot of people really like having the resource, but as I talked to more and more people, it became pretty apparent that they wanted up to date stuff. Some of these other technologies, they barely got a mention. Now, they’re kind of a big deal.

React has really come out of the woodwork since RailsCasts. A lot of people are interested in that, but there’s no RailsCast for it. Angular is another technology that was kind of starting to bloom when RailsCasts quit publishing. People want more stuff on that.

The other thing is that a lot of the other technologies out there have moved ahead. As you can imagine in open source, two years is a long time. I felt that was a good option, as far as being able to fill people’s need.

The other thing is that Ruby Rogues, just to throw a number out there, last time I looked Ruby Rogues was getting just over 20,000 downloads an episode. I felt I could reach out to those 20,000-ish people who were listening, on a weekly or semiweekly basis, and let them know. “Hey, look. I’m going to put together a resource for you,” and see where things go from there.

Since Ryan had already proved out the subscription model with RailsCasts Pro, and Avdi Grimm had proved it out with Ruby Tapas, I felt that was a good way where I could continue to produce content that I really care about, and that I could do it for community that I care about, and that they could help me pay my bills at the same time.

Jason:

I found that if you’re going to do something like a Kickstarter or something like that it can be really beneficial to have been building a list of people over the last however long who care about the stuff that you are talking about. They are kind of pre-qualified and pre-interested in the thing that you are thinking about providing. That way you don’t have to start from square one when you put it out there.

You can have an audience that exists already and say, “Hey, all you guys who have been listening to me and following me for the last couple of years or whatever, there’s this other thing that you might be interested in.”

Did you find that you got a lot of your Kickstarter backers from you audience that you have on Ruby Rogues?

Chuck:

I guess I should stop and rewind. The way that I launched RailsClips was by putting together a Kickstarter campaign. I put it up there, I thought, “OK, if I can raise $5,000 then that’s enough to kind of buy some free time to be able to put out a few months’ worth of episodes.”

The base pledge people could put up was $25 and the reward was six months’ worth of RailsClips. Kickstarter doesn’t allow you to do recurring things on your campaigns. What I had to do is actually put it together and I said, “I’ll put together a video series on how to build APIs on Ruby on Rails, and those are going to be the first months’ or twos’ worth of episode that you’ll get from RailsClips.”

That’s the way I could launch it and we wound up raising $7,000. I had a stretch goal for $7,000 that basically meant that I’m going be putting together a webinar on how to test your Rails apps, which I get a lot of requests for. I’m going to put a little bit of particular focus on how to test your JavaScript.

If you are doing Rails and Angular then this is going to be pretty helpful. Especially since this is coming out of my experience doing Rails and Angular, more than anything else.

It applies to the other frameworks. It applies to regular JavaScript. Obviously you can use Jasmine, or QUnit, or whatever to test those, and Karma works nicely. You get a little bit of PhantomJS. You can do this stuff, and it works the same for all of them.

Some of the approaches are a little bit different, but for the most part it’s the same technology. As a basic primer for that stuff — I don’t have to go too deep into, “Here’s how you test your Angular,” it’s just “Here’s how you get started testing your JavaScript.”

Jason:

That’s kind of an interesting topic and I had a meandering path when I started doing that. Obviously when I’m doing an app that’s just a traditional Rails app I test my models with RSpec. I do integration tests with RSpec and Capybara. That’s how it works, and it works so great.

Once I did a Rails API and an Angular front end, it became much less clear what the best way to do it might be. There were a number of options that were possible, but it was like, “What’s best?”

Obviously you can still test your models with RSpec — that’s not a problem at all. In the in-testing is more the part where you have some different options with different pros and cons. I first tried Protractor, and did it that way, and I’m like, “Maybe I can have the Angular side, spin up a Rails server and blah hlah blah.”

I did it and I got it to work, but it felt really wrong. I’m like, “What if I need to generate some data and put that in my database?” It didn’t make sense.

Then I did it from the other way. I’m like, “Hey, maybe I can just do a build on my Angular app and put that and have it build itself into my Rails public folder.” At that point it’s almost like the fact that my JavaScript happens to be Angular is just a detail that my tests don’t need to care about. So, I did end-to-end tests with RSpec that way.

I’m ever so slightly, not super happy with the fact that I have to have RSpec know about Angular and tell Gulp to do a build. That’s the least worse thing I’ve been able to come up with.

What’s your take on the whole thing?

Chuck:

I do want to get back to your question about Kickstarter and having an existing audience, but we’ll talk about this first. My feeling is that you unit test your Rails models like you said. If you have to unit test your controllers, then you’ve probably overcomplicated your controller actions.

I like the Skinny Controller, Fat Model and then I like Skinny Model Fat Lib somewhere else to simplify things. I will test the Libs, I will test the models, I will test the controllers where I need to which is not usually very often and I will do unit tests on that. I will do some kind of end-to-end tests with Capybara on the things that are just straight up html, there is a not a lot of JavaScript interaction in the page, or for my APIs because you can get Capybara around that, you can tell it to make the requests, you parse the response with the Jason Jem and then it works.

On the JavaScript side, I usually will unit test. I try and break things, in Angular I try to break them out into services, and then I will test the services. As much as I can push the services I kind of treat that as the Lib to the models.

I also tend to treat some of the services as kind of factories for objects or repos for objects and they basically hit the APIs, so I can simulate a lot of that stuff and I can get a good unit test around them. I will do the end-to-end tests and I will do those usually with something like Karma and I will pull in PhantomJS.

I have them a little bit with Protractor. I am not as big a fan of a Selenium WebDriver but I have used it before. The thing is with end-to-end tests, they are expensive as far as time and resources go because it takes longer and they chew up more CPU, more memory when they run.

I tend to keep those to a minimum and just test kind of the happy paths or the critical paths depending on it how you look at it. Basically, the happy path, this is the most common route that people are going to take through my app. That is what I want it to do.

If it’s a social network, it’s OK, go in and post something to my wall, blah blah blah. Go and make a friend. But there are only going to be two or three of those that have to happen in order to make the app worth using.

I also usually will end-to-end test something like if I have a payment process or something in there, where then I have some kind of system that pretends to be the payment processor or, in the case of like Stripe, I just have to hit the test end point. My end-to-end test will fail if I do not have Internet or I cannot connect to the test system but for the most part I am generally not in that place and I know if I am offline then I am expecting it to go all the way down the chain and then blow up.

But what that does then is — then I know OK, I can get paid, people can use the most common features and then all of the edge cases should be covered mainly with my unit test and with some integration test if I really feel like I need them here or there.

Jason:

How does that work mechanically with making the end-to-end test happen, if it is an Angular and Rails app?

Charles:

Generally what you wind up doing is it has to spin up a Rails server and that will load the app along with all of the static, usually compiled assets from your asset pipeline. I have used the Rails asset pipeline, I have looked some other options, I have used Grunt and Gulp to compile. Depending on the app one is usually easier than the other but I have not found that one wins more often than the other.

It just depends if it is really simple the asset pipeline is not a problem. You have to configure it a little bit. For the more complicated apps, a lot of times the Gulp processes work better. It just depends on what you are after and what you are dealing with.

Jason:

OK. Why do you say that Gulp works better for the bigger ones? I usually use Gulp myself but I don’t necessarily have a great reason for having chosen that route. I would be curious to hear your perspective on that?

Charles:

With the smaller sets of Angular functionality the asset pipeline works fine because you more or less chunk it out. In other words I am not building single page apps in those cases. I have got the Angular for each page and it is mostly self-contained.

I am not too awful worried about all the processes and stuff. The way that Rails handles the assets does not affect it much.

However with the larger apps or single page apps, especially things that have a lot of logic in them, the Gulp and Grunt tools, there are more of them that deal with more cases is what I found. You have the plug in that does all the Angular stuff, is it NGAnnotate or is that…? Anyway.

Jason:

That’s one, yeah.

Charles:

You pull that in and you can pull some other tools. They are sort of made to deal with Angular and you just do not have that kind of specialization in the asset pipeline.

Jason:

OK.

Charles:

That is kind of what I found.

Jason:

OK. I originally I think started using Gulp just because I felt like I am trying to be like the Angular Rails guy and if I am going to be that guy I should probably know Grunt and Gulp at least a little bit. I started down those paths and Gulp just seems kind of neat, so I stuck with that.

How much of RailsClips do you think will involve things like Angular, React, whatever other stuff besides Rails? It seems like it is pretty common now a days that if you are going to be building any complex application, there is probably going to be more to it than just rails is kind of my perspective on that.

What do you think, how much non-Rails stuff do you think you might include?

Charles:

At this point it is going to be fairly focused on Rails. I am going to say that 75 to 80 percent of it is going to be stuff that is either Rails or you bring in a Jem to do it with Rails. I foresee that some of it is going to be stuff like DevOps or deployment, which is probably more Ruby or Bash or just general server maintenance as opposed to Rails itself.

But it’s stuff that applies to Rails, some of the caching technologies, how to setup mem cache or Redus and how to use that as a cache. Some of that is going to be DevOps and some of it is going to be Rails.

What I am kind of hoping is that I can put some stuff in that relates to the frameworks one way or another, but it is hard to know exactly what that right balance is. If you make it too Angular-centric or too Ember-centric or too Backbone-centric then it is not going to be as useful to the people who are using other systems.

What I am hoping to do is periodically just have a week which would be two episodes or maybe three episodes that are like, OK, now we are going to take all the stuff we learned and we are going to apply it to the major frameworks. I can say look, you setup device, here is how you set it up so that you can just do it in the single page app or you click on the login button and then it opens something up here, then it does the work for you in an Ember app and in Angular app.

I may throw all those in and just say look, here some shortcuts for the various options you have, but I don’t think it is going to be very focused one way or another on those. The API stuff is going to be, if you want to format it for Ember this is generally how you are going to do it, if you are going to format for Angular, this is generally how you do it.

It is not going to be super focused on that stuff because what it really is, is it is about empowering Ruby on Rails developers to do stuff with Rails and if I go another level deeper then it is not as useful for other people.

Jason:

That makes sense. I thought Ryan Bates struck a good balance with RailsCasts, and he would do an intro to such and such thing. Like, “Here is how you might plug Angluar into Rails,” but then it’s just one episode, the intro thing.

If you want to go deeper with any other of those technologies there are places for that. A big part of the value of RailsCasts and RailsClips is the fact that it is Rails. If you use Rails chances are it’s going to apply to you.

Chuck:

I do intend to treat some of the frameworks, but I don’t intend to do it for very long at a time because people can tune out an episode or two in a row, but I don’t want people tuning out more than that.

Jason:

If they’re interested in any particular technology chances are there’s a podcast that you host or co-host about that technology.

Chuck:

[laughs]

Jason:

Chuck, where should people find you, where should people find RailsClips. Where should people go on the Internet if they want more stuff from you?

Chuck:

All of my stuff is at Devchat.tv. I am probably going to start my own blog again, pretty soon. That would be at CharlesMaxWood.com.

I am trying to figure out how I want to do that exactly, but I will probably windup doing something like Avdi Grimm has done, where he has this development blog, and then he has his other blog that’s all of the other stuff.

RailsClips will be at RailsClips.com or you can find it at Devchat.tv because that’s where it’s going to live. That’s pretty much that.

I do want to address real quickly the other question you asked as far as having an audience and Kickstarter. I looked at the numbers and 90 percent of the traffic that went to the Kickstarter campaign came from Devchat.tv. It came from the podcasts.

The other 10 percent were people that found it through Kickstarter. It makes a big difference.

Jason:

If somebody is thinking of doing a Kickstarter — maybe I might do someday a Kickstarter — probably a good piece of advice would be do something to start building an audience now so when you want to pull the trigger on that you have something to prime the pump.

I imagine part of the success with Kickstarter is that initial wave of backers. People are going be more likely to back something that’s already being backed than something that’s at zero dollars. That is definitely a valuable thing to have.

Chuck:

I did an interview on building an audience on the debossified podcast. I will put a link to that here in the chat and you can share that with the people who are watching this too.

The best place to find me is Devchat.tv. I’m on Twitter @cmaxw, that’s C-M-A-X-W. People can email me, [email protected], I love talking about this stuff.

Jason:

Awesome. All right, Chuck, thanks for talking with me and we’ll put links to all that stuff on the blog post, and we’ll be talking to you soon.

Chuck:

All right. Sounds great.