loading...

Nested Comments in Ruby on Rails

lucysuddenly profile image Lucy Suddenly Updated on ・4 min read

Nesting Dolls

Nested comments was a highly-desirable deliverable for my most recent project, so I learned how to make it happen! Here are the steps necessary so you can make it happen, too:

Step 1: Polymorphic Association

Set up your model to be polymorphic.

Polymorphic associations

from the Ruby on Rails guide

So in our case, we want to make comments polymorphic because they can be made on a post or another comment. We set up the migration this way:

rails generate model comment content:string commentable_id:integer commentable_type:string

When the comment is saved, either "post" or "comment" will be saved to the commentable_type, and the primary key of either the post or the comment will be saved to commentable_id. This acts like a switch for the belongs_to/has_many relationship.

The models for post and comments respectively should look like this:

class Post < ActiveRecord::Base
    has_many :comments, as: :commentable
end

class Comment < ActiveRecord::Base
    belongs_to :commentable, polymorphic: true
    has_many :comments, as: :commentable
end

Step 2: Routing

routing monorails

Your routes.rb should look like this, since we want to be able to pass in the primary key (params[:id]) for commentable_id and either "comment" or "post" for commentable_type -- we get these from the URI.

Rails.application.routes.draw do

  resources :posts do
    resources :comments
  end

  resources :comments do
    resources :comments
  end

end

our

rake routes

will look like this:

              Prefix Verb   URI Pattern                                       Controller#Action
       post_comments GET    /posts/:post_id/comments(.:format)                comments#index
                     POST   /posts/:post_id/comments(.:format)                comments#create
    new_post_comment GET    /posts/:post_id/comments/new(.:format)            comments#new
   edit_post_comment GET    /posts/:post_id/comments/:id/edit(.:format)       comments#edit
        post_comment GET    /posts/:post_id/comments/:id(.:format)            comments#show
                     PATCH  /posts/:post_id/comments/:id(.:format)            comments#update
                     PUT    /posts/:post_id/comments/:id(.:format)            comments#update
                     DELETE /posts/:post_id/comments/:id(.:format)            comments#destroy
               posts GET    /posts(.:format)                                  posts#index
                     POST   /posts(.:format)                                  posts#create
            new_post GET    /posts/new(.:format)                              posts#new
           edit_post GET    /posts/:id/edit(.:format)                         posts#edit
                post GET    /posts/:id(.:format)                              posts#show
                     PATCH  /posts/:id(.:format)                              posts#update
                     PUT    /posts/:id(.:format)                              posts#update
                     DELETE /posts/:id(.:format)                              posts#destroy
    comment_comments GET    /comments/:comment_id/comments(.:format)          comments#index
                     POST   /comments/:comment_id/comments(.:format)          comments#create
 new_comment_comment GET    /comments/:comment_id/comments/new(.:format)      comments#new
edit_comment_comment GET    /comments/:comment_id/comments/:id/edit(.:format) comments#edit
     comment_comment GET    /comments/:comment_id/comments/:id(.:format)      comments#show
                     PATCH  /comments/:comment_id/comments/:id(.:format)      comments#update
                     PUT    /comments/:comment_id/comments/:id(.:format)      comments#update
                     DELETE /comments/:comment_id/comments/:id(.:format)      comments#destroy
            comments GET    /comments(.:format)                               comments#index
                     POST   /comments(.:format)                               comments#create
         new_comment GET    /comments/new(.:format)                           comments#new
        edit_comment GET    /comments/:id/edit(.:format)                      comments#edit
             comment GET    /comments/:id(.:format)                           comments#show
                     PATCH  /comments/:id(.:format)                           comments#update
                     PUT    /comments/:id(.:format)                           comments#update
                     DELETE /comments/:id(.:format)                           comments#destroy

You'll notice we have three sets of comment CRUD actions: one for post comments, one for comment comments, and one for comments -- but they all share one set of controller actions, so all is as it should be. In the controller is where we will differentiate which commentable will be passed into the build method.

Step 3: Controllers

video game controller

Here's what our comment controller should look like:

class CommentsController < ApplicationController
before_action :find_commentable, only: :create

    def new
      @comment = Comment.new
    end

    def create
      @commentable.comments.build(comment_params)
      @commentable.save
    end

    private

    def comment_params
      params.require(:comment).permit(:content)
    end

    def find_commentable
      if params[:comment_id]
        @commentable = Comment.find_by_id(params[:comment_id]) 
      elsif params[:post_id]
        @commentable = Post.find_by_id(params[:post_id])
      end
    end

end

Did you catch it? This is where the magic happens. If the params, which come from the URI, contain a comment_id, the commentable we pass in will be a comment; if the params contain a post_id, the commentable we pass in will be a post!

Step 4: Views

great view

At the end of our post show view, we should include the following reference to a partial we are about to create:

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

Here's how collection works with the render method:

rendering

from the Ruby on Rails guide

Now we have access to each individual comment -- it's almost like being in an each block with |comment| delared. Here's what our partial view "_comment" might look like:

<li>
  <%= comment.content %> -

  <%= form_for [comment, Comment.new] do |f| %>
      <%= f.text_area :content, placeholder: "Add a Reply" %><br/>
      <%= f.submit "Reply"  %>
      <% end %>

  <ul>
      <%= render partial: 'comments/comment', collection: comment.comments %>
  </ul>

</li>

So we display the comment's content, include a form_for for a new nested comment, and then render our partial again! Recursion can be very useful! But notice the differences between our post render method and the comment partial render method: the former's collection renders the post's comments, whereas each comment will render any of its comments. Pretty neat, right?

Here's a sneak peak of what your nested comments can look like:

nested comments

Go forth! Nest your comments!

Discussion

pic
Editor guide
Collapse
ben profile image
Ben Halpern

Nice post.

DEV is a Rails app that has nested comments. Our code looks a lot like this. We use Ancestry to help manage this as well.

GitHub logo stefankroes / ancestry

Organise ActiveRecord model into a tree structure

Build Status Coverage Status Gitter Security

Ancestry

Ancestry is a gem that allows the records of a Ruby on Rails ActiveRecord model to be organised as a tree structure (or hierarchy). It uses a single database column, employing the materialised path pattern. It exposes all the standard tree structure relations (ancestors, parent, root, children, siblings, descendants) and allows all of them to be fetched in a single SQL query. Additional features are STI support, scopes, depth caching, depth constraints, easy migration from older gems, integrity checking, integrity restoration, arrangement of (sub)trees into hashes, and various strategies for dealing with orphaned records.

NOTE:

  • Ancestry 2.x supports Rails 4.1, and earlier
  • Ancestry 3.x supports Rails 5.0, and 4.2
  • Ancestry 4.0 only supports rails 5.0 and higher

Installation

To apply Ancestry to any ActiveRecord model, follow these simple steps:

Install

  • Add to Gemfile:
# Gemfile
gem 'ancestry'
  • Install required gems:
$ bundle install

Add ancestry column to your table

Collapse
lucysuddenly profile image
Lucy Suddenly Author

Wow! If I had known about this shortcut I would have taken it. Thanks for sharing!

Collapse
poafernandes profile image
Alexandre Porto Alegre

Thanks for the post, I'm learning rails and trying to implement nested comments on a project of mine, the problem is that I get an error that comments is an undefined method on my comment controller create. Is there any hidden steps that I should've done in between?

Collapse
vhsoto profile image
Victor Soto

Hi, I get the same error. How you fixed it?

Collapse
flip437 profile image
Philippe DIOLLOT

Hello Guyz!!
I'm trying to make it work but I have a issue in the partial.
When i'm trying to run this view i have this :
"undefined local variable or method `comment' "

comment is not defined.
Can someone tell me where "comment" is defined ?

Thanks for helping.
Flip

Collapse
alexvirtualbr profile image
Alexandre Ferreira

I'm implement comments the same way to you but now I want use Action Cable and I started learn about this resource.

Collapse
nicobobb profile image
Nicolás Bobb

Thanks for sharing!