Skip to content →

Subscribing to many channels in ActionCable

There are many times when logically it makes sense for a page to receive updates about many different items. Sometimes those items are similar, so many ActionCable channels can each listen for one item, or a single channel listen for many different items. Here is an example of using one ActionCable channel to subscribe to many different streams, in a hypothetical chat app.

This will reference the Grabbing ActionCable with Stimulus.js tutorial.

Structuring Our Channel

A Stimulus.js controller will manage the ActionCable connection. It will use data attributes to find the chat rooms that will become individual streams over the connection.

The Stimulus.js controller first setups the ActionCable channel, and sends the follow message to the ActionCable server. This dovetails nicely with Turbolinks, such that every time we visit the page, via a refresh or from another page, our connections are always setup properly. When the controller is destroyed due to a page change, it sends the unfollow command to the ActionCable server, stopping all streams over this particular connection.

The HTML

Let’s assume there are a number of chat rooms, perhaps on a sidebar, that will display the number of new messages in them. Each chat room will be in a <li> tag, and has a data attribute like data-chat-room-id.

<ul data-controller="sidebar">
    <li data-chat-room-id="1">
        Project Alpha <span>3 messages</span>
    </li>
    <li data-chat-room-id="3">
        Project Delta <span>1 message</span>
    </li>
    <li data-chat-room-id="24">
        Project Omega <span>12 messages</span>
    </li>
</ul>

The Controller

The sidebar_controller.js will manage the ActionCable channel, and send the follow and unfollow messages. (createChannel comes from here)

The controller will call the listen() twice when the channel hasn’t been set up, and once when the channel exists. Calling listen() is okay, since there is a race between the WebSocket setting up the connection to the server, and Stimulus connecting the controller to the page’s HTML. The first listen() does nothing, as there is no ActionCable connection for sending the follow command. The second listen() is called from the channel’s connected() method, and since the connection is setup, follow goes back to the server.

The listen() method finds all the chat rooms that we want to listen for from our sidebar, and tells the ActionCable server to stream from all these chat rooms.

import { Controller } from "stimulus"
import createChannel from "cables/cable";

export default class extends Controller {
  initialize() {
    let thisController = this;
    this.channel = createChannel( "ChatRoomSidebarChannel", {
      connected() {
        thisController.listen()
      },
      received({ chat_room_details, chat_room_id }) {
        let existingItem = document.querySelector(`[data-chat-room-id='${ chat_room_id }']`)
        if (existingItem) {
          let html = new DOMParser().parseFromString( chat_room_details , 'text/html');
          const itemHTML = html.body.firstChild;
          existingItem.parentNode.replaceChild(itemHTML, existingItem);
        }
      }
    });
  }

  connect() {
    this.listen()
  }

  disconnect() {
    if (this.channel) {
      this.channel.perform('unfollow')
    }
  }

  listen() {
    if (this.channel) {
      let chatRooms = []
      for (const value of document.querySelectorAll(`[data-chat-room-id]`)) {
        chatRooms.push( value.getAttribute('data-chat-room-id') )
      }
      this.channel.perform('follow', { chatRooms: chatRooms } )
    }
  }
}

The Channel

The channel is not very complicated. It listens for the follow command, and follows each of the chat rooms individually. The unfollow command stops all the streams over the subscribing socket.

class ChatRoomSidebarChannel < ApplicationCable::Channel
  def follow(data)
    stop_all_streams
    chatRooms = data['chatRooms']
    unless chatRooms.nil?
      chatRooms.each do |chatRoom|
        stream_from "ChatRoomSidebarChannel:#{chatRoom}"
      end
    end
  end

  def unfollow
    stop_all_streams
  end
end

Changing the sidebar

Elsewhere in the project, when the sidebar needs to be updated, push the message out to every connection subscribed to that sidebar channel. This assumes there is a partial that renders the <li> element that will be pushed over the wire, and replaced by the Stimulus controller.

ActionCable.server.broadcast "ChatRoomSidebarChannel:#{chatRoom.id}", {
        chat_room_details: ChatRoomsController.render( partial: 'sidebar', locals: { chatRoom: chatRoom }  ).squish,
        chat_room_id: item.id
      }

What else?

This is certainly a skeleton for how an app might structure a listening to changes of a number of similar items on a webpage. This also doesn’t take into account that one chat room subscriber has a different number of unread messages from any other subscriber. But this lays out how to integrate ActionCable easily into a project, all the service of immersive and interactive web apps.

Comments or Questions? Find me on twitter @jpbeatty

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.


Published in ruby on rails Stimulus JS

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *