Blog

Thoughts from my daily grind

ActionCable - The Rails 5 Full Stack WebSocket Solution

Posted by Ziyan Junaideen |Published: 16 July 2016 |Category: Ruby on Rails
Default Upload |

One of the most significant features to come baked into Rails is its support for WebSockets. It was never too challenging to implement WebSockets in Rails using Faye. In fact, the Rails implementation of WebSockets uses Faye internally. However, it is a welcoming move to include WS with Rails. Rails WebSocket implementation, known as ActionCable, is a full-stack solution providing us with both the server-side and client-side features to implement WebSockets in Rails applications.

WebSockets and me

I have used WebSockets since I switched to Ruby in 2013. I joined the London based ClothesNetwork LTD (UK) in 2013, and its CEO Salman Valibeik and CTO Bjoern Rennhak invited me to their flagship product Orpiva. It had certain features that required us to post to the server, wait for an update and update the interface based on the response. The first implementation I did was with a 5s poll. Then Bjoern pointed me towards Faye, and there I was in WebSocket land. I have appreciated any development in the WS world as it has been a strength I have advertised to new clients. Since Orpiva, I have used Faye in 5 projects, including my current client TextBookValet (Update: Link removed - sadly, TextBookValet is no longer in operation).

Environment

I assume you are on a Rails 5 project by upgrading an older Rails project to Rails 5 or starting a new project. I will assume you are new to Rails and try to cover as much. Feel free to leave any comments if I have missed any steps.

gem install rails
rails new websocket_example --database postgresql

Note: ActionCable requires a threaded serve which means you will have to use some threaded server like Puma.

# Gemfile
gem 'puma'

Since we all love to leave TODO in our codes, let's create a ToDo list. We will then go and root the resource to access it through localhost:3000.

# Generate a scaffold Todo
rails g scaffold todo name:string
# config/routes.rb
root to: "todos#index"

Now we have a basic Rails project we can experiment with. Let's go into the details on how to enable WebSockets and use them next.

Configuration

Any WebSocket implementation has a server-side component and a client-side component. We mount the ActionCable server in the routes file, and then we configure the Rails application to point to the mounted ActionCable endpoint.

# config/routes.rb
mount ActionCable.server => '/cable'
# config/development.rb
Rails.application.configure do 
  config.action_cable.url = "ws://localhost:3000/cable"
end

For production configuration, you will have to use the right domain.

Before we proceed to creating a new channel to handle our WebSockets, I would like to point your attention to app/channels/application_cable/channel.rb file and the app/channels/application_cable/connection.rb file. We use these files to authorize incoming connections. Assume we need to isolate the ToDo list by user, then we will have to perform authentication using these files.

Next, we are going to create a new channel, board, a ToDo board. For simplicity and brevity, let's assume that we only create boards.

⇒  rails g channel board
Running via Spring preloader in process 48203
      create  app/channels/board_channel.rb
   identical  app/assets/javascripts/cable.js
      create  app/assets/javascripts/channels/board.coffee

You will notice that it tried to create 3 files, 1 of which already existed. This is because this is not the first channel I am creating.

We requested rails to create a " board " channel.

My plan is to append a table row when a user creates a ToDo item. For that, I will separate the table row as a partial.

<!--- app/views/todos/index.html.erb -->
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th colspan="3"></th>
    </tr>
  </thead>

  <tbody>
    <%= render partial: 'row', collection: @todos, as: :todo %>
  </tbody>
</table>


<!-- app/views/todos/_row.html.erb -->
<tr>
  <td><%= todo.name %></td>
  <td><%= link_to 'Show', todo %></td>
  <td><%= link_to 'Edit', edit_todo_path(todo) %></td>
  <td><%= link_to 'Destroy', todo, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>

Our setup is going to be simple.

  • The user submits the form as usual
  • The Todos controller creates a new Todo
  • We render a table row from this todo
  • Command ActionCable to broadcast it to every browser window subscribed for the channel

Lets first give the board a channel name. I am going to call it board_channel.

class BoardChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'board_channel'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end

Then when we create a ToDo, we are going to broadcast the message. You should do this in a Sidekiq or Resque job, but to keep this tutorial simple, I am going to do it in the controller itself.

class TodosController < ApplicationController
  before_action :set_todo, only: %i[show edit update destroy]

  # ...

  # POST /todos or /todos.json
  def create
    @todo = Todo.new(todo_params)

    respond_to do |format|
      if @todo.save
        # Bad bad code... move to a async job
        # Resque.enqueue TodoBroadcaster, todo.id, 'create'
        # TodoBroadcaster.perform_async @todo.id, 'create'
        ActionCable.server.broadcast(
          'board_channel',
          html: ApplicationController.renderer.render(partial: 'todos/row', locals: { todo: @todo })
        )

        format.html { redirect_to todo_url(@todo), notice: 'Todo was successfully created.' }
        format.json { render :show, status: :created, location: @todo }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @todo.errors, status: :unprocessable_entity }
      end
    end
  end

  # ...
end

Now every time a ToDo is created, we are broadcasting the table row HTML. What we need to do is to intercept the broadcast and append the HTML to the table body.

The following file is the generated board.coffee file with the #received method having 2 lines of JS to extract the rendered HTML and append it to the table.

# app/assets/javascripts/channels/board.coffee
App.board = App.cable.subscriptions.create "BoardChannel",
  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Called when there's incoming data on the websocket for this channel
    body = document.querySelector 'table tbody'
    body.innerHTML = body.innerHTML + data['html']

That is it; now you can go to http://localhost:3000 in two windows. In one, create a ToDo. When you hit submit, you will notice that the table gets appended by the new row in the other window.

Conclusion

This is a very basic demonstration of the capabilities of Ruby on Rails ActionCable. Imagine the possibilities of ActionCable real-tie features. I am planning to update the comments section of this blog and also add an ActionCable backed chat interface for potential customers.

Next

When I find the time I am going to write two posts and share with you a Github repository a ToDo app that supports create, edit, update, delete and mark-complete. I will also write another post on the differences between HTTP and WebSockets. If I cover all these in one post, both you and I are going to get bored.

About the Author

Ziyan Junaideen -

Ziyan is an expert Ruby on Rails web developer with 8 years of experience specializing in SaaS applications. He spends his free time he writes blogs, drawing on his iPad, shoots photos.

Comments