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.
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
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
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
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:
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:
Go forth! Nest your comments!
Discussion (8)
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.
Organise ActiveRecord model into a tree structure
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:
Installation
To apply Ancestry to any
ActiveRecord
model, follow these simple steps:Install
Add ancestry column to your table
Wow! If I had known about this shortcut I would have taken it. Thanks for sharing!
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
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?
Hi, I get the same error. How you fixed it?
Good approach.
For example, unlike the example from chris-gorails you do not create a separate controller for each commentable model - by this your approach is better.
def find_commentable
is good, althrough something tells me it can be further improved.I'm implement comments the same way to you but now I want use Action Cable and I started learn about this resource.
Thanks for sharing!