【Rails】非同期によるDMチャット機能の実装【細かく噛み砕いて解説】

# 前提条件
・Rubyバージョン2.6.5
・Railsバージョン6.0.0
・jQueryを導入済み→未導入の方は、自分が書いた[こちらの記事](https://qiita.com/RIN_HM/items/bdbd76f5015b3c15bfe9)をご参照ください。
・gem 'devise'によるユーザー管理機能を作成済み。

・チャットページへの遷移元となるユーザー詳細ページを作成済み。

```app/views/users/show.html.erb
<% if current_user.id != @user.id %>
  <%= link_to "#{@user.name}さんとチャットする", chat_path(@user.id) %>
<% end %>
```

# この記事の構成
①チャットに必要なモデルの作成
②各モデルへのアソシエーション及びバリデーション設定
③ルーティング設定
④cahtsコントローラーの作成と記述
⑤チャットをするビューの作成
⑥非同期処理に必要な`~.js.erb`ファイルの作成
⑦エラーメッセージを表示するファイルの作成

# ①チャットに必要なモデルの作成
チャットに必要なモデルは次の4つです。
・Userモデル：会話をするユーザー同士を管理。
・Roomモデル：ユーザーが会話するチャットルーム番号を管理。
・UserRoomモデル：UserとRoomの多対多の関係を繋ぐ中間テーブル。
・Chatモデル：チャット内容を管理。

```
% rails g model Room  //Roomモデルに追加するカラムはありません
% rails g model UserRoom user_id:integer room_id:integer
% rails g model Chat user_id:integer room_id:integer message:text
```

**UserとRoomの多対多の関係に関して**
「一人のUserは多くのRoomに所属できる(色んな人とDMでチャットできる)」
「一つのRoomには多くのUserが所属する(今回で言えば、1つのRoomに2人のUserが属してチャットをする)」

この「Room1には、User1とUser2が属する」という関係性を繋ぐために、
中間テーブルとしてUserRoomモデルが必要になります。

**UserRoomsテーブルのイメージ**

|id|user_id|room_id|
|--|--|--|
|1|1|1|
|2|2|1|
↑1つのルームに2人のユーザーが所属している！

# ②各モデルへのアソシエーション及びバリデーション設定

・`through`：「user_rooms」という中間テーブルを使うことを示す。
・`dependent: :destroy`：親のレコードが削除された際、関連する子のレコードも削除する

```app/models/user.rb
has_many :rooms, through: :user_rooms
has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
```

```app/models/room.rb
has_many :users, through: :user_rooms
has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
```

```app/models/user_room.rb
belongs_to :user
belongs_to :room
```

```app/models/chat.rb
belongs_to :user
belongs_to :room

validates :message, presence: true
```
# ③ルーティング設定

```
resources :chats, only: [:show, :create]
```

シンプルなチャット機能に必要なルーティングはこれだけです。(**user関連は作成済みの前提**)
・`chats#show`：チャットをやり取りするビューを返す
・`chats#create`：入力されたメッセージをデータベースに保存する

# ④cahtsコントローラーの作成
chatsコントローラーを作ります。

```
% rails g controller chats
```
まずは、チャットをやり取りする場となる**showアクション**から記述していきます。

```
def show
    @user = User.find(params[:id])  //①チャット相手の特定

    rooms = current_user.user_rooms.pluck(:room_id)  //②自分(current_user)に紐付くRoomを全て取得
    user_room = UserRoom.find_by(user_id: @user.id, room_id: rooms)  //③チャット相手と共通のRoomを持つ中間テーブルがあるか確認

    if user_room.nil?  //⑥共通のチャットルームがない場合
      //⑥-1 新しくチャットルームを作る
      @room = Room.new
      @room.save
      //⑥-2 そのルームidを共通してもつ中間テーブルを、相手と自分の2人分作る
      UserRoom.create(user_id: @user.id, room_id: @room.id)
      UserRoom.create(user_id: current_user.id, room_id: @room.id)
    else
      //④共通のチャットルームがあれば、それに紐付くroomを「@room」に代入する
      @room = user_room.room
    end
　　 //⑤「チャット履歴(@chats)の取得」「新規投稿用の空インスタンス(@chat)作成」
    @chats = @room.chats
    @chat = Chat.new(room_id: @room.id)
  end
```

showアクションでの流れは次の通りです。
①チャット相手を特定する。
②中間テーブルから、自分(current_user)に紐付くRoomのidを全て取得。
③チャット相手と同じRoomのidを持つ中間テーブルがあるか確認。
④共通のRoom.idをもつ中間テーブルがあれば、それに紐付くroomを`@room`に代入する。
⑤「チャット履歴の取得」「空のchatインスタンス作成」を行い、準備完了！
⑥もし共通のRoom.idが中間テーブルになければ(nilならば)、
   1.新しくRoomを作り、
   2.そのRoomのidを共通して持つ中間テーブルを、「チャット相手」「自分」の2人分作成。
   3.同じく⑤を行い、準備完了！

**＜showアクションのポイント＞**
▷`pluck`：引数の値を配列形式で取得できるメソッドです。
　これにより自分と紐付くroomを一旦全部取得し、「チャット相手と共通のroomあるかな〜」
　という照らし合わせを、`UserRoom.find_by〜`で行います。
▷`@room`：2人のチャットルームを表しています。
▷空インスタンス@chatを生成する際、`room_id: @room.id`という風に`room_id`の値を
　指定しています。ここで「2人のチャットルームのidが何番なのか」を渡しておかないと、
　「このメッセージってどのroomで行われたものなの？」と分からなくなってしまうからです。
<br>
続いて**createアクション**を記述していきます。

```
  def create
    @chat = Chat.new(chat_params) //②ストロングパラメーターを引数に@chatを生成

    //③save状況に応じて返すビューを条件分岐する。
    respond_to do |format|
      if @chat.save
        format.html { redirect_to @chat } # HTMLで返す場合、showアクションを実行し詳細ページを表示
        format.js  # create.js.erbが呼び出される
      else
        format.html { render :show } # HTMLで返す場合、show.html.erbを表示
        format.js { render :errors } # 一番最後に実装の解説があります
      end
    end
  end

  private //①入力内容をストロングパラメータで受け取る
  def chat_params
    params.require(:chat).permit(:message, :room_id).merge(user_id: current_user.id)
  end
```

createアクションでの流れは次の通りです。
①送られてきたメッセージを、ストロングパラメータで受け取る。
②ストロングパラメーターを引数に、`@chat`を生成する。
③`@chat.save`の保存状況に応じて、返すビューを条件分岐する。

**＜createアクションのポイント＞**
▷ストロングパラメーターで、chatの`user_id`をmergeしています。
▷`respond_to`は、リクエストされるフォーマット(HTML形式/JS形式)ごとに処理を分ける
　メソッドです。非同期処理であれば`format.html`に関する2行は不要かもしれません。
　[参考にした記事](https://pikawaka.com/rails/remote-true)で記述されていたので残しています。
▷`format.js`が、今回の非同期処理で返すビューです。
　通信に成功した場合、`create.js.erb`というファイルが呼び出されます。
　通信に失敗した場合、`error.js.erb`というファイルが呼び出されます。
　このあと、どちらのファイルも新しく作ります。
<br>

# ⑤チャットをするビューの作成

```app/views/chats/show.html.erb
<div class="container">
    <div class="row">
        <div class="col-xs-6">
            <h2>CHAT WITH <%= @user.nickname %></h2>

            <table class="message table">
              <thead>
                <tr>
                  <th style="text-align: left; font-size: 20px;"><%= current_user.nickname %></th>
                  <th style="text-align: right; font-size: 20px;"><%= @user.nickname %></th>
                </tr>
              </thead>
              <% @chats.each do |chat| %>  //①チャット履歴の表示
                <% if chat.user_id == current_user.id %>
                <tbody>
                  <tr>
                    <th>
                      <p style="text-align: left;"><%= chat.message %></p>
                    </th>
                    <th></th>
                  </tr>
                <% else %>
                  <tr>
                    <th></th>
                    <th>
                      <p style="text-align: right;"><%= chat.message %></p>
                    </th>
                  </tr>
                </tbody>
                <% end %>
              <% end %>
            </table>

            <%= form_with model: @chat, class: "js-form do |f| %>  //②メッセージの新規作成
              <%= f.text_field :message %>
              <%= f.hidden_field :room_id %>
              <%= f.submit "SEND", class:"btn btn-sm btn-success chat-btn" %>
            <% end %>
            <div class="error-alert"></div>  //③エラーメッセージを表示する場所
        </div>
    </div>
</div>
```

chats/showビューでの流れは次の通りです。
コードはごちゃごちゃしていますが、下記3点を把握できれば問題ありません。

①`@chats = @room.chats`で準備しておいた、チャットルームに紐付く全てのメッセージを
　each文で一つずつ取り出して表示する。
②form_withのmodelオプションに空インスタンス`@chat`を指定し、メッセージ入力欄の作成。
③メッセージが空白で送信された際に作動する`バリデーションのエラーメッセージ`を表示する
　スペースだけあらかじめ作っておく。

**＜chats/show.html.erbのポイント＞**
▷`form_with`：form_withはデフォルトで非同期処理を行うようになっています。
　そのため`remote: true`と記述しなくても問題ありません。
▷`<%= f.hidden_field :room_id %>`に関して。
　createアクションで空インスタンス`@chat`を生成する際に、`room_id`を指定していました。
　そのためメッセージを送信するとき、各メッセージに紐付く`room_id`の値もパラメーターへ
　送る必要があります。　

# ⑥非同期処理に必要な`~.js.erb`ファイルの作成

ここまで準備ができたら、いよいよ非同期処理を行うための`js.erb`ファイルを作成していきます。といってもコード自体は数行のシンプルなものです。

まずは次の2つのファイルを作成します。
`app/views/chats/create.js.erb`
`app/views/chats/errors.js.erb`

```app/views/chats/create.js.erb
$('.message').append("<p style='text-align: left;'><%= @chat.message %></p>");
$('.js-form')[0].reset();
$('error-alert').empty();
```
**＜create.js.erbのポイント＞**
上から一つずつ見ていきます。
**①`$('.message').append〜`**
・`('.message')`：チャット一覧を表示しているスペース
　→「`<table class="message table">`」の、「message」部分です。
・`append`：引数に指定した要素を、ある親要素の末尾に追加してくれるメソッドです。
　→今回であれば「append( )」の引数を、親要素「.message」の末尾に追加します。
・`<p style='text-align: left;'>`：レイアウトに関する記述なので無視してください。
・`<%= @chat.message %>`：入力されたメッセージの内容です。

つまり「**入力されたメッセージの内容を、親要素messageの末尾に追加する**」という処理です。

**②$('.js-form')[0].reset();**
・` $('セレクタ名')[0].reset();`で、フォームの内容をリセットできます。
　→今回であれば、form_withのクラス名「`class: "js-form"`」を指定し、
　`reset`で入力フォームの内容をリセットしています。
・`$('input[type=text]').val("")`と記述しても、ここでは同じ役割を果たせます。

※`reset`と`val("")`の違い
・resetメソッド：フォームの値を初期値に戻す
・val("")メソッド：フォームの値を空にする

つまり「**フォーム送信後、メッセージの内容が残りっぱなしだと困るので、フォームの中身をリセットする**」という処理です。

**③$('error-alert').empty();**
・このあと作成する`error.js.erb`でエラー文を表示した際、表示されっぱなしになるのを
　防ぐために、そのエラー文を空にします。

つまり「**一度空送信してエラー文が表示された後、ちゃんと入力して送信してもエラー文が表示されたままでは困るので、エラー文を空にする**」という処理です。
<br>
では続けて、エラーメッセージを表示する`error.js.erb`を編集します。
メッセージが空の状態で送信ボタンが押された際、このファイルが呼ばれます。

```app/views/chats/error.js.erb
$('.error-alert').replaceWith(
  "<%= j(render 'shared/error_messages', model: @chat) %>"
);
```
**＜error.js.erbのポイント＞**
・`('.error-alert')`：ステップ⑤でエラー文を表示するスペースとして作っておいた
　`<div class="error-alert">`のクラス名を指定しています。
・`replaceWith()`：指定したセレクタの要素を、引数の要素で置き換えるメソッドです。
　→今回であれば、`('.error-alert')の要素`を、`render先の要素`で置き換えています。
・`render 'shared/error_messages'`：部分テンプレートを呼び出しています。
　ディレクトリが異なるため(「chats/error.js.erb」と「shared/_error_messages.html.erb」)、
　どのファイルが欲しいのかを**ディレクトリ名から指定**する必要があります。
・`model: @chat`：modelというキーの値に、form_withの`@chat`を指定しています。
　→render先に「この`@chat`が空かどうか判定してください」と伝えるためです。
<br>
最後にrenderの参照先となるエラーメッセージを生成するファイルを作ります。

```app/views/shared/_error_messages.html.erb
<% if model.errors.any? %>
  <div class="error-alert">
    <ul>
      <% model.errors.full_messages.each do |message| %>
        <li class='error-message'><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>
```

**＜_error_messages.html.erbのポイント＞**
・`<% if model.errors.any? %>`：エラーがあれば処理を行う条件分岐です。
　→先ほどrenderのmodelオプションに指定した`@chat`が空かどうか、ここで判断します。
・`<% model.errors.full_messages.each do |message| %>`：
　含まれているエラーをeach文で一つずつ取り出します。今回は`presence: true`しか
　設定していないので、「メッセージを入力してください」という旨のエラー文のみ出ます。

※このエラーメッセージを生成する雛形は他のビューでも同じようにrenderで呼び出せるので、
　新規投稿ページ等でも使ってみてください。
<br>

以上で非同期によるDMチャット機能は完成です！お疲れ様でした！
ご指摘などあれば、ご教授頂けると幸いです。

そもそもjQueryを導入していない！という方は自分が書いたこちらの記事をご参照ください
[【Rails】Uncaught ReferenceError: $ is not definedを解消した話](https://qiita.com/RIN_HM/items/bdbd76f5015b3c15bfe9)

参考にした記事：
[【Rails】非同期通信でチャット/DM機能を実装する](https://qiita.com/mi-1109/items/660ec3630e76f11d1f16)
[【Rails】 remote: trueでフォーム送信をAjax実装する方法とは？](https://pikawaka.com/rails/remote-true)