Ask A Question

Notifications

You’re not receiving notifications from this thread.

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?

Reply

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?

Reply

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()
Reply

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?

Reply

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.

Reply

Great tutorial.

Unfortunately, this breaks the max_nesting from the last episode :/

Reply

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!

Reply

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
Reply

Hey Chris, Adding Ajax breaks the max_nesting. Any idea how to fix it?

Reply

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.

Reply
Join the discussion
Create an account Log in

Want to stay up-to-date with Ruby on Rails?

Join 85,972+ developers who get early access to new tutorials, screencasts, articles, and more.

    We care about the protection of your data. Read our Privacy Policy.