0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Rails6 非同期でコメント返信機能実装(コメントへの投稿・削除)slim】

Posted at

#実装内容
Ajaxを用いて非同期処理を行い、コメント返信機能を実装していきます。

  • 返信コメントの投稿
  • 返信コメントの削除

また、前回のコメント投稿機能の続きになりますので、まだの方は是非こちらからご覧ください(下記記事)

#完成形はこちら
####返信コメントの投稿と削除
b80bacf19744a1db6422cdc7ac5dd2e4 (1).gif

#環境
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の中間テーブル

image.png

#カラムの作成

% rails g migration AddReferencesToComments
db/migrate/_add_references_to_comments.rb
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が作成されます。

db/schema.rb
  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モデルの編集

app/models/comment.rb
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_tohas_manyで自分自身に対する1対多のアソシエーションを設定しています。
また、 parentカラムに対してoptional:trueを付与することで、nilを許可して、返信コメントでない通常コメントも保存できるようにしています。

:parentは、返信対象となるコメントのidで、:repliesは、返信されたコメントが格納されます。

#Commentsコントローラの編集

app/controllers/travel_records_controller.rb
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
app/controllers/comments_controller.rb
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(投稿詳細ページ)

app/views/travel_records/show.html.slim
#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(コメント一覧)

app/views/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

app/views/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(返信フォーム)

app/views/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兼用)

app/views/comments/index.js.erb
$("#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」を追加します。

#最後に
以上で非同期でのコメント返信投稿機能(投稿・削除)の実装になります!

コメント返信機能に関しては、通常のコメント投稿機能と比べて参考記事が少なく、何度も躓いていたのですが、メンターさんに相談を重ねて何とか実装することができました!

同じ境遇で困っている方のお役に立てればうれしいです:grin:

間違っている箇所や分かりづらい箇所が多々あるかと思います。
その際は、気軽にコメントいただけれると幸いです:pray:

最後までご覧いただき、ありがとうございました!

#参考記事

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?