【Rails】Action Cableでコメント機能を実装しよう！

Action Cableを利用してリアルタイムでコメントできる機能を実装します。
今回もレシピアプリを例に作成していきます。

#####完成イメージ
![コメント.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/7b0c7f26-e35c-41d4-ff38-5c0f46666e29.gif)

##Action Cableとは
>Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。Action Cable が導入されたことで、Rails アプリケーションの効率の良さとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できます。クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークを同時に提供する、フルスタックのフレームワークです。
出典：[Railsガイド](https://railsguides.jp/action_cable_overview.html#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB)

なるほど、わからん！

つまりは、通常のRailsのアプリケーションと同様の記述で、即時更新機能を実装できるフレームワークで、メッセージの保存や送信するためにはRubyのコーディングが必要で、保存したメッセージを即時に表示させるためにはJavaScriptのコーディングが必要になります。

##コメント機能の実装
まずは、通常通りコメント機能を実装していきます。

####コメントモデルの作成
```:ターミナル
rails g model comment
```
今回、コメントをtext型でtextカラムとしreferences型でuserとrecipeに紐付けます


```ruby:db/migrate/2021xxxxxxxxxx_create_comments
class CreateComments < ActiveRecord::Migration[6.0]
  def change
    create_table :comments do |t|
    #ここから追記
      t.text :text,         null: false
      t.references :user,   foreign_key: true
      t.references :recipe, foreign_key: true
    #ここまで追記
      t.timestamps
    end
  end
end
```
```:ターミナル
rails db:migrate
```
####アソシエーション、バリデーションの設定
```ruby:app/models/user.rb
class User < ApplicationRecord
  has_many :recipes, dependent: :destroy 
  has_many :comments, dependent: :destroy #追記
#以下略
```

```ruby:app/models/recipe.rb
class Recipe < ApplicationRecord
#中略 
  has_many :comments, dependent: :destroy #追記
  has_many_attached :images
#以下略
```

```ruby:app/models/comment.rb
class Comment < ApplicationRecord
#ここから追記
  belongs_to :user
  belongs_to :recipe

  validates :text, presence: true
#ここまで追記
end
```
####コントローラーの作成、ルーティングの設定
```:ターミナル
rails g controller comments
```
コメントはレシピに紐づくのでネストさせます。

```ruby:config/routes.rb
Rails.application.routes.draw do
  devise_for :users
  root to: 'home#index'
  resources :recipes do
    resources :comments #追記
    collection do
      get :search
      get :result
    end
  end
end
```
####コントローラーの編集
コントローラーにコメントを保存するための記述をしていきます。

```ruby:app/controllers/recipes_controller.rb
class RecipesController < ApplicationController
#中略
  def show
    @recipe = Recipe.find(params[:id])
    @comment = Comment.new #追記
    @comments = @recipe.comments.order(created_at: :desc) #追記
  end
#以下略
end
```

```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = Comment.new(comment_params)
    if @comment.valid?
      @comment.save
      redirect_to recipe_path(@comment.recipe)
    else
      redirect_to recipe_path(@comment.recipe)
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:text).merge(user_id: current_user.id, recipe_id: params[:recipe_id])
  end
end
```
####ビューファイルの編集
```erb:app/views/recipes/show.html.erb
#中略
<div class="show-comment">
    <div class="recipe-comments text-center">コメント一覧</div>
    <div id="comments">
      <% @comments.each do |comment| %>
        <div class="comment"><%= comment.text %></div>
      <% end %>
    </div>
    <div class="text-center">
      <%= form_with model: [@recipe, @comment],local: true do |f| %>
        <label class="text-secondary">コメントする</label><br>
        <%= f.text_field :text, class: "comment-form" %>
        <%= f.submit '送信', class: "btn btn-primary btn-sm" %>
      <% end %>
    </div>
  </div>
```
これでコメント機能の実装は完了です。
ここまでで一度、挙動を確認してみましょう。
![f5958d56e5f33a539ae0d17e2a32bd8c.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/b65779b9-5445-178f-757e-304a2e367b91.gif)

このままでも機能的には問題ないですが、リロードに若干の時間がかかります。
Action Cableを使って即時更新してみましょう。

##Action Cableの実装
####Channel(チャネル)の作成
**チャネルとは**
>チャネル （Channel） は、論理的な作業単位をカプセル化します。通常のMVC設定でコントローラが果たす役割と似ています。Railsはデフォルトで、チャネル間で共有されるロジックをカプセル化するApplicationCable::Channelという親クラスを作成します。
出典：[Railsガイド](https://railsguides.jp/action_cable_overview.html#%E3%83%81%E3%83%A3%E3%83%8D%E3%83%AB)

全くわからん！！！

ざっくりとした説明になりますが、チャネルはAction Cable専用のファイルでMVCのコントローラーと役割が似ています。

チャネルを作成していきましょう。

```:ターミナル
rails g channel comment
```
以下のようにファイルが生成されます。

```:ターミナル
Running via Spring preloader in process 39620
      invoke  test_unit
      create    test/channels/comment_channel_test.rb
      create  app/channels/comment_channel.rb
   identical  app/javascript/channels/index.js
   identical  app/javascript/channels/consumer.js
      create  app/javascript/channels/comment_channel.js
```
ここで生成されたcomment_channel.rbがクライアントとサーバーを結びつけるためのファイルで、comment_channel.jsがサーバーから送られてきたデータをクライアントの画面に描画するためのファイルになります。

####comment_channel.rbの編集
comment_channel.rbを以下のように編集します。

```ruby:app/channels/comment_channel.rb
class CommentChannel < ApplicationCable::Channel
  def subscribed
    stream_from "comment_channel" #編集
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
```
####コントローラーの編集
redirect_toの記述を変更します。

```ruby:app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  def create
    @comment = Comment.new(comment_params)
    if @comment.valid?
      @comment.save
      ActionCable.server.broadcast "comment_channel", comment: @comment #変更
    else
      redirect_to recipe_path(@comment.recipe)
    end
  end

#以下略
```
broadcastはサーバーから送られてくるデータの経路を指し、comment_channel.rbのstream_fromメソッドと関連づいています。

＠commentはcommentに格納されbroadcastを通じcomment_channel.rbのcomment_channelへ送られます。

そして、送られた情報は、comment_channel.jsで受け取ります。

####comment_channel.jsの編集
受け取った情報は、receivedの引数dataに渡されます。
この受け取ったdataを利用してコメントを描画します。

まず、dataをテンプレートリテラルにして定数htmlに格納します。
[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)でコメントを表示させる場所のid "comments"を取得し定数commentsに格納します。
次に[getElementById](https://developer.mozilla.org/ja/docs/Web/API/Document/getElementById)でコメント入力フォームのidを取得し、定数newCommentに格納します。
そして、[insertAdjacentHTML](https://developer.mozilla.org/ja/docs/Web/API/Element/insertAdjacentHTML)で定数htmlを定数commentsに挿入します。
最後に入力フォームに入力された値をvalue = ''で削除します。

```javascript:app/javascript/channels/comment_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("CommentChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
//ここから追記
    const html = `<p>${data.comment.text}</p>`;
    const comments = document.getElementById('comments');
    const newComment = document.getElementById('comment_text');
    comments.insertAdjacentHTML('afterbegin', html);
    newComment.value = '';
//ここまで追記
  }
});
```

以上で完成になります。

実際にコメントが投稿できるか確認してみましょう。
![コメント.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1140333/7b0c7f26-e35c-41d4-ff38-5c0f46666e29.gif)