Let's build a complete chat room feature with multiple rooms and real-time messages.

The Chat Channel

ruby
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    @room = Room.find(params[:room_id])

    # Authorization check
    if @room.accessible_by?(current_user)
      stream_for @room
    else
      reject
    end
  end

  def unsubscribed
    # Notify others user left (optional)
    broadcast_user_left if @room
  end

  # Receive messages from client
  def speak(data)
    message = @room.messages.create!(
      user: current_user,
      body: data["message"]
    )

    # Broadcast to all subscribers
    ChatChannel.broadcast_to(@room, {
      type: "message",
      message: render_message(message)
    })
  end

  # Client can call this action
  def typing
    ChatChannel.broadcast_to(@room, {
      type: "typing",
      user: current_user.name
    })
  end

  private

  def render_message(message)
    ApplicationController.renderer.render(
      partial: "messages/message",
      locals: { message: message }
    )
  end

  def broadcast_user_left
    ChatChannel.broadcast_to(@room, {
      type: "user_left",
      user: current_user.name
    })
  end
end

Client-Side JavaScript

javascript
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"

const chatChannel = {
  subscribe(roomId) {
    return consumer.subscriptions.create(
      { channel: "ChatChannel", room_id: roomId },
      {
        connected() {
          console.log(`Connected to room ${roomId}`)
        },

        disconnected() {
          console.log("Disconnected from chat")
        },

        received(data) {
          switch(data.type) {
            case "message":
              this.appendMessage(data.message)
              break
            case "typing":
              this.showTypingIndicator(data.user)
              break
            case "user_left":
              this.showUserLeft(data.user)
              break
          }
        },

        // Send a message
        speak(message) {
          this.perform("speak", { message: message })
        },

        // Notify typing
        typing() {
          this.perform("typing")
        },

        appendMessage(html) {
          const messages = document.getElementById("messages")
          messages.insertAdjacentHTML("beforeend", html)
          messages.scrollTop = messages.scrollHeight
        },

        showTypingIndicator(user) {
          const indicator = document.getElementById("typing-indicator")
          indicator.textContent = `${user} is typing...`
          clearTimeout(this.typingTimeout)
          this.typingTimeout = setTimeout(() => {
            indicator.textContent = ""
          }, 2000)
        },

        showUserLeft(user) {
          // Show system message
        }
      }
    )
  }
}

// Usage
const roomId = document.getElementById("chat-room").dataset.roomId
const subscription = chatChannel.subscribe(roomId)

// Send message on form submit
document.getElementById("message-form").addEventListener("submit", (e) => {
  e.preventDefault()
  const input = document.getElementById("message-input")
  subscription.speak(input.value)
  input.value = ""
})

// Typing indicator
document.getElementById("message-input").addEventListener("keypress", () => {
  subscription.typing()
})

Message Partial

erb
<!-- app/views/messages/_message.html.erb -->
<div class="message" id="message_<%= message.id %>">
  <div class="message-header">
    <strong><%= message.user.name %></strong>
    <time><%= message.created_at.strftime("%H:%M") %></time>
  </div>
  <div class="message-body">
    <%= message.body %>
  </div>
</div>
✓ Completed