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.
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.