Search code examples
ruby-on-railsruby-on-rails-5actioncableruby-on-rails-5.2

Update presence table with Action Cable


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>

Question

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.


Solution

  • 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