Real time Rails (Introduction to ActionCable) Rails 5

Rails 5 introduced ActionCable which is a built-in module that integrates WebSockets with the rest of your application and sets up both the server and the client side in a rails-magic-friendly way which means that you don’t actually need to know much about WebSockets or how they work in the background.

The WebSocket Protocol

Since this post is Rails ActionCable focused I’ll just take a minute of your time to briefly explain the WebSocket Protocol and how they work. WebSockets provide a persistent connection between a client and server that both parties can use to start sending data at any time. So instead of having a one way communication between server and client, the client requests and initiates a full-duplex communication with the server:

An Upgrade header is added when the client first makes the request and informs the server that the client wishes to establish a WebSocket connection.

The client request looks something like this:

GET /chatroom
Origin: http://michelada.io
Connection: Upgrade
Host: websocket.server.com
Upgrade: websocket

Then if the server supports a WebSocket connection the response will look something like this:

HTTP/1.1 101 WebSocket Protocol Handshake
Date: Wed, 16 Oct 2013 10:07:34 GMT
Connection: Upgrade
Upgrade: WebSocket

The handshake is now complete and the initial HTTP connection will be replaced with a WebSocket connection and both server and client can start sending data.

Check out this post to learn more about websockets.

An introduction to WebSockets

Back to Rails now

To start setting up ActionCable we’ll need to create a route for our WebSocket communication to take place.

# routes.rb
mount ActionCable.server, at: '/cable'

Now communication via that route will be available. Our next step will be to create a channel. You can think of channels as controllers; they are classes that encapsulate a logical unit of work, they accept connections and trigger an action. Alright so let’s go ahead and create one:

bin/rails generate channel chatroom

The command above will create the following files:

create  app/channels/chatroom_channel.rb
identical  app/assets/javascripts/cable.js
create  app/assets/javascripts/channels/chatroom.coffee

Users subscribe to channels, acting as subscribers. Their connection is called a subscription. We will route our messages to our chatroom_channel and the subscribed method will be “streaming” them.

Now in our chatroom coffeescript file we have three functions which are:

  • connected Called when the subscription is ready for use in the server.
  • disconnected Called when the subscription has been terminated by the server.
  • received Called when there’s incoming data on the websocket for this channel.

Our next step will be to subscribe to our channel, we can do this by uncommenting the subscribed method:

#chatroom_channel.rb
def subscribed
  stream_from "chatroom_channel"
end

Now if we start our rails server you should notice your connection being upgraded to websocket.

Started GET "/cable" for 127.0.0.1 at 2019-02-26 12:38:28 -0600
Started GET "/cable/" [WebSocket] for 127.0.0.1 at 2019-02-26 12:38:28 -0600
Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
ChatroomChannel is transmitting the subscription confirmation
ChatroomChannel is streaming from chatroom_channel

Having our connection up and running we can start streaming our data. In my app I have a MessagesController and a ChatroomController , the first one is in charge of creating messages while chatroom is where our messages will be displayed.

# messages_controller.rb
def create
  message = current_user.messages.build(message_params)
  if message.save
    redirect_to root_path
  end
end

Now if we want to broadcast messages we would need to add the ActionCable method and add our channel’s name inside the quotes.

# messages_controller.rb
def create
  message = current_user.messages.build(message_params)
  if message.save
    ActionCable.server.broadcast "chatroom_channel"
  end
end

This method takes a hash and whatever we send inside will be received by received: (data) in our chatroom.coffee file. For example:

# messages_controller.rb
def create
  message = current_user.messages.build(message_params)
  if message.save
    ActionCable.server.broadcast "chatroom_channel",
                                 foo: message.body
  end
end

Our key is foo and the value is our message’s body, this means we now have access to the message’s body in our chatroom.coffee file through data.

# chatroom.coffee
received: (data) ->
  alert data.foo

Great, it’s working! Wait, the messages aren’t displayed yet, how can we achieve this? Well we can use the append method but we would first need to give an id to our messages.

# chatroom/index.html.erb
<div class="ui two column grid">
  <div class="twelve wide column">
    <div class="ui fluid raised card chatbox">
      <div class="content">
        <div class="ui feed" id="message-container">  <-- 'add id'
          <%= render @messages %>
        </div>
      </div>
      <div class="extra content">
        <%= form_for(@message, html: { class: "ui reply form", role:  "form" }, url: message_path) do |f| %>
      <div class="field">
        <div class="ui fluid icon input">
          <%= f.text_field :body, placeholder: "Enter message", autofocus: true %>
          <%= f.button '<i class="inverted orange edit icon chat"></i>'.html_safe  %>
        </div>
      </div>
      <% end %>
    </div>
  </div>
</div>
# messages/_message.html.erb
<div class="event">
  <div class="content">
    <div class="summary">
      <em class="username"><%= message.user.username %>: </em><%= message.body %>
    </div>
  </div>
</div>

Now that we’ve added an id we can append new messages to it.

# chatroom.coffee
$('#message-container').append data.my_message

We have 2 issues right now. First, our appended messages aren’t using the style in our message partial until we reload the page. Second, messages are being submitted through a POST request, what we actually want is to submit them remotely using AJAX. This will make our message submission look seamless, adding remote: true to our form should take care of this.

<div class="extra content">
  <%= form_for(@message, html: { class: "ui reply form", role:  "form" }, url: message_path, remote: true) do |f| %>

Next step will be fixing our message style, you should’ve noticed by now we are using a message partial and instead of rendering from the view we will need to render it in our controller.

# messages_controller.rb
def create
  message = current_user.messages.build(message_params)
  if message.save
    ActionCable.server.broadcast "chatroom_channel",
                                 my_message: message_render(message)
  end
private
def message_params
  params.require(:message).permit(:body)
end
def message_render(message)
  render(partial: 'message', locals: { message: message })
end

We’re basically telling our controller to render the partial named ‘message’ and sending our message object through the locals hash since the partial requires a message object. Our functionality is pretty much complete now, you might notice some details like the input not clearing after sending a message or the page not scrolling down to the most recent message but you should be able to take care of that on your own with some javascript. This would be the final result:

And that’s it, you now have a fully functional chat app!