#1最終準備
今回からactioncableを実装していきます。actioncalbeではjqueryを使用し、bootstrapでレイアウトを調整したいので、bootstrapも導入します。
Gemに
gem 'bootstrap', '~> 4.5'
gem 'jquery-rails'
を追加し、bundleします。その後、application.cssをscssに変更し、bootstrapをインポートさせます。
@import "bootstrap";
最後にapplication.jsに読み込ませて完了です。
//= require jquery3
//= require popper
//= require bootstrap-sprockets
#2いざ実装
準備が全て完了したので、実装していきます。
まずは、ルームの詳細画面にてコメントを送信するフォームと、コメントを表示するviewを作ります。
<div id="chats">
<%= render @chats %>
</div>
<textarea type="text" data-behavior="chat_speaker" class="from-control"></textarea>
<button class="chat_submit btn btn-info" >コメントを送信</button>
これでコメントが送信できるので、次にコメントを表示できる様にしていきます。
<%= simple_format(chat.message) %>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
これでコメントと投稿日時が表示される様になります。
次に、チャネルを作成します。
rails g channel room speak
これで、各種ファイルが生成されるので、App.room.speak
とコンソールで入力し、trueと帰って来れば成功です。
今回はルーム事でのチャットの表示を行いたいので、ルームIDを付与して判別させていきます。
<div id="messages" data-room_id="<%= @room.id %>">
これでルームIDを判断できる様になり、次にcoffeeで以下の記載をします
document.addEventListener 'turbolinks:load', ->
App.room = App.cable.subscriptions.create {"RoomChannel", room_id: room_id: $('#messages').data('room_id')},
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) ->
# Called when there's incoming data on the websocket for this channel
speak: (message) ->
@perform 'speak'
最後にルームチャネルの方でサブスクライブを判別してあげれば、ルームのチャットの準備が完了です。
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params('room_id')}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
end
end
次は、実際にチャットをリアルタイムで反映させていきます。
room_coffeにチャットの内容を送り、その後room_channelにてDBに保存させます。
(省略)
received: (data) ->
# Called when there's incoming data on the websocket for this channel
$('#chats').append data['chat']
speak: (message) ->
@perform 'speak', message: message
$(document).on 'click', '.chat_submit', (event) ->
App.room.speak $('[data-behavior~=chat_speaker]').val()
$('[data-behavior~=chat_speaker]').val('')
event.preventDefault()
これで、meesegeに入力したチャット内容が入るので、rubyの方で保存します。
そして、ブロードキャストでまたコーヒースクリプトの方にデータを渡して、実際に画面に反映させます。
(省略)
def speak(data)
chat = Chat.create! message: data['message'], user_id: current_user.id, room_id: params['room_id']
ActionCable.server.broadcast "room_channel_#{params['room_id']}", chat: render_chat(chat)
end
private
def render_chat(chat)
ApplicationController.renderer.render( partial: 'chats/chat', locals: { chat: chat})
end
これで、入力した内容が即時に反映されるはずです。
出来ました!
#3削除機能を実装
チャット機能自体は完成したので、次に削除機能を作成していきます。
チャットのview画面に削除ボタンを追加します。
<div class="chat" id="<%= chat.id %>"> #IDを判断してチャットを消すのでIDの追加
<%= simple_format(chat.message) %>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
<button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button> #追加
</div>
ボタンが追加出来たので、coffeeの方でデーターを取得します。
destroy: (id) -> #新しく、destroyメソッドを追加
@perform 'destroy', id: id
speak: (message) ->
@perform 'speak', message: message
$(document).on 'click', '.chat_submit', (event) ->
App.room.speak $('[data-behavior~=chat_speaker]').val()
$('[data-behavior~=chat_speaker]').val('')
event.preventDefault()
$(document).on 'click', '.delete-btn', (event) ->
App.chat.destroy event.target.id #削除ボタンのイベント発火追加
これでidが取得出来たのでroom_channelの方で削除します。
(省略)
def destroy(data) #追加
Chat.find_by(id: data['id']).destroy
ActionCable.server.broadcast "room_channel_#{params['room_id']}", id: data['id']
end
最後にリアルタイムに削除する為にreceivedの方に処理を加えて完了です。
received: (data) ->
if data['id'] #取得したデータがIDだったらという条件分岐で判断
id = ('#') + data['id']
$(id).remove()
else
$('#chats').append data['chat']
これで削除機能も実装出来ました!
##3.5 LINE風とコメント時のスクロール機能を追加
これで、チャット機能と削除機能は完成ですが、削除機能は当然入力した本人のみがコメントを消せる様にしたいので、renderを用いて投稿したユーザーとそれ以外のユーザーと判別します。
その後、coffeeの方で条件分岐して反映させていきます。最後にスクロール機能とLINE風なcssを当てて完成です。
まずは、renderでコメントユーザとそれ以外で振り分けていきます。
新たにコメントユーザーとして_chatcurrent.html.erb
を作成します
<div class="chat" id="<%= chat.id %>">
<div class="mycomment">
<%= simple_format(chat.message) %><br>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
</div>
<div class="chat_delete">
<button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button>
</div>
</div>
コメントユーザー以外のユーザーのパーシャルhtmlも_chatother.html.erb
として作成します。
<div class="chat" id=<%= chat.id %>>
<div class="fukidasi">
<div class="chatting">
<div class="says">
<%= simple_format(chat.message) %><br>
</div>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
</div>
</div>
</div>
次にブロードキャストした際の、renderも変更します。
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params['room_id']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
chat = Chat.create! message: data['message'], user_id: current_user.id, room_id: params['room_id']
ActionCable.server.broadcast "room_channel_#{params['room_id']}", chat: render_chat(chat), chatother: render_chatother(chat), chat_user: current_user.id #変更
end
def destroy(data)
Chat.find_by(id: data['id']).destroy
ActionCable.server.broadcast "room_channel_#{params['room_id']}", id: data['id']
end
private
def render_chat(chat)
ApplicationController.renderer.render( partial: 'chats/chatcurrent', locals: { chat: chat, current_user: current_user})
#コメントユーザー側のパーシャル
end
def render_chatother(chat)
ApplicationController.renderer.render( partial: 'chats/chatother', locals: { chat: chat, current_user: current_user})
#コメントユーザー以外のパーシャル
end
end
これでruby側での変更は完了したので,coffee側でも変更を加えます。
その後、jqueryのanimate関数でコメントした際の高さを取得し、最新のコメントが出るたびにスクロールさせます。
received: (data) ->
if data['id']
id = ('#') + data['id']
$(id).remove()
else
show_user = $('#show_user').data('show_user')
if data['chat_user'] == show_user #ここでコメントユーザーを判断します
$('#chats').append data['chat']
$('.chat_box').animate scrollTop: $('.chat_box')[0].scrollHeight #これでコメントした際に一番下にスクロールします
else
$('#chats').append data['chatother']
$('.chat_box').animate scrollTop: $('.chat_box')[0].scrollHeight
ここで出てきた、show_userをview の方に追記します。
<div id="messages" data-room_id="<%= @room.id %>"></div>
<div id="show_user" data-show_user="<%= current_user.id %>"></div> #追加
<div class="chat_box"> #ここでスクロール用の高さを取得します
<div id="chats">
<%= render @chats %>
</div>
</div>
<textarea type="text" data-behavior="chat_speaker" class="from-control"></textarea>
<button class="chat_submit btn btn-info" >コメントを送信</button>
最後に、_chat.html.erbにcurrent_userとそうでない場合の処理を加えて完了です。
<div class="chat" id="<%= chat.id %>">
<% if chat.user == current_user %>
<div class="mycomment">
<%= simple_format(chat.message) %><br>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
</div>
<div class="chat_delete">
<button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button>
</div>
<% else %>
<div class="fukidasi">
<div class="chatting">
<div class="says">
<%= simple_format(chat.message) %><br>
</div>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
</div>
</div>
<% end %>
</div>
最後に、cssを当てて完成です。
.chat_box{
background-color: #fff;
width: 50%;
height: 500px;
padding: 10px;
margin-top: 10px;
overflow: scroll;
border: blue 1px solid;
text-align: right;
font-size: 14px;
background: #7da4cd;
}
#outer_box {
position: relative;
height: 300px;
}
.mycomment {
margin: 10px 0;
}
.mycomment p {
display: inline-block;
position: relative;
margin: 0 10px 0 0;
padding: 8px;
max-width: 250px;
border-radius: 12px;
background: #30e852;
font-size: 15px;
}
.mycomment p:after {
content: "";
position: absolute;
top: 3px;
right: -19px;
border: 8px solid transparent;
border-left: 18px solid #30e852;
-webkit-transform: rotate(-35deg);
transform: rotate(-35deg);
}
.fukidasi {
width: 100%;
margin: 10px 0;
overflow: hidden;
}
.user_image-attachment {
border-radius: 50%;
}
.fukidasi .faceicon {
float: left;
margin-right: -50px;
width: 40px;
}
.fukidasi .chatting {
width: 100%;
text-align: left;
}
.says {
display: inline-block;
position: relative;
margin: 0 10px 10px 50px;
padding: 10px;
max-width: 250px;
border-radius: 12px;
background: #edf1ee;
}
.says:after {
content: "";
display: inline-block;
position: absolute;
top: 3px;
left: -19px;
border: 8px solid transparent;
border-right: 18px solid #edf1ee;
-webkit-transform: rotate(35deg);
transform: rotate(35deg);
}
.says p {
margin: 0;
padding: 0;
}
#4終わりに
以上で終了です。非常に長くなってしまいました。
所々雑な部分がありますが、ひとまずは自分用に復習として記入できたと思います。
又、turbolinksを使用していると、意図しない挙動をする場合があるのでその際はturbolinksを外すことをお勧めします。
#5参考
非常に参考にしたサイト
https://hajimeteblog.com/rails-actioncable/