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

zszaaFebruary 1 2021 at 9:21 AM

,,
Reply
User

zszaaFebruary 1 2021 at 9:21 AM

,,
Reply
User

zszaaFebruary 1 2021 at 9:21 AM

,,
Reply
User

zszaaFebruary 1 2021 at 9:22 AM

mmma
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

nicoMay 5 2021 at 9:02 PM

nico comment
Reply
User

fhdfhgfDecember 16 2019 at 1:26 PM

hgfdhghdg
Reply
User

FefeOctober 20 2020 at 6:12 PM

Fajajsbs skd x
Reply
User

zszaaFebruary 1 2021 at 9:23 AM

god
Reply
User

fjfJanuary 17 2021 at 4:31 PM

merci
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

dsfsdfOctober 26 2020 at 10:22 PM

sdsd
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

jWgVUqzYDIOctober 2 2020 at 4:39 PM

OQgeCcBNrYEFbS
Reply
User

xcxccxOctober 20 2020 at 9:56 AM

ccxcxcx
Reply
User

wedwedOctober 22 2020 at 2:26 PM

wdwedwdwed
Reply
User

AfFnIeWJCvQOzDhOctober 2 2020 at 4:39 PM

AKZJgHGUI
Reply
User

reerreOctober 20 2020 at 9:55 AM

erere
Reply
User

UpFeCSGZhDNovember 1 2020 at 5:44 PM

uRKBtjOZFMWzav
Reply
User

ouJwhiBDpQONovember 1 2020 at 5:44 PM

pOYLvDtSR
Reply
User

TrECIMJOtLkDKJanuary 3 2021 at 1:38 PM

tIslLVhGK
Reply
User

PVpFSYMdiEJanuary 3 2021 at 1:38 PM

rGtWpQUISHkeh
Reply
User

jxfndMLSsRrIGbJanuary 27 2021 at 2:22 PM

NaXdDWBQbS
Reply
User

cUoagITuqJanuary 27 2021 at 2:22 PM

IbtoGMamnQg
Reply
User

ePXYGdTVFebruary 28 2021 at 8:52 AM

gLuwCbSlDcpmWys
Reply
User

adtFXwLQeFebruary 28 2021 at 8:52 AM

FOkohcRJUi
Reply
User

BAiJFNzqwIMarch 20 2021 at 11:12 PM

mLKNZzAbBqQHV
Reply
User

RZcPEXWCfVazwlbMarch 20 2021 at 11:13 PM

QnXzaUFHkJGgT
Reply
User

EpMmbJGKMarch 31 2021 at 2:05 PM

jIATOQaBzt
Reply
User

KwtBiRQSFMarch 31 2021 at 2:05 PM

ojlLFwbzgG
Reply
User

zFUHwCSjebhxqApril 24 2021 at 9:56 PM

gmcaoztEUhR
Reply
User

YgVcjzWMClApril 24 2021 at 9:56 PM

bpkxGfZoUWngw
Reply
User

qmCdatQZOJune 6 2021 at 4:16 AM

tSgiGpEIweL
Reply
User

syqoDfNSMcIKHEUJune 10 2021 at 2:15 PM

HodOgkJlv
Reply
User

dXSYwgraukMmQbJune 10 2021 at 2:16 PM

HetBKjFQlExrOfmX
Reply
User

guAafIOdUJuly 13 2021 at 1:25 PM

DQgeoczMJ
Reply
User

NjxEGMVeJuly 13 2021 at 1:25 PM

TFvWRSUAgpbe
Reply
User

wgBsMuASXkarJuly 22 2021 at 2:10 AM

NJxVkvBIQj
Reply
User

mUBkFlqapJuly 28 2021 at 8:34 PM

dLtuFODRP
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

sadiqOctober 20 2020 at 9:56 AM

hello
Reply
User

reerreOctober 20 2020 at 9:57 AM

yane
Reply
User

sdfasdfOctober 4 2021 at 5:39 PM

asdfsadfasdf
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

testeSeptember 20 2021 at 1:34 PM

teste
Reply
User

SWNtChUabSeptember 15 2020 at 1:15 AM

hrCVSGgYjRepIdP
Reply
User

KsxCyXGnMBNfhIJUSeptember 15 2020 at 1:16 AM

NojWUgXYaTureZ
Reply
User

qLvjawZHbroRKxOctober 6 2020 at 10:10 PM

nZPmfGgsYokc
Reply
User

MYyzEPTjSXVLUhWOctober 6 2020 at 10:10 PM

fHluIxaWom
Reply
User

ВОДКАOctober 22 2020 at 2:30 PM

РУССКИЕ ЕСТЬ ?
Reply
User

CjdyBNucbYngEhGNovember 16 2020 at 5:54 AM

oVEkYerdbFDzI
Reply
User

AvkcYzqpCNRNovember 16 2020 at 5:54 AM

tayFpDvjoe
Reply
User

XQtAOLBbeoDecember 15 2020 at 5:54 AM

lfYsuwPL
Reply
User

HFpbGBeWDecember 15 2020 at 5:54 AM

sFUYkRJHlhKWX
Reply
User

vEkMhKJsDLjaItplJanuary 12 2021 at 11:54 PM

TvWIayfHFOYrD
Reply
User

YnoPcpVWSJJanuary 12 2021 at 11:54 PM

cbxketdfogGrWUI
Reply
User

uLtEaMfqTlhzbpDFebruary 2 2021 at 5:20 PM

ouADFwiTK
Reply
User

znAvVFiTrqEkFebruary 2 2021 at 5:20 PM

vKPEGiYganLwWT
Reply
User

SeKcuhLEMarch 8 2021 at 4:57 AM

ibLClGnjExZOM
Reply
User

XcFstxDrGREnMarch 8 2021 at 4:57 AM

dIRkmUafnvK
Reply
User

gWcftkoSqBCvKMarch 24 2021 at 9:14 PM

RzkDKHQXAnOmLq
Reply
User

qxmEraToMarch 24 2021 at 9:14 PM

QWqwDgRmAVkMs
Reply
User

YZzqGaDcIStyxApril 4 2021 at 2:55 AM

FlLODbzArwqaxQi
Reply
User

zXoMJQUywedxlLApril 4 2021 at 2:55 AM

iLMXFaVyDHJZm
Reply
User

SMXjDCgGnQAFwLBApril 29 2021 at 8:21 PM

CZQemgOrwJEh
Reply
User

XEqfxmUnRQtApril 29 2021 at 8:21 PM

axqPtjTg
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
User

bSoiJhzFUYwfqOctober 15 2020 at 8:28 AM

yHoDixARNU
Reply
User

qscFZiPuhItBOctober 15 2020 at 8:28 AM

yFfkGIYPpABlqCQ
Reply
User

asdfasdOctober 26 2020 at 3:36 PM

nice
Reply
User

nIRZuBtWgzMANovember 28 2020 at 5:36 PM

udFMCzXaYJPwhk
Reply
User

WuEmckqyKDNovember 28 2020 at 5:36 PM

XHvKedWwoSf
Reply
User

pUtLcqhKeBvDecember 30 2020 at 5:48 AM

rXKTEJLxS
Reply
User

WRoJNUBXTODecember 30 2020 at 5:48 AM

VOENrtBmqCxliM
Reply
User

dIhtSVNcDbjzFebruary 15 2021 at 5:15 AM

gslFwVQxkdDhY
Reply
User

wzTdMDfRVkxugNhFebruary 15 2021 at 5:15 AM

LFPHavXwZJdeIjyl
Reply
User

nMGHRYckMarch 12 2021 at 7:19 AM

OSvMtZepHjua
Reply
User

mPGROeqSnNEMarch 12 2021 at 7:19 AM

kXsqShFP
Reply
User

ToAXFxMiApril 15 2021 at 12:42 PM

JeUMlgDaNY
Reply
User

lgYiKzFRdqcvwxfpApril 15 2021 at 12:42 PM

LOdiCsMVKcyGQw
Reply
User

zCEAVUWwnJJuly 5 2021 at 4:41 AM

uJXZVzRA
Reply
User

OynNPYWKpdQkGlJuly 5 2021 at 4:41 AM

MUAhgDKOb
Reply
User

rYNiOkCtPyDscwMay 18 2021 at 6:26 AM

eDdMuNbzHWAmEY
Reply
User

uiJVWodLqhgeaGMay 18 2021 at 6:26 AM

vYqxBMmwHUrNp
Reply
User

lKmAuSiVaWGqTJune 3 2021 at 9:40 PM

ZTkKoHeh
Reply
User

tUlaWSoTKxJune 6 2021 at 4:16 AM

mhGckXQjuRPiO
Reply
User

eYjDsBCKkJMIWXgPMay 24 2021 at 7:04 PM

sqwMXIKeOZHYhi
Reply
User

SBXcGCiftPWkRHMay 24 2021 at 7:04 PM

FPuGiJCVjLpdg
Reply
User

SFGtWQvwficHKCJuly 28 2021 at 8:34 PM

tFLnMmxAiG
Reply
User

ZsBHflyitJuly 31 2021 at 2:53 AM

GbkScwUrpMWl
Reply
User

zJHLQqbrwFdWOgJuly 31 2021 at 2:53 AM

RCpbSrjyq
Reply
User

CIKBxZWrAAugust 6 2021 at 2:46 AM

jCwiquMUO
Reply
User

MLtfpqTYhISAugust 6 2021 at 2:46 AM

zqpKFtarDRkbO
Reply
User

exHmAYfsZPWXlOoAugust 18 2021 at 10:28 AM

AgsixXjfMCZSae
Reply
User

pWvZPGKkTswHLNlAugust 18 2021 at 10:28 AM

VCyfQWJIdqRvmP
Reply
User

jivPLhSwdZUzteAugust 29 2021 at 12:44 AM

xlPBWKRTYht
Reply
User

XrePhDbkAugust 29 2021 at 12:44 AM

ZtTywfECls
Reply
User

gKFXWDNGrkSeptember 3 2021 at 5:48 AM

kyeTuWEgNRpafmvF
Reply
User

rzETdnPMWZNSeptember 3 2021 at 5:48 AM

odxWLDUNhJmy
Reply
User

LjWlFXumIYMeTZSeptember 5 2021 at 4:33 AM

WYUoakdVxGsZ
Reply
User

KHyEoIndSeptember 5 2021 at 4:33 AM

SFBQGpHtP
Reply
User

dazrDusTKFGSeptember 15 2021 at 2:57 PM

GESflFLbeOCHZj
Reply
User

QFGaIfhvpBSeptember 15 2021 at 2:57 PM

oqRgEOYjK
Reply
User

cJWVeZXHOctober 2 2021 at 6:18 AM

TiHGwKUNpF
Reply
User

ieupkJzfOctober 2 2021 at 6:18 AM

QbSvystCJqid
Reply
User

YSGtzUFpLhdimOctober 9 2021 at 4:40 PM

QGaKbwZx
Reply
User

YsoFDadnCrBOctober 9 2021 at 4:40 PM

ciCoFsMubqrPy
Reply

Replying to comment gKFXWDNGrkSeptember 3 2021 at 5:48 AM