Creating a nested comment system in Ruby on Rails

JonthumbPosted by Jonathan Weyermann on January 16, 2018 at 12:00 AM
Comments

One of the most classic things to build in Ruby on Rails is a blog app. In fact, this site is built with Ruby on Rails. Each blog Post object instance can have multiple Comments attached to it. While this is very simple and functional, comments on blogs often have additional functionality: The ability to respond to a specific comment, and thus create a nested comment structure. Thus I've recently begun exploring how I could add this to my own blog. Here are my results:

class PostsController < ApplicationController
  expose :post, find_by: :slug
  expose(:posts) { Post.public }
  expose(:user) { post.user }
end

This is my PostsController. It uses Decent Exposure to simplify the controller actions. On this controller, there are show and index actions, but they're not explicitly written out because the expose applies to all controller actions. The post uses Friendly Id to show by slug not not by id (for prettier urls), and therefore I've set the post to find_by slug. posts is for the index view, and public is a method in the post model that determines which posts should be shown (in my case, based on a specific date, and whether I've set them to published).


class CommentsController < PostsController

  def create
    comment = post.comments.create(comment_params)

    if comment.valid? && comment.save
      flash[:notice] = 'Comment Added'
      session.delete(:comment)
      redirect_to post_path(post, anchor: 'comment')
    else
      session[:comment] = comment
      flash[:alert] = Array(comment.errors).to_sentence
      redirect_to new_post_comment_path(post, comment_id: params["comment"]["reply_comment"], anchor: 'comment')
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:name, :email, :body, :post_id, :reply_comment)
  end
end

The comments controller inherits from the post controller (the comments are also route nested beneath the posts ). Depending on whether validations pass, I create the comment and display appropriate error messaging.


class Comment < ActiveRecord::Base
  belongs_to :post

  validates_presence_of :name, :body
  validates_email_format_of :email, :message => 'please enter a valid email'

  def sub_comments
    Comment.where(reply_comment: id)
  end
end

The important part of the comment model is sub_comments. We look for the active record attribute reply_comment to find the comments that reply to this comment. This helps us know where to place the comment in the view.

we set the the reply_comment attribute as a hidden attribute inside a _comments.html.erb partial

<div class="comments">
  <h2 class="page-header"><%= post.comments.length %> Comments</h2>
  <%= render partial: 'partials/comment', collection: post.root_comments %>

  <div class="well add-comment">
    <h2 class="page-header"><%= comment_descriptor %></h2>
    <a name="comment"></a>
    <% if flash[:notice] %>
      <div class="alert alert-success"><%= flash[:notice] %></div>
    <% end %>
    <% if flash[:alert] %>
      <div class="alert alert-danger"><%= flash[:alert] %></div>
    <% end %>
    <%= simple_form_for [post, post.comments.build(session[:comment]) ], html: { class: 'form-horizontal', data: { toggle: 'validator' }} do |f| %>
      <%= f.error :base, error_method: :to_sentence %>
      <%= f.hidden_field :reply_comment, value: params[:comment_id] %>
      <%= f.input :name, required: true, input_html: {maxlength: 60 } %>
      <%= f.input :email, error: 'Please specify a valid email', required: true, type: 'email', input_html: { maxlength: 100 } %>
      <%= f.input :body, required: true, label: "Comment", type: 'text', input_html: { rows: '8', maxlength: 3600 } %>
      <%= f.button :submit, "Submit Comment", class: 'btn btn-primary' %>
    <% end %>
  </div>
</div>

the partial is called from a  <%= render 'partials/comments' %> inside the post/show. This partial represents all comments and the reply form. It renders a collection comment partials which represent the individual comments.

here is the comment partial. the comment_id is passed from the current comment when 'reply' is clicked

<div class="comment">
  <div class="row">
    <div class="col-sm-2 col-xs-3">
      <%= image_tag("user.jpg") %>
    </div>
  <div class="col-sm-8 col-xs-7">
    <div class="well">
      <h4><%= comment.name %><span>/<%= comment.created_at.to_time.strftime('%B %e at %l:%M %p') %></span></h4>
        <%= comment.body %>
      </div>
    </div>
    <div class="col-xs-2">
      <%= link_to "Reply", "#{post_path(post.slug,comment_id: comment.id)}#comment" %>
    </div>
  </div>
  <div class="row">
    <div class="col-xs-1">
    </div>
    <div class="col-xs-11">
      <%= render partial: 'partials/comment', collection: comment.sub_comments %>
    </div>
  </div>
</div>

this partial is rendered once for every comment that is added to a post. You'll notice that sub_comments are rendered as a sub-collection of the same comment partial - It can recursively render itself as long as there are comments replying to comments.


Root comments are all comments that don't have don't have a reply comment attached - They are not replying to anyone and are at highest level of the comment structure

class Post < ActiveRecord::Base

  ...

  def root_comments
    comments.where(reply_comment: nil)
  end
end




The result is what's visible in the opening screenshot. 

User

EjxbOVIRCiteNDecember 11 2019 at 1:38 AM

xLQcpfHvUAuOrw
Reply
User

nSeptember 11 2020 at 10:25 AM

ggg
Reply
User

trhtrDecember 16 2019 at 1:26 PM

rtyhrtht
Reply
User

ddddMarch 21 2020 at 3:28 PM

dddd
Reply
User

dfhdfhDecember 16 2019 at 1:27 PM

sfdhfhf
Reply
User

fgjhfDecember 16 2019 at 1:27 PM

dgjgdjg
Reply
User

fghjhgfjDecember 16 2019 at 1:27 PM

gjgfj
Reply
User

dsfdghMay 9 2020 at 10:28 AM

teeeeeeest
Reply
User

vncvDecember 16 2019 at 1:28 PM

bcbgcnbv
Reply
User

cvncvbDecember 16 2019 at 1:28 PM

cvncvbncvn
Reply
User

fgjfgDecember 16 2019 at 1:28 PM

gjghjg
Reply
User

fhdfhgfDecember 16 2019 at 1:26 PM

hgfdhghdg
Reply
User

vPaYEUbZrzpyojtCDecember 11 2019 at 1:38 AM

XRAwfWjzbpJFv
Reply
User

TestJanuary 16 2020 at 11:06 AM

Test
Reply
User

bQzFrpDAXPSMay 13 2020 at 3:17 AM

oSDPBYIVpmR
Reply
User

WDBtNOLvclMay 13 2020 at 3:17 AM

aqwnYXitSrD
Reply
User

rhwsHYeDBWbqGjLJuly 17 2020 at 5:36 AM

pbLGQvaVEM
Reply
User

ojfeknPTRsJuly 17 2020 at 5:36 AM

oLWHZuidKOm
Reply
User

pNeOJgodhYjuSAugust 29 2020 at 5:49 AM

SDCYZpkJV
Reply
User

VgJsBtLdUSjuqMYAugust 29 2020 at 5:49 AM

TDBcbfUPdLp
Reply
User

YdHrRhAIouyTSsGFebruary 3 2020 at 10:40 PM

ptOFYNrCL
Reply
User

XPLbTizKqHectkuFebruary 3 2020 at 10:40 PM

VgPSFBGEvTYwjl
Reply
User

bansAugust 17 2020 at 6:12 AM

hey
Reply
User

GXisMtylRdacqMarch 29 2020 at 7:25 PM

xyPZUCqe
Reply
User

QZFOrKhVNTHujLIGMarch 29 2020 at 7:25 PM

UTdzlMGg
Reply
User

dsXJGZleNagyjJune 7 2020 at 5:11 PM

jPZolCLf
Reply
User

ozIPfADdEnwaXJune 7 2020 at 5:11 PM

NsACMzqYVkrOLbi
Reply
User

nandoJuly 26 2020 at 3:07 AM

nando
Reply
User

RqANYHSgvWIreVOpSeptember 4 2020 at 1:30 AM

pQMalhxCcWS
Reply
User

jBouQKLrNwCnMSeptember 4 2020 at 1:30 AM

cGPqrkoyK
Reply
User

SWNtChUabSeptember 15 2020 at 1:15 AM

hrCVSGgYjRepIdP
Reply
User

KsxCyXGnMBNfhIJUSeptember 15 2020 at 1:16 AM

NojWUgXYaTureZ
Reply
User

XDzUfLKHqeoaRDecember 27 2019 at 4:57 PM

saQzkOijynelASo
Reply
User

PrtkqdwnNWhzbOAuDecember 27 2019 at 4:57 PM

nVcMoDSLwqtiNYK
Reply
User

SbzWdVMvrBFebruary 22 2020 at 1:58 AM

TZolMifFjJxEDcRN
Reply
User

UcoVLqQjWaPTgFebruary 22 2020 at 1:58 AM

uhaEHyFW
Reply
User

KqSfxUCNHuriaWApril 29 2020 at 3:54 PM

GozyiDdEvF
Reply
User

DKStNdgeEQYcMVJApril 29 2020 at 3:54 PM

zJmBdYUP
Reply
User

BpCMHeWbJune 20 2020 at 1:35 PM

gNcQGHvY
Reply
User

NdyAufhKqxJune 20 2020 at 1:35 PM

yetbjZkvPNVCA
Reply
User

AKmbRlEStBoAugust 10 2020 at 11:24 AM

UwnbzkZjeWg
Reply
User

EZjCuyYKlGsPoAugust 10 2020 at 11:24 AM

QKpuLNsgCDXWam
Reply
User

OBHeZRaIxUkimvQNovember 6 2019 at 11:06 AM

OnsdcxXbzVIh
Reply
User

FhbutqJdNovember 6 2019 at 11:06 AM

zbnONtBGsqJ
Reply
User

fdgdMay 11 2020 at 10:03 AM

wfgdg
Reply
User

afasJuly 26 2020 at 3:02 AM

asdasdasd
Reply
User

wYBkDamGRdHIJVWSeptember 26 2020 at 6:24 PM

KbYWmcZghJ
Reply
User

cLpVevkgPJuDSeptember 26 2020 at 6:24 PM

VCFpqGhycozBXv
Reply

Replying to comment QZFOrKhVNTHujLIGMarch 29 2020 at 7:25 PM