概要
Rails5 Action Cableを使ってリアルタイムなチャットアプリを作るという記事が多いが、常にと言っていいほどメッセージモデルとルームモデルを紐付ける方法が質問されているので、その方法を書く。
Modelの構成
User, Message, Roomモデルがあり、MessageModelはUserModelとRoomModelに紐づいている。
app/models/user.rb
class User < ActiveRecord::Base
has_many :messages
end
app/models/room.rb
class Room < ActiveRecord::Base
has_many :messages
end
app/models/message.rb
class Message < ActiveRecord::Base
# メッセージの保存に成功したらBroadCastJobに投げる
after_create_commit { BroadCastMessageJob.perform_later self }
# こうしとくと誰が送ったか識別しやすい
belongs_to :sent_user, class_name: "User", foreign_key: 'user_id'
belongs_to :room
end
Controller
app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def show
@room = Room.find(params[:id])
@messages = @room.messages
end
end
view
app/views/rooms/show.html.erb
<div id="messages", data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
<div class="input">
<form>
<label>post</label>
<input type="text" data-behavior="room_speaker">
</form>
</div>
app/views/messages/_message.html.erb
<div class="message">
<p><%= "#{message.sent_user.name}: #{message.content}" %></p>
</div>
Room_idを受け取る
要するにあらかじめrooms_controllerからshow.html.erbにroomの情報(room_id)を渡しておいて、それをjs(room.coffee)で受け取り、その情報に基づいてチャンネル(room_channel.rb)の中でメッセージを作る。
app/assets/javascripts/channels/room.coffee
jQuery(document).on 'turbolinks:load', ->
# viewの<div id="messages">から受け取る
messages = $('#messages')
if $('#messages').length > 0
# ここやで
App.room = App.cable.subscriptions.create { channel: "RoomChannel", room_id: messages.data('room_id') },
received: (data) ->
$('#messages').append data['message']
speak: (message) ->
@perform 'speak', message:
message
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13
App.room.speak event.target.value
event.target.value = ''
event.preventDefault()
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
# このparams["room_id"]は上のroom.coffeeでsubscriptionを作る際に渡しているのがポイント。
stream_from "room_channel_#{params['room_id']}"
end
def speak(data)
# ここでRoomとMessageが紐付けられる。
Message.create(content: data['message'], sent_user: current_user, room: Room.find(params['room_id']))
end
end
ジョブに投げる
MessageModelの中のafter_create_commitで、メッセージの保存に成功したらjobに飛ばすように指定してる。
app/jobs/broadcast_message_job.rb
class BroadCastMessageJob < ApplicationJob
def perform(message)
ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render(partial: messages/message, locals: { message: message })
end
end
これで完成。
投稿した内容が保存され、かつリアルタイムで画面に表示されれば完成。
結論
本当にビューの中にroom_id渡してJSで受け取ってゴニョゴニョするしかないんですかね、、
とりあえず私が調べた限りはこれくらいしかなさそうでしたが、、