5 tips on how to use AngularJS with Rails that changed how we work

Dariusz Gertych

5 tips on how to use AngularJS with Rails that changed how we work

For the last year, the Monterail team has been using AngularJS and Rails together. I'd like to share with you some of the experiences that we've gained throughout this process.

If you don't want to read, then go ahead and dive into our sample application.

Zuza the pug on AngularJS

Zuza the pug on AngularJS

We are using rails-assets

# Gemfile
source 'https://rubygems.org'
source 'https://rails-assets.org'

# etc ..

# assets
gem 'rails-assets-lodash'
gem 'rails-assets-angular',                       '~> 1.2.0'
gem 'rails-assets-angular-cache'
gem 'rails-assets-angular-ui-router',             '~> 0.2.9'
gem 'rails-assets-angular-translate'

We are passing configuration by JsEnv module and AngularJS constant

# lib/templates_paths.rb
module TemplatesPaths
  extend self

  def templates
    Hash[
      Rails.application.assets.each_logical_path.
      select { |file| file.end_with?('swf', 'html', 'json') }.
      map { |file| [file, ActionController::Base.helpers.asset_path(file)] }
    ]
  end
end

# app/controllers/concerns/js_env.rb
require 'templates_paths'

module JsEnv
  extend ActiveSupport::Concern
  include TemplatesPaths

  included do
    helper_method :js_env
  end

  def js_env
    data = {
      env: Rails.env,
      templates: templates
    }

    <<-EOS.html_safe
      <script type="text/javascript">
        shared = angular.module('SampleApp')
        shared.constant('Rails', #{data.to_json})
      </script>
    EOS
  end
end

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  include JsEnv
end
// app/views/layouts/application.html.slim
body
  h1 Sample App Main Page
  = yield

  = javascript_include_tag 'application'
  = js_env
# app/assets/javascripts/controllers/pages_ctrl.coffee
angular.module('SampleApp').controller 'PagesCtrl', ($scope, Rails) ->
  $scope.test = Rails.env

Yes, we use coffeescript and we use ng-min as well.

We take advantage of sprockets and AngularJS interceptors

# config/initializers/sprockets.rb
# register .slim for assets pipeline
Rails.application.assets.register_mime_type 'text/html', '.html'
Rails.application.assets.register_engine '.slim', Slim::Template

We put slim templates under the app/assets/templates directory.

With a JsEnv and small AngularJS interceptor we will always get the right path for our template, even on production after the rake assets:precompile.

# app/assets/javascripts/init.coffee
angular.module('SampleApp').config ($provide, $httpProvider, Rails) ->
  # Assets interceptor
  $provide.factory 'railsAssetsInterceptor', ($angularCacheFactory) ->
    request: (config) ->
      if assetUrl = Rails.templates[config.url]
        config.url = assetUrl
      config

  $httpProvider.interceptors.push('railsAssetsInterceptor')

Throughout the whole AngularJS application we can use asset paths normally like: /pages/index.html. The railsAssetsInterceptor factory will translate asset paths to their version after compilation. It changes the asset path from /pages/index.html to /assets/pages/index-sha.html.

Yes, this works. Check it out!

We are using angular-translate with custom loader

# app/assets/javascripts/init.coffee

angular.module('SampleApp', [
  'pascalprecht.translate'
])
  .factory 'railsLocalesLoader', ($http) ->
    (options) ->
      $http.get("locales/#{options.key}.json").then (response) ->
        response.data
      , (error) ->
        throw options.key
  .config ($translateProvider) ->
    $translateProvider.useLoader('railsLocalesLoader')
    $translateProvider.preferredLanguage('en')

railsLocalesLoader is a custom factory for loading locales from Rails. We serve locales via the assets pipeline in the same manner that we do with templates. It works after the translation has changed in the config/locales/[KEY].yml file and works properly after rake assets:precompile. This is possible thanks to JsEnv and railsAssetsInterceptor.

The Rails part of the code looks like this:

# config/initializers/sprockets.rb
# add custom depend_on_config sprockets processor directive
class Sprockets::DirectiveProcessor
  def process_depend_on_config_directive(file)
    path = File.expand_path(file, Rails.root.join('config'))
    context.depend_on(path)
  end
end

# register .json for assets pipeline
Rails.application.assets.register_mime_type 'application/json', '.json'

# enable to use sprockets directive processor in .json
Rails.application.assets.register_preprocessor 'application/json', Sprockets::DirectiveProcessor

Put locale under the app/assets/locales/locales directory.

// app/assets/locales/locales/en.json.erb
//= depend_on_config locales/en.yml
<%= Translations.new.for(:en).to_json %>

In the code above we use a custom depend_on_config directive which relays on sprockets depend_on directive. Thanks to depend_on_config directive, we can expire an asset's cache in response to a change in yaml file.

Translations service in ruby prepares flatten hash from your locale yaml file. You can find an example implementation here.

We are using client side cache

Before, I explained some of the magic behind how sprockets and AngularJS work together, thanks to server side cache solutions. Now we can just as easily cache templates on the client side to get an even greater boost.

# app/assets/javascripts/init.coffee

angular.module('SampleApp', [
  'jmdobry.angular-cache',
])
  .config ($provide, Rails) ->
    # Template cache
    if Rails.env != 'development'
      $provide.service '$templateCache', ['$angularCacheFactory', ($angularCacheFactory) ->
        $angularCacheFactory('templateCache', {
          maxAge: 3600000 * 24 * 7,
          storageMode: 'localStorage',
          recycleFreq: 60000
        })
      ]

This may not be a lot of words, but it is a lot of code so I hope it will be useful.

Dariusz Gertych avatar
Dariusz Gertych