Railsでのコメント返信機能の実装

#Railsでのコメント返信機能の実装

Railsでコメントの返信機能を実装した際に実施した手順をまとめます。

## 目次
* [動作環境](#動作環境)
* [やりたいこと](#やりたいこと)
* [手順](#手順)
* [結果](#結果)
* [おわりに](#おわりに)

---------------------------------------
## 動作環境
`OS : macOS Mojave 10.14.6`
`ruby : 2.6.3p62`
`rails : 5.2.4`

## やりたいこと
コメントに対して返信できる機能を実装したい。

完成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526570/d4f28a4e-3079-ca2a-3c0f-f80d2f1550b4.png)

## 実装前のモデル

Commentモデルは1対多でUserモデルとPostモデルに紐付いている状態

```ruby:schema.rb

create_table "comments", force: :cascade do |t|
    t.string "content", limit: 1000, default: "", null: false
    t.bigint "user_id"
    t.bigint "post_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
```

```ruby:comment.rb
class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post
```

## 手順

### STEP 1. マイグレーションファイルの作成

まずは自分自身のidを外部キーとするカラム(parent)を追加します。

```ruby:migration
class AddParentToComments < ActiveRecord::Migration[5.2]
  def change
    add_reference :comments, :parent, foreign_key: { to_table: :comments }
  end
end
```

マイグレーションの実行!

```bash
rails db:migrate
```

マイグレーション後にparent_idとインデックスの張られたt.index["parent_id"]が作成されます。

```ruby:schema.rb
create_table "comments", force: :cascade do |t|
    t.string "content", limit: 1000, default: "", null: false
    t.bigint "user_id"
    t.bigint "post_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "parent_id"
    t.index ["parent_id"], name: "index_comments_on_parent_id"
    t.index ["post_id"], name: "index_comments_on_post_id"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end

```
### STEP 2. Commentモデルの編集

次に以下のようにbelongs_toとhas_manyで自分自身に対する 1 対 多 のアソシエーションを設定します。
ここでポイントとしてparentカラムに対してoptional:trueとすることです。
nilを許可することで返信でない通常のコメントも登録できるようにします。

:parentは返信対象となるコメントのID
:repliesは返信されたコメント

```ruby:comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  belongs_to :post
  + belongs_to :parent,  class_name: "Comment", optional: true
  + has_many   :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy

  validates :content, presence: true, length: { in: 1..1000 }
end


```

### STEP 3. Commentsコントローラ

CommentsコントローラのStrongパラメータに`:parent_id`を追加します。
またformat.jsは今回はajaxを利用するため書いてますが
通常のredirect_toメソッドで良いです。

```ruby:CommentsController.rb

class CommentsController < ApplicationController
  def create
    @comment = current_user.comments.build(comment_params)
    respond_to do |format|
      if @comment.save
        format.js { render :index }
      else
        format.html { redirect_to post_path(@comment.post), notice: '投稿できませんでした...' }
      end
    end
  end

  private

  def comment_params
    params.require(:comment).permit(:content, :post_id, :user_id, :parent_id)
  end
end

```
### STEP 4. View/Routing

コメントはPostモデルのshow.htmlに表示されるため、
PostsControllerのshowメソッドでコメントのインスタンス変数を設定します。

```ruby:PostsController.rb

class PostsController < ApplicationController
  before_action :set_post, only: %i[show edit update destroy]
  before_action :authenticate_user!, only: %i[new edit update destroy]

  def index
    @posts = Post.all
  end

  def show
    @comments = @post.comments
    @comment = @post.comments.build #投稿全体へのコメント投稿用の変数
    @comment_reply = @post.comments.build #コメントに対する返信用の変数
  end

  private

  def set_post
    @post = Post.find(params[:id])
  end

  def post_params
    params.require(:post).permit(:title, :detail, :post_image, :post_image_cache, :deadline, :status, category_ids: [])
  end
end
```

CommentはPostモデルでネストします。

```ruby:routing.rb
Rails.application.routes.draw do
  root 'posts#index'
  devise_for :users
  resources :posts do
     resources :comments
  end
end

```

__Point 1. 2つのform_withタグは
1.コメントへの返信用
2.投稿へのコメント投稿用__

__Point 2. 1つ目のform_withでは返信対象のcommentを特定するため`form.hidden_field :parent_id, value: comment.id`を記載していますが
2つ目のform_withには返信対象のコメントはないためparent_idを渡していません__

```html:Show.rb
<h4>Post Detail</h4>

    <label>タスク</label>
    <input type="text" readonly class="form-control show-form" value="<%=@post.title%>">

  <div id="comments_area">
    <h2>comments</h2>
    <% @comments.each do |comment| %>
      <% if comment.parent_id.present? || comment.id.blank? %>
        <% next %>
      <% end %>
        <hr>
        <p> <%= comment.user.name %> : <%=comment.content%></p>
        <p> date : <%=comment.created_at.strftime("%Y-%m-%d %H:%M") %>
      <div id="reply_area">
        <% comment.replies.order(:created_at).each do |reply| %>
        <p> <%= comment.user.name %> : <%=reply.content%></p>
        <% end %>
      </div>
        <%= form_with(model:[@post,@comment_reply]) do |form| %>
        <div class="row">
          <div class="form-group col-md-6">
            <p><label>Reply</label></p>
            <textarea class="form-control input-form" name="comment[content]" rows="2"><%=@comment_reply.content%></textarea>
          </div>
        </div>
          <%= form.hidden_field :post_id, value: @post.id %>
          <%= form.hidden_field :parent_id, value: comment.id%>
        <div class="actions">
          <%= form.submit %>
        </div>
      <% end %>
    </div>
    <% end %>

    <%= form_with(model:[@post,@comment]) do |form| %>
      <p><label>New Thread</label></p>
      <textarea class="form-control input-form" name="comment[content]" rows="5"><%=@comment.content%></textarea>
      <%= form.hidden_field :post_id, value: @post.id %>
        <div class="actions">
          <%= form.submit %>
        </div>
    <% end %>
  </div>

    <%= link_to '編集', edit_post_path(@post) %>
    <%= link_to '戻る', posts_path %>
```

## 結果

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/526570/24ebcf87-dfec-b824-c694-eba3cb00c085.png)

## おわりに

コメント返信機能で自分自身のidを持つモデルを構築した。
1. 現状の問題点として全ての返信は全て最初のコメントを親として持つため、最新の返信コメントを親とするように修正が必要だと感じました。
2. 返信ボタンを押すと返信フォームが表示されるように設定したい