Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly render nested comments in rails?

I'm trying to make nested comments work properly in a rails 5 app and running into a lot of difficulty.

I'm building a question and answer site, and I have an Acomment model in which there are comments that belong to answers, and then there are comments that belong to other comments:

class Acomment < ApplicationRecord
  belongs_to :answer
  belongs_to :user
  belongs_to :parent, class_name: "Acomment", optional: true
  has_many :replies,
           class_name: "Acomment", foreign_key: :parent_id, dependent: :destroy
end

I want to display all comments, and then all replies to comments, and replies to replies, etc. But I can't figure out how to make the nesting work properly.

In my view, inside an each loop, I'm rendering the _comment.html.erb partial:

<% answer.acomments.each do |comment| %>
  <%= render :partial=>"comment", :object=>comment %>
<% end %>

Then, inside my _comment.html.erb partial, I'm displaying the comments, a reply link, and then rendering a partial for the replies to comments:

<div>
  <%= comment.body %>
  <%= link_to "Reply", new_acomment_path(:parent_id => comment, :answer_id => comment.answer) if comment.parent_id.blank? %>

  <%= render partial: "reply", collection: comment.replies %>
</div>

Here's the _reply.html.erb partial:

<%= reply.body %>
<%= link_to "Reply", new_acomment_path(:parent_id => reply, :answer_id => reply.answer) %>

I'm running into two problems:

1) When I render the first partial, not only does it render the comments, but it also renders the replies at the same time (since they are also comments).

2) When I render the second partial (the replies), they are not nested within the comments where they are supposed to. Everything is out of order.

EDIT: I expect to see this:

Comment 1 
  Comment on Comment 1 
    Comment on Comment on Comment 1 
Comment 2 

But instead this is what I'm seeing:

Comment 1 
Comment on Comment 1 
Comment 2 
Comment on Comment 1 
Comment on Comment on Comment 1 
Comment on Comment on Comment 1

Any help would be greatly appreciated.

like image 357
Tony M Avatar asked Feb 15 '18 03:02

Tony M


2 Answers

As Taryn wrote, you first need to filter the comments with no parent_id, which means they are first level comments to an answer.

views/answers/show.html.erb

<%= @answer.text %>
<h4>Comments</h4>
<% @answer.acomments.parents.each do |comment| %>
  <%= render :partial=>"comment", locals: { comment: comment } %>
<% end %>

Then you need a recursive approach:

views/acomments/_comment.html.erb

<div>
  <%= comment.body %>
  <%= link_to "Reply", new_acomment_path(:parent_id => comment, :answer_id => comment.answer) if comment.parent_id.blank? %>

  <% comment.replies.each do |reply|
    <%= render :partial => "comment", locals: { comment: reply } %>
  <% end %>
</div>

You need to provide some indentation so that it is clear the hierarchy between comments.

like image 53
Pablo Avatar answered Oct 18 '22 11:10

Pablo


For the first part of your question, you'll probably need some scopes that help you distinguish between "parent comments" (ie comments that aren't replies) and "child comments" (replies).

something like:

class Acomment < ApplicationRecord
  belongs_to :answer
  belongs_to :user
  belongs_to :parent, class_name: "Acomment", optional: true
  has_many :replies, class_name: "Acomment", foreign_key: :parent_id, dependent: :destroy

  scope :is_parent, where(parent_id: nil)
  scope :is_child, where("parent_id IS NOT NULL")
end

You can use these eg below to render only parents in the main loop... with the intention each parent will render its own children.

<% answer.acomments.is_parent.each do |comment| %>
    <%= render :partial=>"comment", :object=>comment %>
<% end %>

WRT your second question - I'll need a bit more info - I'll ask in comments above.

After:

So... My guess at what's happening is: you're just getting all the comments (as you mentioned) and they're being displayed in the outer loop (instead of just the parents being displayed int he outer loop) and they're in that order because that's the database-order for the comments.

Nothing is actually being displayed in the inner loop due to a default naming convention in partials.

If you use collection in a partial - Rails will feed each item into the partial... and then name the local variable after the name of the partial. In this case, you are trying to render a partial named "reply", but passing in a set of comments... so the partial is going to look for a variable called "reply".

I'd recommend you just reuse the same "comment" partial that you already use and instead render it this way:

<div>
  <%= comment.body %>
  <%= link_to "Reply", new_acomment_path(:parent_id => comment, :answer_id => comment.answer) if comment.parent_id.blank? %>

  <%= render partial: "comment", collection: comment.replies %>
</div>

It will be recursive (just like Pablo's suggestion) and thus render comment threads by indenting for every set of sub-comments.

Looking at your conversation with pablo... it's possible that parents is a bad name for a scope... it sounds like it might be interfering with a ruby-method named the same thing that traverses up the class hierarchy... so perhaps it's a bad name - as such I'll rename the scope.

like image 1
Taryn East Avatar answered Oct 18 '22 10:10

Taryn East