Angularjs and Rails

There's been a lot of writing about Angularjs, and for good reason -- it's awesome.

I'm going to leave the heavy lifting to the excellent docs and focus on what I struggled with most: project organization.

Most of my programming time these days is spent with Ruby on Rails. The conventions around CRUD are excellent, but front end organization is lacking.

Enter Angular.

This guide assumes a good understanding of both Angular and Rails.

Dependency injection and the asset pipeline work wonderfully together. Let's get them setup together.

Make a subfolder, with the same name as the app, in apps/assets/javascript and include the path in application.js.coffee

Within that folder create a set of folders not dissimilar from the normal Rails structure:

~/src/rails/cogs/

▾ app/
  ▾ assets/
    ▸ images/
    ▾ javascripts/
      ▾ cogs/
        ▸ controllers/
        ▸ filters/
        ▸ services/
          app.js.coffee.erb
        application.js.coffee
    ▸ stylesheets/
    ▸ templates/
  ▸ controllers/
  ▸ helpers/
  ▸ mailers/
  ▸ models/
  ▸ views/
▸ bin/
▸ config/
▸ db/
▸ lib/
▸ log/
▸ public/
▸ spec/
▸ tmp/
▸ vendor/
Cogs.sublime-project
config.ru
Gemfile
Gemfile.lock
Rakefile
README.md
Termfile

Initialize the app in the aptly named app.js.coffee.erb file.

# declare modules with require param (the array) then add to them without the require param
angular.module('appServices', ['ngResource', 'ng-rails-csrf'])
angular.module('cogs', ['nt-services', 'appServices', 'momentFilters']);

# configure the app module
angular.module('cogs')
  .config(['$routeProvider', '$httpProvider', (($routeProvider, $httpProvider) ->
    $routeProvider.
      when('/', {templateUrl: '<%%= asset_path("assets/home/_index.html") %>',   controller: HomeIndexCtrl}).
      otherwise({redirectTo: '/'})
    $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content')
  )
])

Why all the extensions you ask? Yep, it's that erb tag with the asset_path call which I'll explain in a minute.

Require the tree. You'll notice I'm using Rails 4 with turbolinks.

application.js.coffee

#= require jquery
#= require jquery_ujs
#= require turbolinks

#= require underscore
#= require moment

#= require twitter/bootstrap

#= require angular
#= require angular-resource

#= require_tree ./cogs

Ok, about that erb code in the app routing config: templateUrl: '<%%= asset_path("assets/home/_index.html") %>', we're using front end templates for angular, which we need to configure.

config/initializers/haml_template.rb

Rails.application.assets.register_mime_type 'text/html', '.html'
Rails.application.assets.register_engine '.haml', Tilt::HamlTemplate

The asset_path call will add the appropriate digest to the asset's url in environments with digests enabled.

Finally, you'll notice I'm using angular-resource. If we look closer at the project organization, you'll see the resources have all been defined in the services folder.

~/src/rails/cogs

▾ app/
  ▾ assets/
    ▸ images/
    ▾ javascripts/
      ▾ cogs/
        ▾ controllers/
          ▾ home/
              home_index_ctrl.js.coffee
        ▾ filters/
            moment.js.coffee
        ▾ services/
          ▾ nt/
              debouncer.js.coffee
          ▾ resource/
              meeting.js.coffee
              note.js.coffee
              person.js.coffee
            csrf.js.coffee
          app.js.coffee.erb
        application.js.coffee

person.js.coffee

angular.module('appServices').factory('Person', ['$resource', ($resource) ->
    Person = $resource('/people/:id/:action', { format: 'json' }, {
      search: { method:'GET', params:{}, isArray: true },
      update: { method:'PUT' }
    })
    return Person
  ])

The extra :id and :action params let you do fancy things like:

  # Person gets injected into our controller and somewhere along the line we call
  Person.$get({ action: 'say_hi', id: 2 })
  # to say hi to person 2

Ok, we've got our front end all organized, but how does that 'say_hi' get handled on the back end?

Rails 4 added jbuilder by default. I'll let you read up, but basically it allows you to define your json responses in a .jbuilder file as you would any other type of view for a given format.

I usually split it out like this:

app/views/people/index.json.jbuilder

json.array! @people do |person|
  json.partial! 'full', person: person
end

app/views/people/show.json.jbuilder

json.partial! 'full', person: @person

app/views/people/_full.json.jbuilder

json.id person.id
json.name person.name

The controller is boilerplate:

app/controllers/people_controller.rb

class PeopleController < ApplicationController
  before_action :set_person, only: [:show, :edit, :update, :destroy]

  # GET /people
  def index
    @people = Person.all
  end

  # GET /people/1
  def show
  end

  # GET /people/1/say_hi
  def say_hi
  end

  # GET /people/new
  def new
    @person = Person.new
  end

  # GET /people/1/edit
  def edit
  end

  # POST /people
  def create
    @person = Person.new(person_params)

    respond_to do |format|
      if @person.save
        format.html { redirect_to @person, notice: 'Person was successfully created.' }
        format.json { render :show }
      else
        format.html { render action: 'new' }
        format.json { render json: @person.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /people/1
  def update
    respond_to do |format|
      if @person.update(person_params)
        format.html { redirect_to @person, notice: 'Person was successfully updated.' }
        format.json { render :show }
      else
        format.html { render action: 'edit' }
        format.json { render json: @person.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /people/1
  def destroy
    @person.destroy
    redirect_to people_url, notice: 'Person was successfully destroyed.'
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_person
      @person = Person.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def person_params
      params[:person].permit(:first_name, :last_name, :email)
    end
end