Recursively displaying nested child fields
Imagine you’re creating a Reddit-like comment system where people can respond to comments. This can get very nested as people get deeper into reply loops, replying to comments that are children within children of comments. No matter how nested a child comment is, it’s important to us, so we’d like to display it.
Zayne and I were trying to figure out how to do this for one of our projects the other day and got caught in what seemed like a never-ending loop:
<% @post.comments.where(parent_id: nil).each do |comment| %> <%= comment.body %> <% @post.comments.where(parent_id: comment.id).each do |reply| %> <%= reply.body %> <% @post.comments.where(parent_id: reply.id).each do |reply| %> <%= reply.body %> ... <% end %> <% end %> <% end %>
We don’t know how many levels a hypothetical comment chain could go (we didn’t want to set a limit, so technically it could probably go forever), and we don’t want to keep writing iterations like the above over and over again. The answer to this is to display the comments recursively.
We already were keeping track of a child’s comment’s parent with a “parent_id” field, so the schema and model of our comments table look like the following:
/db/schema.rb
create_table "comments", force: :cascade do |t| t.integer "user_id" t.integer "parent_id" t.integer "post_id" t.text "body" end
/app/models/comment.rb
class Comment < ApplicationRecord belongs_to :post belongs_to :commenter, class_name: “User”, foreign_key: “user_id” belongs_to :parent, class_name: "Comment", optional: true has_many :replies, class_name: "Comment", foreign_key: "parent_id" end
This is a self-referential model. Each comment has a parent comment (except top level comments, which is why we set belongs_to :parent as optional: true), and every comment can have many comment replies.
Now, we want to be able to render a post’s comments in the post show page:
/app/views/posts/show.html.erb
<%= @post.name %> <%= @post.description %> <%= render @post.comments.where(parent_id: nil) %>
We render a collection of comments where the parent_id is nil, basically rendering only top level comments. Create a partial to display the comment collection:
/app/views/comments/_comment.html.erb
<div style="padding:15px;"> <%= comment.commenter.username %> <div style="padding-left:3px;border-left-width:1px;border-left-color:black;border-left-style:solid;"> <%= comment.content %> <%= render comment.replies if comment.replies.any? %> </div> </div>
The recursion lies in this line: render comment.replies if comment.replies.any? Basically, if there are replies for the comment the partial is currently displaying, it will recursively render it.
With the above, you get something that looks like this:
We put our form for the comments in a partial that accepts 3 local variables: the comment initializer (Comment.new), the post (@post), and the parent coment id (nil if it is a parent comment).
In the show page: /app/views/posts/show.html.erb
... <%= render partial: 'comments/form', locals: {comment: Comment.new, post: @post, parent: nil} %> ...
Along with each comment: /app/views/comments/_comments.html.erb
... <%= render partial: 'comments/form', locals: {comment: Comment.new, post: comment.post, parent: comment.id} %> ...
And the form partial: /app/views/comments/_form.html.erb
<%= form_for [post,comment] do |f| %> <div> <%= f.text_area :content %> </div> <% if parent %> <%= f.hidden_field :parent_id, value: parent %> <% end %> <%= f.submit "Take that!" %> <% end %>









