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

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

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

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

https://qiita.com/taddy_korokoro/items/4d75e33ceb5c7a5124a8

#完成形はこちら
####返信コメントの投稿と削除
![b80bacf19744a1db6422cdc7ac5dd2e4 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131643/0b2c7ba5-434f-e23b-1e13-2d3ca852f007.gif)

#環境
`macOS Big Sur 11.2.3`
`ruby: 2.7.2`
`rails: 6.1.3`
`テンプレートエンジン: slim`
`レイアウト: Bootstrap4`

#DB設計（ER図）
users : travel_records = １対多
users : comments = １対多
travel_records : comments = １対多
comments = usersとravel_recordsの中間テーブル

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1131643/aaccf9ff-eea4-4114-06bc-603d2f4a0530.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_to`と`has_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
    # 返信フォームに渡しているインスタンス変数の追加（下記２行）
    @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`で返信コメントを表示することができます。

また、**通常コメントの削除と違い**、第２引数のパスの引数を`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:

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

#参考記事

https://qiita.com/j-sunaga/items/af4014a99f5704b04d95

https://qiita.com/rockguitar67/items/4fd28cba0c243a8d0ba5