#実装内容
Ajaxを用いて非同期処理を行い、コメント返信機能を実装していきます。
- 返信コメントの投稿
- 返信コメントの削除
また、前回のコメント投稿機能の続きになりますので、まだの方は是非こちらからご覧ください(下記記事)
#環境
macOS Big Sur 11.2.3
ruby: 2.7.2
rails: 6.1.3
テンプレートエンジン: slim
レイアウト: Bootstrap4
#DB設計(ER図)
users : travel_records = 1対多
users : comments = 1対多
travel_records : comments = 1対多
comments = usersとravel_recordsの中間テーブル
#カラムの作成
% rails g migration AddReferencesToComments
class AddReferencesToComments < ActiveRecord::Migration[6.1]
def change
add_reference :comments, :parent, foreign_key: { to_table: :comments }
end
end
commentsテーブルに、自身のidを外部キーとするカラム「:parent
」を追加します。
% rails db:migrate
マイグレーションを実装すると下記の通り、commentsテーブルにparent_idが作成されます。
create_table "comments", force: :cascade do |t|
t.text "comment"
t.integer "user_id"
t.integer "travel_record_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
# カラムが追加されました
t.integer "parent_id"
t.index ["parent_id"], name: "index_comments_on_parent_id"
end
#Commentモデルの編集
class Comment < ApplicationRecord
belongs_to :user
belongs_to :travel_record
# 追加
belongs_to :parent, class_name: "Comment", optional: true
# 追加
has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy
validates :comment, presence: true, length: { maximum: 300 }
end
追加したbelongs_to
とhas_many
で自分自身に対する1対多のアソシエーションを設定しています。
また、 parentカラムに対してoptional:true
を付与することで、nilを許可して、返信コメントでない通常コメントも保存できるようにしています。
:parent
は、返信対象となるコメントのidで、:replies
は、返信されたコメントが格納されます。
#Commentsコントローラの編集
class TravelRecordsController < ApplicationController
def show
@user = @travel_record.user
@comments = @travel_record.comments.order(created_at: :desc)
@comment = Comment.new
# 返信コメントの作成
@comment_reply = @travel_record.comments.new
end
class CommentsController < ApplicationController
def create
@travel_record = TravelRecord.find(params[:travel_record_id])
#投稿に紐づいたコメントの作成
@comment = @travel_record.comments.new(comment_params)
@comment.user_id = current_user.id
# 返信コメントの作成
@comment_reply = @travel_record.comments.new
if @comment.save
flash.now[:notice] = "コメントの投稿に成功しました。"
render :index
else
flash.now[:alert] = "コメントの投稿に失敗しました。"
render :index
end
end
def destroy
# 返信フォームに渡しているインスタンス変数の追加(下記2行)
@travel_record = TravelRecord.find(params[:travel_record_id])
@comment_reply = @travel_record.comments.new
@comment = Comment.find(params[:id])
@comment.destroy
flash.now[:notice] = "コメントを削除しました。"
render :index
end
private
# ストロングパラメーターの追加
def comment_params
params.require(:comment).permit(:comment, :user_id, :travel_record_id, :parent_id)
end
end
返信コメントを作成するため、travel_recordsコントローラとcommentsコントローラに@comment_reply
というインスタンス変数を作成しています。
また、destroyアクションには、非同期で削除するために必要になるインスタンス変数をcreateアクション同様記述しています。
ストロングパラメーターには、先ほど追加したカラム:parent_id
を記述しています。
#ビューの編集
###travel_records/show(投稿詳細ページ)
#comments_area
/ ローカル変数の追加
= render 'comments/index', comments: @comments, comment_reply: @comment_reply
- if user_signed_in?
= render 'comments/form', travel_record: @travel_record, comment: @comment
travel_recordsコントローラのshowアクションで定義した@comment_reply
をローカル変数「comment_reply: @comment_reply
」に追加しています。
###comments/_index.html.slim(コメント一覧)
/ ----- 'travel_records/show'で使用中 -----
h5 コメント一覧(#{comments.count}件)
hr
= render 'layouts/flash_messages'
/ where(parent_id: nil)の追加
- comments.where(parent_id: nil).first(2).each do |comment|
/ ローカル変数の追加
= render 'comments/hide_comments', comment: comment, comment_reply: comment_reply
/ where(parent_id: nil)の追加
- comments.where(parent_id: nil).offset(2).any?
.text-right
button.btn.btn-primary[data-toggle="collapse" data-target="#collapseExample" aria-expanded="false" aria-controls="collapseExample"]
| もっと見る....
.collapse#collapseExample
- comments.where(parent_id: nil).offset(2).each do |comment|
/ ローカル変数の追加
= render 'comments/hide_comments', comment: comment, comment_reply: comment_reply
where(parent_id: nil)
を付与することで、返信コメントではないコメントを表示するようにしています。
これは、パーシャル化したcomments/_hide_comments.html.slim
内に記述している通常コメントの表示「= comment.comment
」と返信コメントの表示「= reply.comment
」が重複して表示されるのを防ぐためです。
また、ここでも各パーシャルにローカル変数「comment_reply: @comment_reply
」に追加しています。
###comments/_hide_comments.html.slim
/ ----- 'comments/_index'で使用中 -----
.container
.row.d-flex.align-items-center.pb-3
.col
- if comment.user.profile_image?
= link_to image_tag(comment.user.profile_image.thumb.url), user_path(comment.user.id)
- else
= link_to image_tag('no_profile_img.png'), user_path(comment.user.id)
= link_to "#{comment.user.name}さん", user_path(comment.user.id), class: 'link pl-1'
.col.d-flex.justify-content-end
= comment.created_at.strftime('%Y/%m/%d')
.card-body.border.rounded.mb-3
= comment.comment
hr.d-none
/----- 返信コメントの表示追加 -----
- comment.replies.each do | reply |
.container.w-95.pt-3
.row.d-flex.align-items-center.pb-3
.col
- if reply.user.profile_image?
= link_to user_path(reply.user.id)
= image_tag(reply.user.profile_image.thumb.url)
- else
= link_to user_path(reply.user.id)
= image_tag('no_profile_img.png')
= link_to user_path(reply.user.id), class: 'link pl-1'
| #{reply.user.name}さん
.col.d-flex.justify-content-end
= reply.created_at.strftime('%Y/%m/%d')
.card-body.border.rounded.mb-3
= reply.comment
- unless comment.user == current_user
= link_to travel_record_comment_path(comment.travel_record_id, reply.id), method: :delete, remote: true do
.text-right
button.btn.btn-outline-secondary
| 削除する
/ ----- ここまで ----
- if comment.user != current_user
/ コメント返信フォーム
= render 'comments/reply_form', comment: comment, comment_reply: comment_reply
- if comment.user == current_user
= link_to travel_record_comment_path(comment.travel_record_id, comment.id), method: :delete, remote: true do
.text-right
button.btn.btn-outline-secondary
| 削除する
hr
= comment.comment
**(通常コメント)**の中に返信コメント用の表示画面を追加しています。
- comment.replies.each do | reply |
とすることで、reply
に返信コメントの情報が格納され、= reply.comment
で返信コメントを表示することができます。
また、通常コメントの削除と違い、第2引数のパスの引数をcomment.id
からreply.id
に変更しています。
こうすることで、返信コメントを削除することができるようになります。
- if comment.user != current_user
とすることで、他ユーザーのコメントのみ返信フォームを表示させるようにしています。
####comments/_reply_form.html.slim(返信フォーム)
/ ----- 'comments/_hide_comments'で使用中 -----
= form_with model: [@travel_record, comment_reply], local: false do |f|
.form-group
= f.label "コメントに返信する"
= f.text_area :comment, :size => "3x3", autocomplete: 'off', placeholder: "コメントに返信してみよう", class: 'form-control'
= f.hidden_field :parent_id, value: comment.id
.text-right
= f.submit "返信する", class: 'btn btn-warning text-white'
返信対象のコメントを特定するため、= f.hidden_field :parent_id, value: comment.id
を記述し、送信先コメントのidをparent_idに格納しています。
###comments/index.js.erb(create,destroy兼用)
$("#comments_area").html("<%= j(render 'index', { comments: @comment.travel_record.comments.order(created_at: :desc), comment_reply: @comment_reply }) %>");
$("textarea").val('');
最後に、jsファイルにもローカル変数「comment_reply: @comment_reply
」を追加します。
#最後に
以上で非同期でのコメント返信投稿機能(投稿・削除)の実装になります!
コメント返信機能に関しては、通常のコメント投稿機能と比べて参考記事が少なく、何度も躓いていたのですが、メンターさんに相談を重ねて何とか実装することができました!
同じ境遇で困っている方のお役に立てればうれしいです
間違っている箇所や分かりづらい箇所が多々あるかと思います。
その際は、気軽にコメントいただけれると幸いです
最後までご覧いただき、ありがとうございました!
#参考記事