YouTubeっぽいコメント欄を作ってみよう！（返信機能追加）

# はじめに
こんにちは！今回はYouTubeっぽいコメント欄を作ってみようと思います！
色々調べていたのですが、似たような記事がなかったので自力で実装した部分が多いです(><)
もっと良い方法があるかもしれないので、今後アップグレードしていければと思います。

それではいきましょう！


# 完成イメージ

![exhoop_200420_reply.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/487789/596291b7-72a8-1675-e23c-63c3141ccb2d.gif)

ユーザーアイコンの実装については過去記事を参照ください。
https://qiita.com/naoki00m/items/6430ab0b62766b582c9a


# 開発環境
- ruby 2.5.1
- Rails 5.2.3
- mysql
- Haml 5.1.2
- Ruby Sass 3.7.4


# 前提条件
- 投稿に紐づくコメント機能が実装できていること（TECH CAMPでいうところのPictweetコメント機能）


# 実装内容
- どのコメントに対する返信かを保存するカラムを追加
（Commentテーブルにreplyカラムを追加）

- コメントに対する返信フォームを作成
（form_withを使用）

- 返信フォームを表示するためのボタンを作成
（FontAwesomeを使用）

- 返信数をコメントの下に表示
（replyカラム内の値を計算）

- 返信数をクリックして返信を閲覧表示


# テーブル

### Comment テーブル
|Column  |Type   |Options    |
|--------|-------|-----------|
|user_id |integer|null: false|
|video_id|integer|null: false|
|text    |text   |null: false|
|reply   |integer|           |

#### Assosiation
- belongs_to :user
- belongs_to :video


# 実装手順

## 1. どのコメントに対する返信かを保存するカラムを追加
Commentテーブルにreplyカラムを追加します。これはどのコメントに対する返信かを表すカラムです。
id:1のコメントに対してid:2で返信した場合、idが2のレコードにあるreplyカラムに1が保存されるといった具合です。

```ターミナル.
$ rails g migration AddReplyToComment reply:integer
```

生成されたマイグレーションファイルを反映させましょう。

```ターミナル.
$ rails db:migrate
```


## 2. コメントに対する返信フォームを作成 + 返信フォームを表示するためのボタンを作成

### まずはビューの実装から！
今回はVideoのshowアクション内でコメントを表示しています。一般的にはPostのshowアクションと同義です。

```show.html.haml
(省略)
- @comments.each do |comment|
  -# replyカラムがnull（空）の場合（返信ではない場合）
  - if comment.reply.blank?
    .content__show__comment__bottom__top
      .content__show__comment__bottom__top__user
        -# ユーザーアイコンを表示
        .content__show__comment__bottom__top__user__icon
          = link_to "/users/#{comment.user.id}" do
            - if comment.user.image.present?
              = image_tag comment.user.image.url, width: '100%'
            - else
              = image_tag('/images/default_user.jpg', width: '100%')
        -# ユーザー名を表示
        .content__show__comment__bottom__top__user__name
          = link_to "/users/#{comment.user.id}" do
            = comment.user.name
        -# 返信ボタン、削除ボタンを表示
        .content__show__comment__bottom__top__icons
          -# コメントした本人であれば返信ボタンと削除ボタンを表示
          - if user_signed_in? && current_user.id == comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
            .content__show__comment__bottom__top__icons__delete
              = link_to video_comment_path(comment.video_id, comment.id), method: :delete do
                = icon('fa', 'trash-alt')
          -# ログインしているがコメントした人でない場合返信ボタンのみ表示
          - elsif user_signed_in? && current_user.id != comment.user.id
            .content__show__comment__bottom__top__icons__reply
              = link_to '#' do
                = icon('fa', 'reply', id: "reply-btn#{comment.id}")
          -# それ以外は何も表示しない
          - else
              = ''
  -# ユーザーがログインしている場合のみ返信できるようにする
  - if user_signed_in?
    .content__show__comment__bottom__reply
      -# idをcomment.idに応じて変化させる
      = form_with(model: [@video, @new_comment], local: true, id: "reply-form#{comment.id}") do |f|
        = f.text_area :text, placeholder: "Add a public reply...", id: "reply-text"
        -# 画面上には表示しないが、裏でreplyカラムに保存させるためにhidden_fieldを用いる！
        = f.hidden_field :reply, value: comment.id
        .content__show__comment__bottom__reply__btn
          .content__show__comment__bottom__reply__btn__cansel
            = 'Cansel'
          = f.submit "Reply", class: 'content__show__comment__bottom__reply__btn__send'
```

###そしたらJqueryでコメントのフォームに動きをつけます！

- 入力フォーム選択時にCansel, Replyボタンを表示する
- Canselボタン選択時に入力フォームを非表示にする

ファイル名は◯◯.jsになっていればなんでもOKです！

```video-comment.js

// 返信フォームの表示・非表示切り替え

$(function() {
  // replyという配列に1から1000までを順番に格納していく
  // 本当はビューやコントローラーからcomment.idを取ってきて格納できるのが理想
  var reply = [];
  for ( var i = 1 ; i <= 1000 ; i++ ) {
    reply.push(i);
  }
  // eachでreplyの中から1つずつ値を取り出しビューで定義したid名に付与
  $.each(reply, function(index, value) {
    // 入力フォームは初め隠しておく
    $("#reply-form" + value).hide();
    // 返信ボタンをクリックしたら入力フォームを表示
    $("#reply-btn" + value).on("click", function() {
      $("#reply-form" + value).show();
    });
    // Canselボタンをクリックしたら入力フォームを非表示に
    $(".content__show__comment__bottom__reply__btn__cansel").on("click", function() {
      $("#reply-form" + value).hide();
    });
  });
});
```


## 3. 返信数をコメントの下に表示

### 返信ができるようになったので試しに返信してみましょう！

テーブルのreplyカラムにidが保存されているか確認してください。
ビューに記載した「if comment.reply.blank?」を外すと返信も表示されますが、
コメントの下に紐づくような感じで表示はまだできていません。


### replyカラム内の値を計算しよう！

さて、replyカラムに保存できるようになりましたが、コメントに対して何件の返信があるかを表示するには
コントローラーでこんな記述をする必要があります。

```videos_controller.rb

class VideosController < ApplicationController
  (省略)

  def show
    (省略)
    @new_comment = Comment.new
    @comments = @video.comments.includes(:user)
    # Commentテーブルのreplyカラムの数をカウント
    @replies = Comment.group(:reply).count
  end
end
```

試しにビューファイルで下記のように記載してみるとこのように表示されます。

```show.html.haml
= @replies
```

左側がreplyカラムの中身、右側がその合計数になります。
今回の例でいくとコメントが2件あり、コメント1に対する返信が3件、コメント2に対する返信が1件あるといった状況です。

```
{nil=>2, 1=>3, 2=>1}
```

この@repliesは配列の形になっているので下記のように書いてあげると合計数だけを取り出すことができます。

```show.html.haml
= @replies[1]
```

```
3
```

### これを用いて返信数を実装していきます！


```show.html.haml

(省略)
.content__show__comment__bottom__view-btn
  -# 返信がなければ
  - if @replies[comment.id] == nil
    = ""
  -# 返信が1件なら
  - elsif @replies[comment.id] == 1
    = "View #{ @replies[comment.id] } reply"
  -# 返信が2件以上なら
  - else
    = "View #{ @replies[comment.id] } replies"
```


## 4. 返信数をクリックして返信を閲覧表示

### 最後に返信をそれぞれのコメントの下に表示できるようにしよう！

ここで重要となるのはコメントをeach doで繰り返し表示している中で
更にreplyを繰り返し表示させるということ。
こうすることでそれぞれのコメントの下に返信が紐づいているように見えます。

```show.html.haml
-# 前の実装
- @comments.each do |comment|
  - if comment.reply.blank?
  (省略)
  -# ここから実装
  -# replyという変数を用意して、再度繰り返し表示
  - @comments.each do |reply|
    -# (重要)replyカラムがcomment.idと同じ場合のみ表示する
    - if reply.reply == comment.id
      .content__show__comment__bottom__view-reply
        .content__show__comment__bottom__view-reply__top
          .content__show__comment__bottom__view-reply__top__user
            .content__show__comment__bottom__view-reply__top__user__icon
              = link_to "/users/#{reply.user.id}" do
                - if reply.user.image.present?
                  = image_tag reply.user.image.url, width: '100%'
                - else
                  = image_tag('/images/default_user.jpg', width: '100%')
            .content__show__comment__bottom__view-reply__top__user__name
              = link_to "/users/#{reply.user.id}" do
                = reply.user.name
            .content__show__comment__bottom__view-reply__top__icons
              - if user_signed_in? && current_user.id == reply.user.id
                .content__show__comment__bottom__view-reply__top__icons__delete
                  = link_to video_comment_path(reply.video_id, reply.id), method: :delete do
                    = icon('fa', 'trash-alt')
              - else
                = ''
        .content__show__comment__bottom__view-reply__text
          // 改行を含んだテキストも表示できるようにsimple_formatを使用
          = simple_format(reply.text)

```

### Jqueryで動きを加えて完了です！

```video-comment.js

// 返信の表示・非表示切り替え
$(function() {
  $(".content__show__comment__bottom__view-reply").hide();
  $(".content__show__comment__bottom__view-btn").on("click", function() {
    // toggleにすると同じボタンを押したときに表示・非表示を切り替えてくれる
    $(".content__show__comment__bottom__view-reply").toggle();
  });
});
```

### コード量は少し長いですが、ポイントを掴めれば実装できると思うので是非真似してみてください！


# 今後の追加実装予定

- 返信を非表示にするときは「View」ではなく「Hide」にする
- メンションをつけられるようにする
- コメント・返信した際に通知としてストックされるようにする


# 参考
https://qiita.com/rockguitar67/items/4fd28cba0c243a8d0ba5
https://pikawaka.com/rails/count