Getting started with AngularJS in Rails

Jasim A Basheer

I have found that emails are how I get started with writing anything substantial that people find useful. The following post originated as an email to the London Ruby Usergroup when someone asked for pointers in integrating AngularJS with Rails. It was written in a hurry and is presented without any modification. Read on to listen to what I have to say about building an AngularJS project within Rails. I welcome your thoughts.

General

  • Use Coffeescript.
  • Start with Egghead.io. Read AngularJS by Brad Green, Shyam Seshadri. Spend a few hours and skim it cover-to-cover at least once. Angular documentation on the web is quite fragmented; a book is the best way to begin learning Angular.
  • If you aren't familiar with Underscore.js already, go through it. Add it to the project as soon as you begin. You'll need it.
  • Drop gem 'ng-rails-csrf' into your Gemfile to make Angular AJAX requests play well with Rails CSRF protection.

Directives

  • Some material on the web might lead you to think that a deep understanding of Directives is crucial to ‘get’ Angular. But for a beginner, the important thing is to just have fun doing things the Angular way (it is loads of fun, let me assure you). The basic directives Angular provides (eg: ng-repeat and ng-show) can go a surprisingly long way. Worry about transclusion, $compile, $digest and other arcane incantations only after you get comfortable with the basics.
  • One reason for you to write a directive is when you need to use existing widgets. A lot of popular widgets already have Angular wrappers. For example, we use the select2 wrapper for multi-select. There is also AngularStrap with a bunch of nice components that wraps over Twitter Bootstrap. The v1 (http://mgcrea.github.io/angular-strap/1.0/) version has Datepicker and Timepicker which isn't available in the new version yet.
  • You would hit a point when off-the-shelf directives won’t be enough and you'd want to do something that requires DOM manipulation. That would be a good time to start digging into Directives. You can also start exploring Directives when you start seeing patterns in your controllers that can be extracted out into reusable components.

Serialization

  • We use ActiveModel::Serializer. It is a safe choice.
  • Pay close attention to what you include in each serializer. Here is the obvious thing: you can put a bunch of has_many in your User model, and if you keep doing that for other objects, a render json: User.all will end up sending your entire db graph to the client. 
  • We implement a base serializer for most entities with some basic fields and no associations. When we need more data/associations, we sub-class them for that specific use case. Discourse follows a similar style - https://github.com/discourse/discourse/tree/master/app/serializers
  • Try not to do any decoration on the server side; it is tempting with all the view helper goodness Rails provides, but then you are splitting logic between client and server and it causes all sorts of headaches. You can use Angular filters as client-side helpers on some occasions. For example, {{ start_time | date }} prints out nicely formatted dates. 

Forms, validations and POJO form objects

  • Angular gives you validation attributes like ng-required, ng-minlength, ng-maxlength etc. on all form INPUTs. For simple use cases, this is the best way to go. But for a complex form, we ended up building an ActiveModel-like POJO Form Object to hold validation information, and for doing some slight transformations on the data before submission. We bound this object to the scope and removed a lot of clutter from our controllers that had begun to get bloated. We have to see how this pans out in the long run; I can give code snippets if you wish.
  • When I introduced Angular to our Rails project that was already under development, I tried to minimize the amount of change required by trying to keep form POSTs as they were.

<input type='text' ng-model='post.title' name='post[title]'>
<input type='submit'/>

It worked for the most part, but we ran into some edge cases that wasted a lot of time. That was when I learned to let go and fully embrace the Angular way. We switched over to something like this:

<input type='text' ng-model='post.title' name='post[title]'>
<button ng-click='submitForm()'>

And the controller:

angular.module('myApp').controller 'postCtrl', ['$scope', '$http', '$state', 'flash',
($scope, $http, $state, flash) ->
$scope.submitForm = ->
$http.post(
Routes.posts_path,
$scope.post # this is the ng-model hash we bound the input boxes to.
).success((data, status) ->
$state.go("posts.show", {post_id: data.post.id})
).error((data, status) ->
if data['message']
flash.error = data['message']
else
flash.error = "Could not create Post. #{data}"
)
]
# `flash` is from https://github.com/wmluke/angular-flash.
# $state.go is ui-router. https://github.com/angular-ui/ui-router. More on that later.
# we use JSRoutes to access Rails routes from JS. Quite useful. https://github.com/railsware/js-routes/

The above code isn't ideal. You will be better off putting the $http.post inside an Angular service. This looks like a good post describing that: http://sravi-kiran.blogspot.in/2013/03/MovingAjaxCallsToACustomServiceInAngularJS.html.

Rails+Angular

  • Go all the way in. Don’t try to keep a mix of Angular and non-Angular pages. Except for Devise, all our pages are rendered client-side. We don't have to worry about SEO since this is an internal app.
  • Use ui-router, not the one that comes with Angular. The ui-router README could be confusing, but like most of Angular documentaiton, it becomes obvious in hindsight if you persevere.
  • We keep everything together in the same Rails project. Here is a rough structure:

routes.rb:

root ‘base#angular’
resources :posts, only: [:index]

app/views/base/angular.html.erb:

<div ng-app='myApp'>
<div ui-view></div>
</div>

posts_controller.rb:

class PostsController < ApplicationController
respond_to :json

def index
render json: Post.all, root: false
end
end

app/assets/javascripts/routes.coffee.erb:

angular.module('myApp').run ($rootScope, $state, $stateParams) ->
$rootScope.$state = $state
$rootScope.$stateParams = $stateParams

angular.module('myApp').config ($stateProvider, $urlRouterProvider) ->

# The default route
$urlRouterProvider.when("", "/posts/list")

$stateProvider
.state("posts",
url: "/posts"
controller: "postsRootCtrl"
template: '<div ui-view></div>'
abstract: true
).state("posts.index",
url: "/list"
controller: "postsIndexCtrl"
templateUrl: "<%= asset_path('posts/index.html') %>"
resolve:
postsPromise: ($http) ->
$http.get(Routes.posts_path())
)

app/assets/javascripts/posts.js.coffee:

angular.module('myApp').controller 'postsCtrl', ['postsPromise', '$scope', '$http', (postsPromise, $scope, $http) ->
$scope.posts = postsPromise.data

app/assets/templates/index.html

{% raw %}
<div ng-repeat="post in posts">
<h1>
{{post.title}}
</h1>
<p>
{{post.body}}
</p>
</div>
{% endraw %}

Quirks and bugs

  • It is worth keeping in mind that in JS {} == {} is false, since comparisons are by object identity, not by object value. You can do explict comparison using Angular.equals when you need it. But in some cases Angular does an implicit comparison, for which it doesn't use Angular.equals. For example, if you try loading a page with a bunch of pre-selected radio-buttons, if their values are JS Objects (Hash), Angular won't pre-select them since the comparison will fail. This is not an issue if your values are strings/numbers.
  • JS treats the keys of all objects as strings. a={1: "Hello"}; _.each(a, (value, key) -> console.log(typeof key)). If your server sends Hashes whose keys are numeric ids, they'll end up in JS with string ids.
  • A quick and dirty way to inspect an Angular model is to drop this in your Angular view template:
    {% raw %}
    <pre>
    {{ post | json }}
    </pre>
    {% endraw %}
  • There is also Batarang, a Chrome plugin which lets you inspect your scope interactively. But I've found it to cause my pages to misbehave in certain cases. I these days use angular-chrome-debug, which is quite light-weight. I load this script in my app in development env so that I don't have to go thru Chrome Snippets everytime.
  • Angular has Angular.copy for deep copy when you need it - https://github.com/angular/angular.js/blob/master/src/Angular.js#L725.

Well, that's mostly it. I am generally liking building rich UIs with Angular and wouldn't go back to building for the web without using data-binding.

Please keep in mind that the structure I described is working well for our app, which is not meant for public consumption. You might want to look at keeping your Angular and Rails project separately and utilize JS build tools like Bower, Yeoman and Grunt if your app needs that.