Nested Comments in Ruby on Rails, part 2: Controllers and Views


  1. The Model Layer
  2. Controllers and Views
 

Part 1 of this series came out exactly 3 months and 3 days ago. Special thanks to a reader named Edward who prodded me to finally add the controllers and views to this.

Going beyond the model layer for nested comments introduces a new programming idiom: recursion. Some ruby developers may not be familiar with it – especially if your experience is mostly web-related, where the need doesn’t come up as often. Recursion in a nutshell is the act of a method calling itself. If you’ve seen Inception, The ability to have dreams within dreams within dreams means those dreams are recursive. If you haven’t seen the movie, think of russian matryoshka dolls. You won’t experience star-studded special effects with the dolls, but you’ll at least get the idea of recursion.

Unlike russian dolls or most of Leo’s recent work, recursion in software is potentially infinite. Practically speaking though, it’s more like the doll thing. After all, a system only has so many resources, and recursion is expensive in this regard – the method must copy itself in memory at each layer, local variables and all. On the plus side, they tend to be lightning fast compared to standard iteration using loops. And in our case, we’ll be hitting the database at each layer. We’ll ignore the dangers in our simple app, though.

Routing

Let’s start with our routing file:

# config/routes.rb
NestedComments::Application.routes.draw do
  resources :comments do
    resources :comments
  end

  resources :posts do
    resources :comments
  end
  
  root :to => 'posts#index'
end

Working backward, we’re making our Posts controller’s index action our default route. That’s just to get the app functional. Next comes something interesting: nesting our comments inside of our posts. Interesting, but boring. Finally, the main event: nesting our comments within our comments!

Before you get too excited and start pulling out your Nana’s childhood russian doll set for comparision, this isn’t true recursion. It’s well documented that nesting resources any more than two layers deep is painful and unnecessary, so think of this as the lamest russian doll ever.

Controllers

First, our Posts controller, which is less exciting:

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    @posts = Post.all
  end

  def show
    @post = Post.find(params[:id])
  end

  def new
    @post = Post.new
  end
  
  def create
    @post = Post.new(params[:post])
    
    if @post.save
      redirect_to posts_path, :notice => "Your post was created successfully."
    else
      render :action => :new
    end
  end
end

We’re setting up a pretty standard restful resource here, with a couple actions skipped for simplicity. Now the comments controller (get those dolls ready):

# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_filter :get_parent
  
  def new
    @comment = @parent.comments.build
  end

  def create
    @comment = @parent.comments.build(params[:comment])
    
    if @comment.save
      redirect_to post_path(@comment.post), :notice => 'Thank you for your comment!'
    else
      render :new
    end
  end

  protected
  
  def get_parent
    @parent = Post.find_by_id(params[:post_id]) if params[:post_id]
    @parent = Comment.find_by_id(params[:comment_id]) if params[:comment_id]
    
    redirect_to root_path unless defined?(@parent)
  end
end

It’s not much bigger, but there’s a lot going on here! First, since comments are nested, we have to look for a parent. We’re only creating comments in this example, so we only have those related actions. Comments will always be shown on a post page.

The really exciting part is after a successful comment creation. How do we redirect back to the post page? For all we know, this comment could buried down 12 layers of replies. All we really have access to so far is the parent of the object. This necessitates a new model method:

# exerpt from app/models/comment.rb
def post
  return @post if defined?(@post)
  @post = commentable.is_a?(Post) ? commentable : commentable.post
end

Recursive functions are often short and sweet for two reasons: they’re already complex by nature, and adding more code than necessary would make them unmanageable. Also, they’re getting a lot done in just a few lines. In this case, the second line is the key: if “commentable” (the parent object) is a post, return that. Otherwise, call this same method on the parent, which will in turn check if *it* is a Post, and so on.

I could have written it shorter, like this:

def post
  commentable.is_a?(Post) ? commentable : commentable.post
end

In fact, I did at first. But the extra code that checks and sets an instance variable is caching the result. This way, if we call the same method on an object more than once, it stores the result for future use. Remember, recursion can be expensive – especially when the database is involved.

Views

Finally, it’s view time, with one more bit of recursion for fun.

Or post views are standard scaffolding mostly, with the exception of the show view:

# app/views/posts/show.html.erb
<h1><%= @post.title %></h1>

<div class="body">
  <%= @post.body %>  
</div>

<h2>Comments</h2>

<p><%= link_to 'Add a Comment', new_post_comment_path(@post) %></p>

<ul class="comment_list">
  <%= render :partial => 'comments/comment', :collection => @post.comments %>
</ul>

Notice we have the partial app/views/comments/_comment.html.erb. We’re calling this for each of our post’s comments. Nothing too fancy here. Now, for the partial itself:

# app/views/comments/_comment.html.erb
<li class="comment">
  <h3><%= comment.title %></h3>

  <div class="body">
    <%= comment.body %>
  </div>
  
  <p><%= link_to 'Add a Reply', new_comment_comment_path(comment) %></p>
  
  <% unless comment.comments.empty? %>
    <ul class="comment_list">
      <%= render :partial => 'comments/comment', :collection => comment.comments %>
    </ul>
  <% end %>
</li>

This partial is recursive! The comments controller doesn’t have a show method, because we’re never going to view a comment by itself. Instead, the show-like code is in this partial, and at the end it checks to see if *this* comment has comments. If so, it calls the partial again on the whole collection. The end result is a nested, bulleted list of comments. This is not very sexy if you fire up the code yourself, but it’s a great starting point.

Summary

Hopefully this article as done a good job of explaining both recursion, and how to use it to achieve nested comments in your applications. If you’re new to recursion as a concept, haven’t seen Inception, didn’t inherit russian dolls from Nana or receive them as a snazzy graduation present, and my explanation somehow fell short, it’s a well documented programming idiom. There are tons of resources online, so take the time to learn this powerful tool, then learn not to overuse it :)

Please download the code and play with it if you want to learn more – the code is fully test-driven so you can see how that works, which is just as important.

On a final note, I’m tempted to do a follow-up article with ajax and some nicer formatting. Perhaps in 3 months and 3 days…

Advertisements

Tags: , , , , , , , ,

9 Responses to “Nested Comments in Ruby on Rails, part 2: Controllers and Views”

  1. ed Says:

    Great post, the tutorial was presented in a way that is easy to understand. the brief intro on recursion was masterful.

  2. Haze Long Says:

    hmm. have you ever tried to do a comments count for each post? the way you solved the problem raised many more issues.

  3. Jaime Bellmyer Says:

    Hi Haze – If you want to have nested objects, there’s no way around needing recursion to do the job. Counting comments would use the same technique. Any other issues you can think of? I could roll a few answers into another article easier than trying to paste code into a comment.

    • Haze Long Says:

      just that its not very scalable in terms of database design.. to retrieve comments for each post requires numerous calls to the database since the comments are nested within the comments. For example, first it calls all the Post’s comments. then it checks each comment for nested comments n so on n so forth.

      i had implemented using post_id and comment_id previously, it retrieves all the Post’s comments which includes all the comments nested all in one go. then for layout’s sake, it calls the nested comments from the already loaded set of comments.

      I been looking around for clues but I guess maybe there is a reason WordPress allow a limit for the threading. It will not be able to support heavy traffic blogs with alot of comments. Then again, WP database design is abit different than yours. It keeps the post_id in the comments table but the comments parenting in yet another table.

      (btw, i m looking forward to the ajax implementation. especially for the reply button :D )

      • Jaime Bellmyer Says:

        You bring up a really good topic – database optimization. The easiest way to do this is the way you mentioned – every child contains the id of the top parent, so they can be grabbed all at once.

        That’s a little off-topic with the main goal of my post – to show polymorphism in rails. Comments were the simplest and most common example I could think of. The problem with examples like this is they have to stay simple – otherwise it’s harder for people to understand the main topic. For example, I’m a *nut* about testing, but I don’t include most of my testing code in my posts, because I don’t want to cloud the main issue. Instead I make the tests available through github.

        That being said, there’s obviously interest in expanding this, so I think it’s a good idea. I think I’ll try to tackle database optimization and record counting (which go hand in hand) in my next post. Thanks!

  4. Shannon Says:

    Great tutorials and I am looking forward to dipping into past articles when I have more time, but I have a question, all the more because I’m struggling with nested polymorphic comments myself.

    In part one of your tutorial, the goal seemed to be to make the whole comments model polymorphic. But as I look at the Comments controller here in part two, it seems to be referencing ‘post_path,’ Post.find_by_id, etc.

    Correct me if I’m wrong, but wouldn’t the above only make the Comments controller work on a Post content type, and not anything else like say…Blog, Article, etc?

  5. kp Says:

    great tutorial! but how would you grab a user id if you wanted to assign to that the comments as well? …assuming you wanted to implement authentication.

  6. Anil Punjabi Says:

    Many thanks Jaime.
    You are the #1 post on this topic.

    Greatly appreciate the focus on recursion.

    Any suggestions for the AJAX version of this code, especially for the Reply.

  7. Doszhan Says:

    Hi, thank you very much!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s


%d bloggers like this: