6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

前提条件

・Rubyバージョン2.6.5
・Railsバージョン6.0.0
・jQueryを導入済み→未導入の方は、自分が書いたこちらの記事をご参照ください。
・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で行われたものなの?」と分からなくなってしまうからです。


続いて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行は不要かもしれません。
 参考にした記事で記述されていたので残しています。
format.jsが、今回の非同期処理で返すビューです。
 通信に成功した場合、create.js.erbというファイルが呼び出されます。
 通信に失敗した場合、error.js.erbというファイルが呼び出されます。
 このあと、どちらのファイルも新しく作ります。

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

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("")と記述しても、ここでは同じ役割を果たせます。

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

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

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

つまり「一度空送信してエラー文が表示された後、ちゃんと入力して送信してもエラー文が表示されたままでは困るので、エラー文を空にする」という処理です。


では続けて、エラーメッセージを表示する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が空かどうか判定してください」と伝えるためです。


最後に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で呼び出せるので、
 新規投稿ページ等でも使ってみてください。

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

そもそもjQueryを導入していない!という方は自分が書いたこちらの記事をご参照ください
【Rails】Uncaught ReferenceError: $ is not definedを解消した話

参考にした記事:
【Rails】非同期通信でチャット/DM機能を実装する
【Rails】 remote: trueでフォーム送信をAjax実装する方法とは?

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?