【Ruby on Rails備忘録】　コメント機能　返信機能つき

## 初めに
レシピ投稿サイトを作成中の初学者です。
自分の記録のために書いてますが、どなたかの参考にもなれたのなら幸いです。

## 前提
コメントモデルは作成されているものとして進めていきます。
コメントモデルのカラムは以下のように設定しました。

``` Ruby
  create_table "comments", force: :cascade do |t|
    t.text "body"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "recipe_id"
    t.integer "user_id"
    t.integer "parent_id"
    t.integer "reply_to_id"
  end
```

後にこのカラムの意味は解説します。


## モデルにアソシエーションの追加

userモデルとrecipeモデルに関連づけます

``` Ruby:app/models/recipe.rb
class Recipe < ApplicationRecord
  has_many :comments, dependent: :destroy
end
```

``` Ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :comments, dependent: :destroy
end
```

``` Ruby:app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :recipe
  belongs_to :user
  belongs_to :parent, class_name: 'Comment', optional: true
  has_many :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: :destroy
  belongs_to :myreply, class_name: 'Comment', foreign_key: :reply_to_id, optional: true
end
```

Commentモデルは返信機能にちなんだモデル設計となっています。
説明します。

``` Ruby
belongs_to :parent, class_name: 'Comment', optional: true
```

これは返信先のコメントのみを格納するための関連名です。
youtubeで言うところのこの部分ですね。

![c5abe0683f35c32a968c59313728ab72.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758355/f54b82e7-bc9b-19dd-be11-bfb2ee145bec.png)

次行きます。

``` Ruby
has_many :replies, class_name: 'Comment', foreign_key: :parent_id, dependent: 
```

これは返信コメントを返信先ごとにまとめたものですね。
youtubeで言うところのこのコメントたちです。

![904e87a8a256c1da8b5ce7ba8e5f7df8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758355/06c28e7a-e832-0d3a-9d64-80da13b65303.png)


``` Ruby
foreign_key: :parent_id
```
を目印に仕分けをしています。

次行きます。

``` Ruby
belongs_to :myreply, class_name: 'Comment', foreign_key: :reply_to_id, optional: true
```

これは返信コメントに対して返信するコメントを格納しています。
youtubeで言うところのこの赤枠部分です。

![61b55c2dccb40652e94dd3e3dfcccad3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758355/36819a6f-b0d3-5e63-0805-baf52467dfc9.png)


少し自分で書いていても混乱してきました笑
まあこれらの関連名に格納していくことでコメントが誰に返信しているかわかるようになっています。

## ルーティングの設定

```Ruby:config/routes.rb
Rails.application.routes.draw do
  resources :recipes do
    resources :comments, only: [:create, :destroy, :update]
  end
end
```

コメントのルーティングはレシピにネストされた形で描きます。
これでレシピ一つ、一つに対して、コメントが個々に割り振られるようになりました。

## Commentsコントローラの作成
``` Ruby
class CommentsController < ApplicationController
  before_action :set_recipe

  def create
    comment = @recipe.comments.build(comment_params)
    if comment.save
    end
  end

  private
  
  def comment_params
    params.require(:comment).permit(:body, :parent_id, :reply_to_id)
  end

  def set_recipe
    @recipe = Recipe.find(params[:recipe_id])
  end
end

```

今回はコメントの作成部分に焦点を当てて解説します。
まずset_recipeです。これはURLから受け取ったidを使ってレシピを検索し,@recipeに設定しています。
before_actionで全てのアクションが実行される前に設定させています。

次にコメントの作成部分である、createアクションです。
``` Ruby
@comment = @recipe.comments.build(comment_params)
```

このコードでコメントのインスタンスを作っています。
インスタンスはかんたんにいうと変数みたいなものです。
この変数（インスタンス）にコメントの中身が入れられ、

``` Ruby
if comment.save
```

で保存されます。

## viewで使う変数

次はviewに渡す変数を決めます。
以下のように設定してください。

``` Ruby:app/controllers/recipes_controller.rb
def show
  @recipe = Recipe.includes(:steps).find(params[:id])
  @comments = @recipe.comments.includes(:user, replies: [:user, {myreply: :user}]).order(created_at: :desc)
  @comment = @recipe.comments.new
  @replies = Comment.includes(:user).where.not(reply_to_id: nil)
  @parent_comments = @comments.where(parent_id:nil)
end
```

順に解説していきます。
``` Ruby
@recipe = Recipe.includes(:steps).find(params[:id])
```
これはレシピ詳細画面で見ているレシピを取得しています。URLに書かれたidのレシピですね。
N＋1問題が起きないようにincludes(:steps)でレシピの手順のモデルごとクエリを発行しています。まあ今回関係ないので省略ですね。

``` Ruby
@comments = @recipe.comments.includes(:user, replies: [:user, {myreply: :user}]).order(created_at: :desc)
```

これはレシピに付属するコメントを全て取得しています。
includes(:user, replies: [:user, {myreply: :user}])は先ほど同様N＋1問題避けです。
order(created_at: :desc)はコメントを新しい順にするためのものです。


``` Ruby
@comment = @recipe.comments.new
```

これはコメントを作成する際に新しくインスタンス（変数）を作らないといけないので書きました。
view側でも設定できますが、自分はshowアクションに書いておきました。

``` Ruby
@replies = Comment.includes(:user).where.not(reply_to_id: nil)
```
これは返信先が親コメントだけへの返信コメントをクエリで取得した変数です。
これですね。

![904e87a8a256c1da8b5ce7ba8e5f7df8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3758355/06c28e7a-e832-0d3a-9d64-80da13b65303.png)

## view
続いてコメントをレンダリングするhtmlの中身についてです。
このように書きます。

``` Ruby:app/views/recipes/show.html.slim
  - @parent_comments.each do |comment|
    = render partial: 'shared/comment/comment_body', locals: {recipe: @recipe, comment: comment }
      
    - comment.replies.each do | reply |
      = render partial: 'shared/comment/comment_reply', locals: { recipe: @recipe, comment: comment, replycomment: reply }
```

解説します。

``` Ruby
- @parent_comments.each do |comment|
```
これは先ほど格納した親コメントを一つずつ取り出すコードです。
そして

``` Ruby
= render partial: 'shared/comment/comment_body', locals: {recipe: @recipe, comment: comment }
```
に取り出したcommentを渡すことで親コメントを表示させることができます。

そしてその親コメントを取り出した状態でまたeach文を回します。

``` Ruby
- comment.replies.each do | reply |
```
これはその親コメントに存在する返信コメント達でeach文を回しています。
そして取り出したその返信コメントを
``` Ruby
= render partial: 'shared/comment/comment_reply', locals: { recipe: @recipe, comment: comment, replycomment: reply }
```
に渡して返信コメント画面に表示させます。
これは返信コメントであれば全て表示されます。
返信先への返信コメントもまとめて表示です。

親コメントのviewはこのようになってます。

``` html
.container.w-95.pt-3
  p #{comment.user.name} さん　#{comment.created_at.strftime('%Y/%m/%d　%H時%M分')}
```

親コメントは返信コメントに比べてカードが大きめです。
視覚的にわかりやすくしました。

返信コメントは
```
.container.w-75.pt-3
  -if replycomment.myreply.present?
    p 返信先: #{link_to replycomment.myreply.user.name,user_path(replycomment.myreply.user)}
  = replycomment.body
```
返信コメントはカード小さめです。
また

```
p 返信先: #{link_to replycomment.myreply.user.name,user_path(replycomment.myreply.user)}
```

は返信コメントの中でも返信先を持つものだけ表示される返信先への名前ですね。
これで返信への返信を表現しています。

## formのview

コメント入力フォームはこのようになってます。
```
= form_with model: [recipe, Comment.new], url: recipe_comments_path(recipe), local: true, html: { class: "unique-form-class" } do |f| 
  .row_margin
    = f.text_field :body, placeholder: 'コメントを入力してください', class: 'form-control' 
  = f.submit 'コメントを作成する'
```

親コメントが持つ返信フォームはこのようになっています。
```
= form_with model: [@recipe, Comment.new], method: :post, id: "replyFormTemplate", local: true do |f|
  .row_margin
    p 返信先 #{comment.user.name}
    = f.text_field :body, placeholder: '返信を入力してください', class: 'form-control' 
    = f.hidden_field :parent_id, value: comment.id, id: "parent_id"
    = f.hidden_field :id
  .text-end
    = f.submit '返信する', class: 'btn btn-warning text-white'
```
返信コメントが持つ返信フォームはこのようになっています。
```
= form_with model: [recipe, @comment], id: "replyFormTemplate", local: true, html: { class: "unique-form-class" } do |f|
  .row_margin id = "aaa"
    /# 返信コメント主名前を表示
    p 返信先 #{replycomment.user.name}
    = f.text_field :body, placeholder: '返信を入力してください', class: 'form-control' 
    = f.hidden_field :parent_id, value: comment.id, id: "parent_id"
    = f.hidden_field :reply_to_id, value: replycomment.id, id: "reply_to_id"
  .text-end
    = f.submit '返信する', class: 'btn btn-warning text-white'
```

違いは以下のようになってます。

親コメント
parent_id　なし
reply_to_id　なし

親コメントへの返信
parent_id　あり
reply_to_id　なし

返信コメントへの返信
parent_id　あり
reply_to_id　あり

これらの要素でコメントの状態を表しています。

以上で完成です。

完成系はこのような形です。

[![Image from Gyazo](https://i.gyazo.com/b8dd45d9464b8243932e59b811ae8e98.png)](https://gyazo.com/b8dd45d9464b8243932e59b811ae8e98)


参考記事
https://qiita.com/asami___t/items/b47d90e2f565f5836039