Skip to content

Instantly share code, notes, and snippets.

@michael-harrison
Last active August 29, 2015 14:05
Show Gist options
  • Save michael-harrison/53f715fc7dda62308cb2 to your computer and use it in GitHub Desktop.
Save michael-harrison/53f715fc7dda62308cb2 to your computer and use it in GitHub Desktop.
Rails & the Angular road: Beginnings

This article expects that you already have done the 101 AngularJS journey. If you're new to AngularJS then you'd best checkout the AngularJS Tutorial or Code Academy - AngularJS Patterns Tutorial(NB: this is for Angular 1.0.6) before continuing with this article. What we're really focusing on here are the decisions I've made and nuances discovered during the transition of a Rails app with the least resistance using the following stack:

  • Rails 4.0.x
  • Haml
  • Sass
  • Coffee
  • Devise with CanCan
  • jQuery
  • Twitter Bootstrap
  • PostgeSQL
  • Unicorn

As a part of the transition I wanted to reuse much of the work I had already done. So reuse of items such as markup and client side code in CoffeeScript were important. I didn't want to go down the track of having to rewrite large chunks of code. Ideally it would be good to transition all of those items releasing my new Angular application form the Rails asset pipeline but Rome wasn't built in a day: small steps.

Getting Started

Installation

You can follow the bootstrapping steps however as with most things in the ruby world: there's a gem for that :) With the path of least resistance in mind I chose to use the angularjs-rails gem. The maintainers of the gem have implemented an auto update mechanism that updates the gem as new versions of AngularJS become available with gem version bumps match the AngularJS version where practical. The world of JS frameworks is a fast moving one so be sure to include the version when adding the gem to your Gemfile otherwise you'll be in for a world of hurt.

gem 'angular-rails', '1.2.21'

Activation

The beauty of AngularJS is you can choose to apply it where you need it. So if you've got a large application that you what to incrementally move over to AngularJS then it's totally possible. Using the ngApp directive you can choose place in rendered HTML where the Angular application will live. In the sample below we have a single page application with the ngApp directive being applied to the html element (see %html{ 'ng-app' => 'MoreBeerOnline' }). This could as easily be applied to a div on a particular view giving you the option to angularise one page of your Rails app.

app/views/layouts/application.html.haml

!!!
%html{ 'ng-app' => 'AngularMusic' }
  %head
    %meta{:name => "viewport", :content => "width=device-width, initial-scale=1.0"}
    %title= content_for?(:title) ? yield(:title) : 'Angular Music'
    %meta{:name => "description", :content => "#{content_for?(:description) ? yield(:description) : 'Angular Music'}"}
    = favicon_link_tag 'favicon.ico'
    = stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true
    = javascript_include_tag "application", "data-turbolinks-track" => true
    = csrf_meta_tags
  %body
    #container
      %ng-view

View Transitions

For my project making the transition to AngularJS didn't happen overnight and it's still ongoing. To maintain a functional application during the transition I took an incremental approach. I identified discrete sections of my application (e.g. User Management, Fiscal Statements, etc.) then converted each of them to AngularJS.

Redoing all of the markup back into plain old HTML was looking to be a fair bit work. So I did a bit of searching and I found I could use the tilt gem for Haml in my Angular views. The meant making sure my view files were in the assets directory so they were included in the asset pipeline. Tilt isn't just for Haml, it supports many other template engines like Slim, Haml, Erb, CoffeeScript, etc. To enable it all you need to do is to create the following initialiser:

config/initializers/angular_assets.rb

Rails.application.assets.register_engine('.haml', Tilt::HamlTemplate)

If you have a mix of template engines then you can add more. For my project all of the markup was in HAML so all I needed was the .haml to be registered. Once in place my views compiled down to HTML as part of the asset pipeline.

This isn't a free ride you will still need to covert all of the readers partials to ng-includes and change references to Rails controller attributes to Angular controller attributes.

Everything in it's right place?

With Angular you have a free hand to organise your application files where you want them. This can be a blessing and a curse at the same time. While you can craft a beatify file structure it will be particular to your style application and you will have to educate new developers as they come to work on your codebase. For my application I chose to replicate something fairly similar to Rails but living in the app/javascript directory. So my resulting structure looks like the following:

app/
  assets/
    javascripts/
      models/
        songs.js.coffee
        ...
      controllers/
        songs_controller.js.coffee
        authentication_controller.js.coffee
        ...
      views/
        login.html.haml
        password_reset.html.haml
        ...
      common/
    application.js.coffee
    router.js.coffee

You'll all be familiar with models,controllers, views and application.js.coffee. There are some extras I've added:

  • common/: This directory is where I'm keeping extentions to AngularJS that are used throughout the application.

  • router.js.coffee: Think of this as the AngularJS version of config/routes.rb. Here's what it might look like

    @AngularMusic.config [
      "$routeProvider"
      "$locationProvider"
      ($routeProvider, $locationProvider) ->
        $locationProvider.html5Mode(true)
    
        $routeProvider
        .when("/login",
          templateUrl: "/assets/views/login.html"
          controller: 'AuthenticationController'
        )
        .when("/password_reset",
          templateUrl: "/assets/views/password_reset.html"
          controller: 'PasswordController'
        )
        .when("/change_password",
          templateUrl: "/assets/views/change_password.html"
          controller: 'PasswordController'
        )
        .when('/home',
          templateUrl: "/assets/views/home.html"
        )
        .when('/balances',
          templateUrl: "/assets/views/songs.html"
          controller: "SongsController"
        )
        .otherwise(redirectTo: "/login")
    ]

Order matters

As Rails developers we're very used to seeing the inclusion of javascripts in the assets:

app/assets/javascripts/application.js.coffee

#= require jquery
#= require jquery_ujs
#= require turbolinks
#= require bootstrap-sprockets
#= require_tree .

Unfortunately we can just rely on the require_tree . to include all of our javascript files for angular. The problem with that method is that it will include everything in alphabetical order not the dependency order that Angular requires. So it becomes a manual task where I had to handcraft the order as follows:

app/assets/javascripts/application.js.coffee

#= require jquery
#= require jquery_ujs
#= require turbolinks
#= require bootstrap-sprockets

#= require angular
#= require angular-mocks
#= require angular-resource
#= require angular-route
#= require_self
#= require_tree ./common
#= require_tree ./models
#= require_tree ./controllers
#= require router

@AngularMusicControllers = angular.module("AngularMusic.Controllers", [])
@CommonForms = angular.module("common.forms", [])
@AngularMusic = angular.module("AngularMusic", [
  "ngRoute"
  "ngResource"
  "ngDialog"
  "common.authentication"
  "common.error_handling"
  "common.forms"
  "AngularMusic.Controllers"
])

You'll notice directly after all of the requires there's the creation of the AngularJS application via using Angular's modules. This may seem like it's in the wrong place but that's sorted out by the require_sef.

Rails Routing

With the separation of the the UI control from Rails it's now possible to have many versions your application running while you do updates. To allow for this the API provided by Rails needs to be versioned. Following is a sample for routing:

config/routes.rb

AngularMusic::Application.routes.draw do

  namespace :api do
    namespace :v1 do
      resources :songs
    end
  end

  root to: 'home#index'
  devise_for :users, controllers: {sessions: 'sessions', passwords: 'passwords'}
  get 'home' => 'home#index'
  get '*path', to: 'home#index'
end

Note the routing outside of the API all heads to one place: home#index. This geared for a one page application. You do variations on this depending on how you want to transition your application leaving old routes in place and not including get '*path', to: 'home#index' which is a catch all for any unknown routes.

Be RESTful

You can use $http.get, $http.post like the following:

AngularMusic.factory "Song", ($http) ->
  index: ->
    $http.get "/api/v1/songs"
  song: (id) ->
    $http.get "/api/v1/songs/:id"

  #... etc

However to make the flow easy for development with a RESTful API like that Rails provides it's best to use ngResource.

@AngularMusic = angular.module("AngularMusic", ["ngResource"])

AngularMusic.factory 'Song', ($resource) ->
  Song = $resource "/api/v1/songs/:id",
    { id: "@id" }

  return Song

It implements RESTful methods making for less code, provides a clear standard and fits in well with Rails:

 Song.get # HTTP GET
 Song.query # HTTP GET
 Song.save # HTTP PUT
 Song.delete # HTTP DELETE
 Song.remove # HTTP REMOVE

Testing

Installing

Angular's focus on testing goes hand and hand with the very active Ruby testing community. There are a number of testing frameworks to choose from jasmine, mocha and QUint to name a few. The one that stood out for me is Jasmine 2.0 which closely resembles RSpec. It's relatively easy to setup however I had use a gem to run the tests due to the need for asset compilation during testing. Finding a gem that supports Jasmine 2.0 rather than the older Jasmine 1.3 proved difficult. Both jasminerice and teaspoon didn't support it so I decided to upgrade one them. I chose jasminerice as it was by far the simpler of the two. Since the pull request hasn't been accepted yet you can check out my Jasmine 2.0 support at https://github.com/michael-harrison/jasminerice which has full details on installation.

Full Stack Testing

Full stack testing can be done with your new Angular app however you will get intermittent failures due to Angular calls to your Rails API not being completed. There is a gem to resolve this Capybara::Angular but I was unable to get it working so I had a look at the code worked then came up with the following solution which is more explicit. I hope to do pull request in the future to resolve the gems issues.

spec/support/angular_waiter.rb This is the core of the waiter doing the waiting on Angular to complete its requests

module Angular
  class Waiter
    attr_accessor :page

    def initialize(page)
      @page = page
    end

    def wait_until_ready
      return unless angular_app?

      setup_ready

      start = Time.now
      until ready?
        timeout! if timeout?(start)
        setup_ready if page_reloaded_on_wait?
        sleep(0.01)
      end
    end

    private

    def timeout?(start)
      Time.now - start > Capybara.default_wait_time
    end

    def timeout!
      raise TimeoutError.new("timeout while waiting for angular")
    end

    def ready?
      page.evaluate_script("window.angularReady")
    end

    def angular_app?
      begin
        js = "(typeof angular !== 'undefined') && "
        js += "angular.element(document.querySelector('[ng-app]')).length > 0"
        page.evaluate_script js
      rescue Capybara::NotSupportedByDriverError
        false
      end
    end

    def setup_ready
      page.execute_script <<-JS
        window.angularReady = false;
        var app = angular.element(document.querySelector('[ng-app]'));
        var injector = app.injector();

        injector.invoke(function($browser) {
          $browser.notifyWhenNoOutstandingRequests(function() {
            window.angularReady = true;
          });
        });
      JS
    end

    def page_reloaded_on_wait?
      page.evaluate_script("window.angularReady === undefined")
    end
  end
end

spec/support/wait_for_angular.rb This provides a method for explicitly waiting on Angular

module WaitForAngular
  def wait_for_angular
    Angular::Waiter.new(Capybara.current_session).wait_until_ready
  end
end

spec/support/wait_for_ajax.rb This provides a method for explicitly waiting on jQuery which is used by Angular under covers.

module WaitForAjax
  def wait_for_ajax
    Timeout.timeout(Capybara.default_wait_time) do
      active = page.evaluate_script('jQuery.active')
      until active == 0
        active = page.evaluate_script('jQuery.active')
      end
    end
  end
end

To make the two methods available simply add the two following lines

RSpec.configure do |config|
  config.include WaitForAjax, type: :feature
  config.include WaitForAngular, type: :feature
end

Then in your spec use it like the following example:

require 'spec_helper'

describe 'login', :type => :feature, :js => true do
  context 'for customers' do
    let(:customer) { FactoryGirl.create :customer }

    context 'with user name and password' do
      it 'will allow the visitor to login' do
        visit root_path
        fill_in 'user name', with: customer.username
        fill_in 'password', with: 'passwordpassword'
        click_button 'enter'
        wait_for_angular
        expect(page).to have_content 'You have signed in successfully'
        expect(last_activity.key).to eq 'user.successful_login'
      end
    end
  end
end

Security

On the Angular Side

When the site visitor doesn't have a session I wanted to redirected them to a page that's appropriate. In the case of my application redirection to the 'login' page was required. The following module intercepts the http request and check to see if it's a 401 (Unauthorised) then redirects the visitor to the login page.

app/assets/javascripts/common/authentication.js

/**
 * Authentication module, redirects to homepage if not logged in
 */

angular.module('common.authentication', [])

.config(function($httpProvider){
  // Intercepts every http request.  If the response is success, pass it through.  If the response is an
  // error, and that error is 401 (unauthorised) then the user isn't logged in, redirect to the login page
  var interceptor = function($q, $location, $rootScope) {
    return {
      'responseError': function(rejection) {
        if (rejection.status == 401) {
          $rootScope.$broadcast('event:unauthorized');
          $location.path('/login');
          return rejection;
        }
        return $q.reject(rejection);
      }
    };
  };
  $httpProvider.interceptors.push(interceptor);
});

Devise

Devise will work with Angular however with protect_from_forgery enabled on your Rails application, authentication will fail due to the lack of protection tokens not being passed back to Rails on calls. In fact this will case problems with all API calls. The following additions to the application_controller.rb and sessions_controller.rb allow for tokens to be transmitted to the browser and received on requests with the token being reset on each request.

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery

  after_filter  :set_csrf_cookie_for_ng

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

  protected
  def verified_request?
    super || form_authenticity_token == request.headers['X-XSRF-TOKEN']
  end
end

app/controllers/sessions_controller.rb

class SessionsController < Devise::SessionsController
  after_filter :set_csrf_headers, only: [:create, :destroy]

  respond_to :json

  protected
  def set_csrf_headers
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end
end

While the code below is not necessary for getting Devise working with Angular I like to add this to ensure unauthenticated CSRF attempts cause exceptions.

app/controllers/application_controller.rb

  protected
  # Unauthenticated CSRF protection
  def handle_unverified_request
    super
    raise(ActionController::InvalidAuthenticityToken)
  end

Rack Attack

With AngularJS we're calling back to an API served up by application who's code is hidden from the unscrupulous. The code for the Angular application however is available. Even though we may obfuscate the javascript code through gems like uglifier it's still relatively easy to reverse engineer the code to identify what the full API is for your Rails. So it's important to take measures to minimize attacks using this information. I implemented Rack::Attack!!! which give me a high level of configurability to craft something specific to my needs. I also used secure_headers to catch some of the more common security issues. Another gem to check, which is very simple to implement is Rack::Protection and may suffice for what you need; in my case it didn't.

Resources

Learning

Setup

View Templating

Testing

Security

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment