For a Rails 5.2.2 application with Devise I implemented the presence example of https://guides.rubyonrails.org/action_cable_overview.html#example-1-user-appearances with the following files:
app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
def subscribed
current_user.appear
end
def unsubscribed
current_user.disappear
end
def appear(data)
current_user.appear(on: data['appearing_on'])
end
def away
current_user.away
end
end
app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create "AppearanceChannel",
# Called when the subscription is ready for use on the server.
connected: ->
@install()
@appear()
# Called when the WebSocket connection is closed.
disconnected: ->
@uninstall()
# Called when the subscription is rejected by the server.
rejected: ->
@uninstall()
appear: ->
# Calls `AppearanceChannel#appear(data)` on the server.
@perform("appear", appearing_on: $("main").data("appearing-on"))
away: ->
# Calls `AppearanceChannel#away` on the server.
@perform("away")
buttonSelector = "[data-behavior~=appear_away]"
install: ->
$(document).on "turbolinks:load.appearance", =>
@appear()
$(document).on "click.appearance", buttonSelector, =>
@away()
false
$(buttonSelector).show()
uninstall: ->
$(document).off(".appearance")
$(buttonSelector).hide()
Then I added the following two methods to my users
model to update the is_present
attribute when ever a user is present or not.
app/models/users.rb
[...]
def appear
self.update_attributes(is_present: true)
end
def disappear
self.update_attributes(is_present: false)
end
[...]
On the index page main#index
I display a list of all users with their presence status:
app/controllers/main_controller.rb
[...]
def index
@users = User.order(:last_name)
end
[...]
app/views/main/index.html.erb
<h1>Users</h1>
<%= render partial: "presence_table", locals: {users: @users} %>
app/views/main/_presence_table.html.erb
<div class="presence-table">
<table class="table table-striped">
<thead>
<tr>
<th><%= User.human_attribute_name("last_name") %></th>
<th><%= User.human_attribute_name("is_present") %></th>
</tr>
</thead>
<tbody>
<% users.each do |user| %>
<tr>
<td><%= user.last_name %></td>
<td><%= user.is_present %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
How can I auto update the table with Action Cable when ever a presence of a user gets changed? In my understanding it should be possible with the existing parts but I don't know how to do it.
I don't want the user to have to reload the main#index
page to get an up to date presence list but to push the content of _presence_table.html.erb
when ever a users presence changes.
Looks to me like you are actually just updating the user presence status in your DB when the client connects or sets itself as away.
You also need to broadcast these events to the channel, and other clients need to listen to the events and manipulate the DOM.
On the client side, in your AppearanceChannel
you need to implement the received -> (data)
method. This will be called every time the server sends an event to the subscribers of this channel.
Here code in plain JS:
App.presence = App.cable.subscriptions.create("AppearanceChannel", {
received: (data) => {
// here you have access to the parameters you send server side, e.g. event and user_id
let presenceEl = document.getElementById(data.user_id).querySelectorAll("td")[1]
if (data.event == "disappear") {
presenceEl.innerHtml = "false"
} else {
presenceEl.innerHtml = "true"
}
}
})
And on the server in app/models/users.rb
def appear
self.update_attributes(is_present: true)
ActionCable.server.broadcast("appearance_channel", event: "appear", user_id: id)
end
def disappear
self.update_attributes(is_present: false)
ActionCable.server.broadcast("appearance_channel", event: "disappear", user_id: id)
end