ユーザー同士1対1のチャット機能を非同期通信(Ajax)で実装する方法をまとめています!
お気づきの点などあれば、コメント頂戴できれば幸いです。
#前提
- deviseでUserモデルを作成している
- 各ユーザーの詳細画面(show)が作成されている(ユーザーの詳細画面にチャットへのリンクを作ります)
👆以上の前提で進めていきます
###使用するモデル
- User = ユーザーの定義
- Chat = チャットの定義
- Room = チャットルームの定義
- UserRoom = ユーザーとチャットルームの関連付けの定義(中間テーブル)
###大まかな流れ
- モデル生成とアソシエーション
- コントローラー作成 & ルーティング設定
-
chats_controller
に記述 - チャットのビューを記述
-
create.js.erb
ファイルを作成&記述
#STEP1: モデル生成とアソシエーション
まずUserモデル以外に必要なモデルを作成していきましょう!
$ rails g model Room
$ rails g model Chat user_id:integer room_id:integer message:text
$ rails g model UserRoom user_id:integer room_id:integer
$ rails db:migrate
モデルが生成できたら、アソシエーションを設定します。
(それぞれ前後の記述は省略しています)
has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
belongs_to :user
belongs_to :room
has_many :chats
has_many :user_rooms #1つのルームにいるユーザ数は2人のためhas_manyになる
belongs_to :user
belongs_to :room
👆user_rooms
テーブルはusers
テーブルとrooms
テーブルの中間テーブルです。
外部キーとしてuser_id
とroom_id
を持っています。
user_rooms
テーブルは、ユーザーとルームの紐づけを行います:
- ユーザーにどのroom_idが紐づいているか
- ルームにどのuser_idが紐づいているか
例えば、AさんとBさんのチャットの場合、
- Aさん(user_id=3)がroom_id=1
- Bさん(user_id=4)がroom_id=1
であれば、AさんとBさんは共通のroom_idを持ちます。
#STEP2: コントローラー作成 & ルーティング設定
では、chats_controller
とshow
アクションを一緒に作成します👇
$ rails g controller chats show
今回は、chats_controller
のshow
アクションでチャットを行うので、以下のようにルーティングを設定しておきましょう。
ちなみにcreate
アクションは後でコントローラに定義するように、コメントの投稿に必要になってきます。
get 'chat/:id', to: 'chats#show', as: 'chat'
resources :chats, only: [:create]
#STEP3: chats_controller
に記述
まずはコメントなしの記述内容がこちら👇
class ChatsController < ApplicationController
def show
@user = User.find(params[:id])
rooms = current_user.user_rooms.pluck(:room_id)
user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)
if user_rooms.nil?
@room = Room.new
@room.save
UserRoom.create(user_id: @user.id, room_id: @room.id)
UserRoom.create(user_id: current_user.id, room_id: @room.id)
else
@room = user_rooms.room
end
@chats = @room.chats
@chat = Chat.new(room_id: @room.id)
end
def create
@chat = current_user.chats.new(chat_params)
@chat.save
end
private
def chat_params
params.require(:chat).permit(:message, :room_id)
end
end
それぞれ何をしているかコメントを入れたものがこちらです👇
※ちなみに、どちらのユーザーに関する記述なのかがわかるように「Aさん」「Bさん」がコメントに出てきますが、「AさんがBさんに対してチャットする」想定です。つまり、Aさんがcurrent_user
になります。
class ChatsController < ApplicationController
def show
#BさんのUser情報を取得
@user = User.find(params[:id])
#user_roomsテーブルのuser_idがAさんのレコードのroom_idを配列で取得
rooms = current_user.user_rooms.pluck(:room_id)
#user_idがBさん(@user)で、room_idがAさんの属するroom_id(配列)となるuser_roomsテーブルのレコードを取得して、user_room変数に格納
#これによって、AさんとBさんに共通のroom_idが存在していれば、その共通のroom_idとBさんのuser_idがuser_room変数に格納される(1レコード)。存在しなければ、nilになる。
user_room = UserRoom.find_by(user_id: @user.id, room_id: rooms)
#user_roomでルームを取得できなかった(AさんとBさんのチャットがまだ存在しない)場合の処理
room = nil
if user_room.nil?
#roomのidを採番
room = Room.new
room.save
#採番したroomのidを使って、user_roomのレコードを2人分(Aさん用、Bさん用)作る(=AさんとBさんに共通のroom_idを作る)
#Bさんの@user.idをuser_idとして、room.idをroom_idとして、UserRoomモデルのがラムに保存(1レコード)
UserRoom.create(user_id: @user.id, room_id: room.id)
#Aさんのcurrent_user.idをuser_idとして、room.idをroom_idとして、UserRoomモデルのカラムに保存(1レコード)
UserRoom.create(user_id: current_user.id, room_id: room.id)
else
#user_roomに紐づくroomsテーブルのレコードをroomに格納
room = user_room.room
end
#roomに紐づくchatsテーブルのレコードを@chatsに格納
@chats = room.chats
#form_withでチャットを送信する際に必要な空のインスタンス
#ここでroom.idを@chatに代入しておかないと、form_withで記述するroom_idに値が渡らない
@chat = Chat.new(room_id: room.id)
end
def create
@chat = current_user.chats.new(chat_params)
@chat.save
end
private
def chat_params
params.require(:chat).permit(:message, :room_id)
end
end
#STEP4: チャットのビューを記述
チャット画面を記述していきましょう👇
なおform_with
は非同期通信処理のためremote: true
にしています(省略可)。
以下の記述でチャット画面はこんな感じになります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/0bc2b1bc-14cf-1567-ddd5-c85caa7da162.png)※ビューの記述は練習用ということで最低限にしてあります。
スタイリッシュな感じにぜひ仕上げてください。
<div class="container">
<div class="row">
<div class="col-xs-6">
<h2>CHAT WITH <%= @user.name %></h2>
<table class="message table">
<thead>
<tr>
<th style="text-align: left; font-size: 20px;"><%= current_user.name %></th>
<th style="text-align: right; font-size: 20px;"><%= @user.name %></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, remote: true do |f| %>
<%= f.text_field :message %>
<%= f.hidden_field :room_id %>
<%= f.submit "SEND", class:"btn btn-sm btn-success chat-btn" %>
<% end %>
</div>
</div>
</div>
さて、ユーザーの詳細画面(show)から、そのユーザーとチャットができるように、ビューに記述を加えましょう👇
<!-- current_userと@userが一致していない場合に`Begin Chat`を表示 -->
<% if current_user != @user %>
<%= link_to 'Begin Chat', chat_path(@user.id) %>
<% end %>
もしusers_controller
のshow
アクションに、@user
が定義されていない場合は、忘れず記述してください👇
def show
@user = User.find(params[:id])
end
#STEP5. create.js.erb
ファイルを作成&記述
最後に、非同期通信の設定をしていきましょう。
といってもすでにform_withにremote: true
を記述したので、あとはcreate.js.erb
ファイルを作成&記述するだけです。
手動で構わないので、views/chats
フォルダ直下にcreate.js.erb
ファイルを作成して、以下を記述しましょう。
/*.messageを部分更新。appendメソッドで要素を追加 */
$('.message').append("<p style='text-align: left;'><%= @chat.message %></p>");
/*チャット投稿後、フォームの値に前回の投稿内容が残らないようにする */
$('input[type=text]').val("")
これで完成です!おつかれさまでした。
#参考資料