Cable Ready – use websockets in Rails app without custom JavaScript

Introduction

Cable Ready is a great addition for Action Cable, especially if you don’t like to write JavaScript code to interact with your website’s DOM in a real-time. It helps us to quickly write real-time applications that work out of the box.

To demonstrate the features that the gem provides, we will build a simple chat where new messages will appear on the website without reloading. We will start with creating a brand-new Rails application, creating a little code, adding the Cable Ready gem, and making things real-time.

Demo

At the end of the article, we will have fully working simple chat.

Creating example application

Our application will consist of only one model: Message. The model will have the following columns: username and body. The main purpose of the application is to let guests to simply write messages on the chat.

Application sekeleton

We will use Ruby 2.7.0 and Rails in version 6.0.3.2 . Since we don’t need anything fancy on the database side, we will use SQLite as a database engine.

rails new chat
cd chat

Model

As mentioned before, we would need one model – Message where we will save the guest username along with the message that will appear in the chat. Let’s create it:

rails g model Message username:string body:text
rails db:setup
rails db:migrate

Controller

We will need a controller to display messages and save new messages. Let’s create one and save the following code into app/controllers/messages_controller.rb:

class MessagesController < ApplicationController
  def index
    @message = Message.new
    @messages = Message.order('created_at DESC')
  end

  def create
    Message.create!(message_params)
    
    redirect_to :messages
  end

  private

  def message_params
    params.require(:message).permit(:username, :body)
  end
end

update routes in config/routes.rb

Rails.application.routes.draw do
  resources :messages, only: %i[index create]
  root to: 'messages#index'
end

and create app/views/messages/index.html.erb view:

Current messages:

<ul>
  <% @messages.each do |message| %>
    <li><%= message.username %>: <%= message.body %>
  <% end %>
</ul>

<h2>Add new message:</h2>

<%= form_for(@message) do |f| %>
  <%= f.text_field :username, placeholder: 'username' %>
  <%= f.text_area :body, placeholder: 'message' %>
  <%= f.submit 'Send' %>
<% end %>

Now our “application” works but looks terrible and page is reloading each time we hit the Send button.

Real-time chat first version

Adding proper styles to the application

Before we add Cable Ready to our application, let’s refactor the look a little bit so it looks more like a web chat.

Bootstrap installation

We will use the Bootstrap framework to save tons of time and adding nicely look to our simple application. The installation process is simple and consists of two steps. The first one is to install the library using yarn:

yarn add [email protected]

and the second one is to load styles. Update app/assets/stylesheets/application.css and add the following line:

*= require bootstrap

Updating styles of messages list and new message form

Make sure that your app/views/layouts/application.html.erb looks like the following:

<!DOCTYPE html>
<html>
  <head>
    <title>Chat</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <div class="flex-md-row p-3 px-md-4 mb-3 bg-white border-bottom box-shadow">
      <h5 class="text-center">Simple chat</h5>
    </div>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

and app/views/messages/index.html.erb :

<div class="row justify-content-center">
  <div class='col-6'>
    <div class="list-group" id="messages">
      <% @messages.each do |message| %>
        <a href="#" class="list-group-item list-group-item-action">
          <p class="mb-1"><%= message.body %></p>
          <small class="text-muted"><%= message.username %></small>
        </a>
      <% end %>
    </div>
  </div>
</div>
<div class="row justify-content-center">
  <div class='col-6'>
    <hr class="mt-3 mb-3"/>
    <h2>Add new message:</h2>
        
    <%= form_for(@message) do |f| %>
      <div class="form-group">
        <label>Username</label>
        <%= f.text_field :username, placeholder: 'username', class: 'form-control' %>
      </div>
      <div class="form-group">
        <label>Message</label>
        <%= f.text_area :body, placeholder: 'message', class: 'form-control' %>
      </div>
      <%= f.submit 'Send', class: 'btn btn-primary' %>
    <% end %>
  </div>
</div>

Now our application looks way better:

Real-time chat updated version

Making our chat real-time

It’s time to update our code so new chat messages will appear without reloading the whole page. In the first step, we will update the form so the request will be sent in the background via AJAX, not as the normal POST request with redirection. In the second step, we will finally add ActionCable along with CableReady to make real-time updates.

Making the submit form working with AJAX

Update our form code and add remote: true option:

<%= form_for(@message, remote: true) do |f| %>
  ...
<% end %>

You might be surprised but when you will submit the form after making the change, the new message will appear automatically. It happens because of the turbolinks and another GET request to our index action that returns updated data.

Turbolinks request screenshot

Cool. But there is one problem. We chat with other users and they won’t see this update so we need to add ActionCable so anyone can subscribe to the chat room and receive any message at the same time it is submitted.

Installation of ActionCable and Cable Ready

Time to give Cable Ready a try. Installation is simple:

bundle add cable_ready
yarn add cable_ready

Now we have to create a new channel:

bundle exec rails generate channel chat

update the app/channels/chat_channel.rb file and make sure it looks the same as below:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_channel"
  end
end

we also have to edit the javascript channel file and include Cable Ready there. Open app/javascript/channels/chat_channel.js and put there the following code:

import CableReady from 'cable_ready'
import consumer from './consumer'

consumer.subscriptions.create('ChatChannel', {
  received (data) {
    if (data.cableReady) CableReady.perform(data.operations)
  }
})

Broadcast new messages

We have now ActionCable and Cable Ready installed and configured, we can proceed by creating a method that will broadcast newly created message to the chat channel. Open app/models/message.rb and add the following method:

class Message < ApplicationRecord
  include CableReady::Broadcaster

  def broadcast
    cable_ready["chat_channel"].insert_adjacent_html(
      selector: "#messages",
      position: 'afterbegin',
      html: "<a href='#' class='list-group-item list-group-item-action'>
              <p class='mb-1'>#{body}</p>
              <small class='text-muted'>#{username}</small>
            </a>"
    )

    cable_ready.broadcast
  end
end

last change to our controller:

class MessagesController < ApplicationController
  def index
    @message = Message.new
    @messages = Message.order('created_at DESC')
  end

  def create
    message = Message.create!(message_params)
    
    message.broadcast
  end

  private

  def message_params
    params.require(:message).permit(:username, :body)
  end
end

the application now works as expected but there is one annoying behavior we have to change before we will give it a final test. Each time you submit a message it appears at the top of the messages list but values in form don’t disappear.

Final changes to our application

jQuery installation

We have to install jquery first:

yarn add jquery

to make it available, update config/webpack/environment.js and put there the following code:

const { environment } = require('@rails/webpacker')

const webpack = require("webpack")

environment.plugins.append("Provide", new webpack.ProvidePlugin({
  $: 'jquery',
  jQuery: 'jquery'
}))

module.exports = environment

Making the form more responsive

We will now listen for the result of our request and if it will be successful then we will reset the values in the form. To achieve that we have to update the app/javascripts/packs/application.js file with the following code:

$( document ).ready(function() {
  $("#new_message").on("ajax:success", function(event) {
    $('#new_message')[0].reset();
  });
});

The final application

Our application is now ready for the final tests. It is very simple but works as expected.

The code I wrote in this article is just a demonstration. To use it in a production environment you would need data validation and of course tests to make sure your code is working correctly and adding new changes won’t break it. However, for experimenting, such architecture is fine.