Comment
user:references, table:references, record_id:integer parent:references rely:references content:text
has_many :children
and has_many: replies
through parent/reply_id. cascade_reply_ids
which find all comment IDs that need to be deleted. Code
1. use data-turbolinks=false
to make anchor works without reloading pages
2. use js.erb
file to make operating comments AJAX without reloading pages
# db/migrate/20210227120803_create_comments.rb
class CreateComments < ActiveRecord::Migration[6.0]
def change
create_table :comments do |t|
t.references :user, null: false, foreign_key: true
t.references :table, null: false, foreign_key: true
t.integer :record_id, null: true
# For nested comment, parent links to the top-level comment, reply links to its reply
# parent comment
t.references :parent, null: true, foreign_key: {to_table: "comments"}
# reply comment
t.references :reply, null: true, foreign_key: {to_table: "comments"}
t.text :content, null: false, default: ""
t.timestamps
end
add_index :comments, [:table_id, :record_id]
add_index :comments, [:table_id, :record_id, :parent_id]
end
end
# app/models/comment.rb
class Comment < ApplicationRecord
default_scope { order('id DESC') }
belongs_to :user
belongs_to :table
belongs_to :parent, class_name: "Comment", optional: true
belongs_to :reply, class_name: "Comment", optional: true
has_many :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
has_many :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :destroy
# delete_all has better performance if many records. InvalidForeignKey error
#has_many :children, class_name: 'Comment', foreign_key: :parent_id, dependent: :delete_all
#has_many :replies, class_name: 'Comment', foreign_key: :reply_id, dependent: :delete_all
validates_presence_of :content
def cascade_reply_ids
# top-level comment
return self.children.pluck(:id) unless self.parent_id?
# non-top-level comment
ids = self.replies.pluck(:id); nids=ids.clone
while nids.any?
id = nids.pop
nn = Comment.where(table: self.table, record_id: self.record_id, parent_id:self.parent_id, reply_id: id).pluck(:id)
ids += nn;
nids.unshift(*nn)
end
return ids;
end
end
# app/views/comments/_comment_section.html.erb
<div class="comment-container mt-5 small">
<% if logged_in? %>
<div class="new-comment" id="new-reply">
<%= render "comments/new_comment", comment: comment %>
</div>
<% end %>
<div class="comments" id="children">
<%= render comments %>
</div>
</div>
# app/views/comments/_comment.html.erb
<% comment_id = "comment-#{comment.id}" %>
<% reply_container_id = "reply-container-#{comment.id}" %>
<% new_reply_id = "new-reply-#{comment.id}" %>
<% children_id = "children-#{comment.id}" %>
<div class="card mx-4 my-1" id="<%=comment_id%>">
<div class="card-body">
<div class="card-title">
<h6><%= link_to comment.user.name, f_user_path(comment.user) %></h6>
</div>
<% if (comment.parent_id && comment.reply_id && (comment.parent_id!=comment.reply_id)) %>
<div class='markdown-body'>
<% rc = "> @#{comment.reply.user.name} \n >> #{comment.reply.content.truncate(100)}" %>
<a href="#comment-<%=comment.reply.id%>" data-turbolinks="false">
<%= markdown(rc) %>
</a>
</div>
<% end %>
<div class='markdown-body'>
<%= markdown(comment.content)%>
</div>
<% tstyle = fmt_sh_style(comment.children.any?) %>
<% cid = "children-num-#{comment.id}" %>
<% cnum = comment.children.count %>
<% tstr = t_cat([("<div id='#{cid}' style='#{tstyle}'>"+cnum.to_s+"</div>"),t('reply'),">"]).html_safe %>
<div class="d-flex flex-row justify-content-between mt-2">
<div>
<%= link_to tstr, "javascript:void(0)", class: "click-toggle-id", "toggle-id": reply_container_id%>
<%= fmt_time(comment.created_at) %>
</div>
<% if logged_in? %>
<% if current_user==comment.user || admin? %>
<%= link_to fa_destroy_icon, comment_path(comment), class: "#{lh_btn_cls} btn-sm", method: :delete, remote: true, data: { confirm: t('destroy_confirm') } %>
<% end %>
<% end %>
</div>
<div class="comment-reply-container" id="<%=reply_container_id%>" style="<%=fmt_hide_style%>">
<div class="new-comment" id="<%=new_reply_id%>">
<% if logged_in? %>
<%= render "comments/new_comment", comment: Comment.new(table: comment.table, record_id: comment.record_id, parent_id: comment.parent_id || comment.id, reply_id: comment.id) %>
<% end %>
</div>
<div class="comment-children" id="<%=children_id%>" %>
<%= render comment.children if comment.children.any? %>
</div>
</div>
</div>
</div>
# app/views/comments/_new_comment.html.erb
<%= form_with(model: comment, remote: true) do |form| %>
<% if comment.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(comment.errors.count, "error") %> prohibited this comment from being saved:</h2>
<ul>
<% comment.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field form-group row justify-content-between">
<%= form.hidden_field :table_id %>
<%= form.hidden_field :record_id %>
<%= form.hidden_field :parent_id %>
<%= form.hidden_field :reply_id %>
<div class="col-md-10 col-xs-12">
<%= form.text_area :content, rows: 1, class:"form-control", placeholder: t("comment"), required: true %>
</div>
<div class="col-md-2 col-xs-12 text-center">
<%= form.submit t("submit"), class: "btn btn-secondary form-control"%>
</div>
</div>
<% end %>
# app/views/comments/create.js.erb
<% if @comment.parent_id.present? %>
// Clear form
$('.comment-container #new-reply-<%=@comment.reply_id%> form')[0].reset()
<% if @comment.reply_id!=@comment.parent_id %>
$('.comment-container #reply-container-<%=@comment.reply_id%>').hide()
<% end %>
// Add comment
$('.comment-container #children-<%=@comment.parent_id%>').prepend('<%= escape_javascript(render(@comment)) %>')
// Update children number
$('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: inline-block;')
<% cnum = @comment.parent.children.count %>
$('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
<% else %>
$('.comment-container #new-reply form')[0].reset()
$('.comment-container #children').prepend('<%= escape_javascript(render(@comment)) %>')
<% end %>
# app/views/comments/destroy.js.erb
// Remove comments
$('.comment-container #comment-<%=@comment.id%>').remove()
<% if @comment.parent_id.present? %>
// Remove comments's replies
<% @reply_ids.each do |id| %>
$('.comment-container #comment-<%=id%>').remove()
<% end %>
// Update children number
<% cnum = @comment.parent.children.count %>
// Set reply num
$('.comment-container #children-num-<%=@comment.parent_id%>').text(<%=cnum%>)
// Hide if no reply
<% if cnum <=0 %>
$('.comment-container #children-num-<%=@comment.parent_id%>').attr('style','display: none;')
<% end %>
<% end %>