Zack Hubert

Batman.js and Rails Part 4, Associations

Last time, we looked at some basic CRUD views. Let’s extend this brain dead example by adding associations into the mix.

Since I have a Post model, it only makes sense to have a Comments model, where the Post has_many Comments and the Comment belongs_to one Post.

It is very easy.

rails g resource comment content:string post_id:integer; rake db:migrate

app/controllers/comments_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class CommentsController < ApplicationController
  respond_to :json

  def index
    @post = Post.find(params[:post_id])
    respond_with(@post,@post.comments)
  end

  def show
    @comment = Comment.find(params[:id])
    respond_with @comment
  end

  # for validation, can't use responders (batman expects errors to not have a root)
  def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.build(params[:comment])
    respond_to do |format|
      if @comment.save
        format.json { render :json => @comment }
      else
        format.json { render :json => @comment.errors, :status => :unprocessable_entity}
      end
    end
  end

  # for validation, can't use responders (batman expects errors to not have a root)
  def update
    @comment = Comment.find(params[:id])
    respond_to do |format|
      if @comment.update_attributes(params[:comment])
        format.json { render :json => @comment }
      else
        format.json { render :json => @comment.errors, :status => :unprocessable_entity}
      end
    end
  end

  # responder ok here
  def destroy
    @comment = Comment.find(params[:id])
    respond_with(@comment.post,@comment.destroy)
  end
end

For this string of examples, we are using serializers (though I don’t use them anymore for reasons I might explain later):

app/serializers/comment_serializer.rb
1
2
3
class CommentSerializer < ActiveModel::Serializer
  attributes :content, :id, :post_id
end

And the model is obvious…

app/models/comment.rb
1
2
3
4
5
class Comment < ActiveRecord::Base
  attr_accessible :content
  validates_presence_of :content, :post_id
  belongs_to :post
end

Tweak the post.rb to reflect the association

app/models/post.rb
1
2
3
4
class Post < ActiveRecord::Base
  attr_accessible :content, :title
  has_many :comments
end

As are the routes, but this is stuff you already know.

config/routes.rb
1
2
3
  resources :posts do
    resources :comments
  end

So how does Batman handle associations? Well, you will be pleasantly surprised by how similar it is to Rails, here is how the route will look:

app/assets/javascripts/batty.js.coffee
1
2
  @resources 'posts', ->
    @resources 'comments'

The Batman model has all the things you’d expect:

app/assets/javascripts/batman/models/comment.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Batty.Comment extends Batman.Model
  @resourceName: 'comment'
  @storageKey: 'comments'

  # saving everything to the Rails backend
  # gives validation errors
  @persist Batman.RailsStorage

  # fields
  @encode "content", "id", "post_id"

  # validations
  @validate "content", presence: true

  # associations
  @belongsTo 'post', { inverseOf: 'comments'}

  # indicates that rails is nesting resources, shallow!
  @urlNestsUnder 'post'

And we have to tell the post model about its new association:

app/assets/javascripts/batman/models/post.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
class Batty.Post extends Batman.Model
  @resourceName: 'post'
  @storageKey: 'posts'

  @persist Batman.RailsStorage

  # fields
  @encode "title", "content"
  @hasMany "comments"

  # validations
  @validate "title", presence: true
  @validate "content", presence: true

And the controller also follows the RESTful form as exemplified in Rails:

app/assets/javascripts/batman/controllers/comments_controller.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Batty.CommentsController extends Batman.Controller
  routingKey: 'comments'

  show: (params) ->
    comment = new Batty.Comment(id: params.id, post_id: params.postId)
    comment.load (err,result)  =>
      throw err if err
      @set 'comment', result

  edit: (params) ->
    comment = new Batty.Comment(id: params.id, post_id: params.postId)
    comment.load (err,result) =>
      throw err if err
      @set 'comment', result
    @form = @render()

  new: (params) ->
    @set 'comment', new Batty.Comment(post_id: params.postId)
    @form = @render()

  create: (params) ->
    @new_comment = @get('comment')
    @new_comment.save (err,record) =>
      $('#new_comment').attr('disabled', false)

      if err
        throw err unless err instanceof Batman.ErrorsSet
        #@set 'comment', record
      else
        Batty.flashSuccess "Comment created successfully!"
        @redirect '/posts/' + @new_comment.get('post_id')

  update: (params) ->
    @new_comment = @get('comment')
    @new_comment.save (err,record) =>
      $('#new_comment').attr('disabled', false)

      if err
        throw err unless err instanceof Batman.ErrorsSet
        #@set 'comment', record
      else
        Batty.flashSuccess "Comment updated successfully!"
        @redirect '/posts/' + @new_comment.get('post_id')

  destroy: ->
    @new_comment = @get('comment')
    @new_comment.destroy (err) =>
      if err
        throw err unless err instanceof Batman.ErrorsSet
      else
        Batty.flashSuccess "Removed successfully!"
        @redirect '/posts/' + @new_comment.get('post_id')

The views would get a bit tedious to put in here (check out Github for details), but a salient part to demonstrate is on posts#show:

app/assets/javascripts/batman/views/posts/show.html.haml
1
2
3
4
5
6
7
  %a.btn.btn-primary{'data-route' => 'routes.posts[post].comments.new'} New Comment

.row
  #content.span12
    #comments
      %div{"data-foreach-comment" => "post.comments"}
        .comment{"data-partial" => "comments/_comment"}

Here you can see how we refer to an association for routing purposes (not good form to click off to a separate page, but keeping it basic) as well as iterating over the association for view partials.

To the Rails developer, Batman should feel like idiomatic Rails written in Coffeescript, with similar project layout, MVC definition, and convenient macros for the things you use regularly. This association example demonstrates that it is exactly how you would expect it to work. Minimizing surprise is definitely a strength of Batman.

Comments