Nested Comment Threads in Rails - Part 3 Discussion
Really great series, thank you for doing it. One question:
I have a comment form below the comments and it is not clearing out. However, when I move above the comments it does. Any thoughts?
Really great series, thank you for doing it. One question:
I have a comment form below the comments and it is not clearing out once the comment is posted. However, when I move the form above the comments (as shown in the tutorial) it does clear the form. Any thoughts?
There is a way to do this... on the show page for posts (or in my case, articles), pull the form into it's own div below and give it an ID like such:
<div id="comments">
<%= render @article.comments.where(parent_id: nil), max_nesting: 4 %>
</div>
<div id="topform">
<%= render partial: "comments/form", locals: { commentable: @article } %>
</div>
Then in your create.js.erb file, target that ID to reset the form in there as well like such:
var form = comments.parentElement.querySelector("form")
form.reset()
var topform = document.getElementById("topform").querySelector("form")
topform.reset()
Rails 5.2.1 comes with Content Security Policy DSL by default. Here we can specificy what is allowed to run. If we have something like
Rails.application.config.content_security_policy do |policy|
policy.default_src :self, :https
policy.connect_src :self
#...
policy.script_src :self
end
# If you are using UJS then enable automatic nonce generation
Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
in our CSP file I think this would disallow everything in create.js.erb
? We could add unsafe_eval
to the policy but I believe this negates the whole purpose.
What can we add to allow the create.js.erb
to be allowed by the Content Security Policy? I tried adding the <%= csp_meta_tag %>
as recommeded here https://edgeguides.rubyonrails.org/security.html#content-security-policy and mentioned here https://github.com/rails/rails/pull/32018. Am I understanding the architecture correctly?
Hi Chris - great tutorial. Is this methodology what you would recommend if you are adding comments to a new site? I know there are gems out there that build this in automatically. Also, what about Vue.js - do you think using Ajax in this way is preferable to building out some Vue.js component.
Hey Chris, just following up on Jake's comment. I'm also having the issue where max_nesting no longer works after implementing ajax following this video (the comments keep nesting past the max_nesting depth). Any idea what's causing this to break or how to fix it?
This series has been very helpful. Thanks so much!
I have fixed this issue but it requires quite a few changes. First of all, the cause is that the ajax is only rerendering the partial, which means the nesting value is not being incremented. That much is fairly obvious.
To fix this, I moved the max_nesting
into the Comment
model as a class variable. ie def self.max_nesting 3 end
. I then replace all references as Comment.max_nesting
. You can then move that part of the logic into the comments helper.
The second fix was to take the nesting value for the comment and add it as a field on the Comment model. So you know that the @comment.nesting
value is stored with the comment itself.
It is worth noting I have the paranoia/soft delete function set which I think has reduced my chances of the nesting becoming broken as comments are deleted.
In my comment controller, I am storing the comment nesting value through a Comment model method called set_nesting
. This increments from the parent comment OR sets it to 1.
comments_helper.rb
def reply_to_comment_id(comment, nesting)
nesting = 1 unless nesting.present?
max_nesting = Comment.max_nesting
if max_nesting.blank? || nesting < max_nesting
comment.id
else
comment.parent_id
end
end
end
comments_controller.rb
class CommentsController < ApplicationController
before_action :authenticate_user!
def create
@comment = @commentable.comments.new(comment_params)
@comment.nesting = @comment.set_nesting
@comment.user = current_user
if @comment.save
respond_to do |format|
format.html { redirect_to @commentable }
format.js
end
else
redirect_to @commentable, alert: "Something went wrong."
end
end
def destroy
@comment = @commentable.comments.find(params[:id])
@comment.destroy
redirect_to @commentable
end
def restore
@comment = @commentable.comments.with_deleted.find(params[:id])
@comment.restore
redirect_to @commentable
end
private
def comment_params
params.require(:comment).permit(:body, :parent_id)
end
end
comment.rb
class Comment < ApplicationRecord
acts_as_paranoid
belongs_to :user
belongs_to :commentable, polymorphic: true
belongs_to :parent, optional: true, class_name: "Comment"
validates :body, presence: true
validates_length_of :body, maximum: 140
def comments
Comment.with_deleted.where(commentable: commentable, parent_id: id).order(created_at: :asc)
end
def self.max_nesting
3
end
def set_nesting
if self.parent.present? && self.parent.nesting.present?
self.nesting = self.parent.nesting + 1
else
self.nesting = 1
end
end
end
_comment.html.erb
<div class="border-gray-300 border-l p-4 my-4 mt-2 ml-2">
<div class="flex"><%= comment.user.name %> says..</div>
<% if comment.deleted? %>
<div class="border-gray-300 border-l p-2 italic text-gray-500">
<%= simple_format "This comment has since been deleted..." %>
<div class="italic text-gray-500 text-sm">
<div class="flex">
<%= comment.created_at.strftime("%I:%M %p") %> • <%= comment.created_at.strftime("%d %b %y") %> <%= "~" + time_ago_in_words(comment.created_at) + " ago."%>
</div>
</div>
</div>
<% else %>
<div class="border-gray-300 border-l p-2">
<%= simple_format comment.body %>
<div class="italic text-gray-500 text-sm">
<div class="flex">
<%= comment.created_at.strftime("%I:%M %p") %> • <%= comment.created_at.strftime("%d %b %y") %> <%= "~" + time_ago_in_words(comment.created_at) + " ago."%>
</div>
</div>
</div>
<% end %>
<div class="mt-2" data-controller="reply">
<% if policy(comment).create? %>
<%= link_to "Reply", "#", class: "text-green-700", data: { action: "click->reply#show" } %>
<% end %>
<% if policy(comment).destroy? %>
<%= link_to "Delete", comment_path(comment, post_id: comment.commentable), method: :delete, class: "text-red-700", data: { confirm: "Are you sure?" } %>
<% end %>
<% if policy(comment).restore? %>
<%= link_to "Undo", restore_comment_path(comment, post_id: comment.commentable), method: :patch, class: "text-red-700", data: { confirm: "Restore this comment?" } %>
<% end %>
<%= render partial: "comments/form", locals: {
commentable: comment.commentable,
parent_id: reply_to_comment_id(comment, comment.nesting),
method: "post",
class: "hidden",
target: "reply.form"
} %>
</div>
<%= tag.div id: "#{dom_id(comment)}_comments" do %>
<%= render comment.comments, nesting: comment.nesting %>
<% end %>
</div>
create.js.erb
<% if @comment.parent_id? %>
var comments = document.querySelector("#<%= dom_id(@comment.parent) %>_comments")
<% else %>
var comments = document.querySelector("#comments")
<% end %>
comments.insertAdjacentHTML('beforeend', '<%=j render partial: "comments/comment", locals: { comment: @comment, nesting: @comment.nesting }, format: :html %>')
var form = comments.parentElement.querySelector("form")
form.reset()
<% if @comment.parent_id? %>
form.classList.add("hidden")
<% end %>
show.html.erb
<h2 class="title-2">Comments</h2>
<div class="mt-4">
<div id="comments">
<%= render partial: "comments/form", locals: {commentable: @post, method: "post" } %>
</div>
</div>
<%= render @comments %>
</div>
20200901061626_add_nesting_to_comments.rb
class AddNestingToComments < ActiveRecord::Migration[6.0]
def change
add_column :comments, :nesting, :integer
end
end
Hey Chris i followed everything and used the source code yet the comments wont load with the JS. I have other elements on the web application that render find with no refresh ajax and all. I don't understand why this isn't working.. Please let me know if you have any ideas!! Thank you very much I hope you're doing well during this time. Thank you.