Rails 5 has introduced many new cool features and ActionCable is probably the most anticipated one. To put it simply, ActionCable is a framework for real-time configuration over web sockets. It provides both client-side (JavaScript) and server-side (Ruby) code and so you can craft sockets-related functionality like any other part of your Rails application. I really like this new addition and recommend giving it a shot.
There are a handful of introductory tutorials on the Internet explaining how to get stated with ActionCable, however students often ask me how to introduce file uploading functionality over web sockets. This topic is not really covered anywhere, so I decided to research it myself.
Table of Contents
In this two-parted tutorial we will create a basic chat application powered by Rails 5.1 and ActionCable with the ability to upload files. We will utilize Clearance for authentication as well as Shrine and FileReader API for file uploading.
The source code for this article is available at GitHub. The final application will look like this:
In the first part of the tutorial we are going to create a new application, introduce basic authentication, integrate ActionCable and utilize ActiveJob for the broadcasting. Shall we proceed?
Laying Foundations
Start off by creating a new Rails application:
rails new ActionCableUploader
At the time of writing the newest version of Rails was 5.1.1, so I am going to use it for this demo. Please note that ActionCable is not included in Rails 4 and older.
We will require a basic authentication system. To speed things up, we are not going to write it from scratch but rather use some third-party solution. The most obvious choice that comes to mind is probably Devise but let's make things a bit more interesting and use another solution called Clearance. This gem is similar to Devise, but is intended to be smaller and simpler. After all, we really do need something simple, as this article is not about authentication solutions. Clearance was created by Thoughtbot, the guys who brought us Paperclip, FactoryGirl and other great solutions.
BeginnerTailwind.com Learn Tailwind CSS from ScratchSo, drop a new gem into the Gemfile:
gem 'clearance', '~> 1.16'
and then run:
bundle install
rails generate clearance:install
The latter command is going to equip your application with the Clearance's code. It is going to perform the following operations:
- Create a
User
model and the corresponding migration. If you already have a model with such name, it will be tweaked properly - Create an initializer file for Clearance. You are welcome to check it out and modify as needed
- Insert a
Clearance::Controller
module into theApplicationController
- Make you a coffee (well, actually it won't)
When you are ready, apply the migration:
rails db:migrate
That's it, the preparations are done and we can move to the next section!
Adding Chat Page
What I want to do now is create the chat page and restrict access to it. The corresponding controller will be called ChatsController
. Add a root route now:
# config/routes.rb
root 'chats#index'
Don't forget to create the controller itself:
# controllers/chats_controller.rb
class ChatsController < ApplicationController
before_action :require_login
def index
end
end
The before_action :require_login
line, as you've probably guessed, restricts access to all actions of the current controller. This action does pretty much the same as the authenticate_user!
method in Devise.
Now create a views/chats/index.html.erb view that will have only a header for now:
<h1>Demo Chat</h1>
Lastly, populate your application layout with the following contents to display a sign out link and flash messages (if any):
<!-- views/layouts/application.html.erb -->
<% if signed_in? %>
Signed in as: <%= current_user.email %>
<%= button_to 'Sign out', sign_out_path, method: :delete %>
<% else %>
<%= link_to 'Sign in', sign_in_path %>
<% end %>
<div id="flash">
<% flash.each do |key, value| %>
<%= tag.div value, class: "flash #{key}" %>
<% end %>
</div>
Now start the server and navigate to http://localhost:3000
. You should see a page similar to this one.
Note that you are automatically redirected to the Sign In page as you are not yet logged in.
Register with some sample credentials—after that you should be able to see the chat page which means everything is working just fine.
Messages
Now let's create a new model and the corresponding table. I'll call the model Message
which is quite an unsuprising name. It will have a body and a foreign key to establish an association to the users
table:
rails g model Message user:belongs_to body:text
rails db:migrate
Make sure that your models have the proper associations set up, as we want each message to have an author (that is, a user):
# models/user.rb
has_many :messages, dependent: :destroy
# models/message.rb
belongs_to :user
Brilliant. Next, of course, we'll need a form to actually send a message. To render it, I am going to use a new helper method called form_with introduced in Rails 5 which is meant to replace form_for
and form_tag
(though the latter methods are still supported). The form will be processed by JavaScript, so I'll use #
for the URL. Place the following code into your views/chats/index.html.erb view:
<div id="messages">
<%= render @messages %>
</div>
<%= form_with url: '#', html: {id: 'new-message'} do |f| %>
<%= f.label :body %>
<%= f.text_area :body, id: 'message-body' %>
<br>
<%= f.submit %>
<% end %>
Note that the form_with
does not generate any ids for the tags so I am adding them manually to further select these elements using JS.
I've also provided the #messages
block to render all the messages. This requires the views/messages/_message.html.erb partial to be present, so add it now:
<div class="message">
<strong><%= message.user.email %></strong> says:
<%= message.body %>
<br>
<small>at <%= l message.created_at, format: :short %></small>
<hr>
</div>
Lastly, load the messages inside the index
action:
# chats_controller.rb
def index
@messages = Message.order(created_at: :asc)
end
This will sort them by creation date, ascending. Your chat page will look something like this:
Okay, now it is time for the ActionCable to step into the limelight.
ActionCable: Time for Action!
Our next task is to enable real-time conversation between the client and the server powered by the ActionCable's magic. Let's start with adding some configuration:
# config/environments/development.rb
config.action_cable.url = 'ws://localhost:3000/cable'
config.action_cable.allowed_request_origins = [ 'http://localhost:3000', 'http://127.0.0.1:3000' ]
Next, a route:
# routes.rb
# ...
mount ActionCable.server => '/cable'
Lastly, meta tags:
<!-- views/layouts/application.html.erb -->
<!-- ... -->
<%= action_cable_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<!-- ... -->
Now let's take care of the client and write some CoffeeScript code. Create a new file app/assets/javascripts/channels/chat.coffee with the following contents:
jQuery(document).on 'turbolinks:load', ->
$messages = $('#messages')
$new_message_form = $('#new-message')
$new_message_body = $new_message_form.find('#message-body')
if $messages.length > 0
App.chat = App.cable.subscriptions.create {
channel: "ChatChannel"
},
connected: ->
disconnected: ->
received: (data) ->
send_message: (message) ->
Here we are checking if the #messages
block is present on the page and, if yes, set up a new subscription to the ChatChannel
. This channel will be used to communicate with the server in real time. Note that there are a bunch of callbacks that you can use: connected
, disconnected
and received
. send_message
will be used to actually forward the messages to the server. This new file will be loaded automatically as javascripts/cable.coffee requires the channels folder by default.
One thing to note, however, is that Rails 5.1 apps do not include jQuery as a dependency anymore, so you'll need to add it yourself:
# Gemfile
gem 'jquery-rails
Run:
bundle install
and include jQuery to the javascripts/application.js file:
//= require jquery3
I am including the latest version of jQuery to support only the modern browsers, but you can also choose versions 1 or 2.
Now we need to listen for the form submit event, prevent the default action and call the send_message
method defined for the channel instead:
jQuery(document).on 'turbolinks:load', ->
# ...
if $messages.length > 0
# ...
$new_message_form.submit (e) ->
$this = $(this)
message_body = $new_message_body.val()
if $.trim(message_body).length > 0
App.chat.send_message message_body
e.preventDefault()
return false
Here I am checking if the body has at least one character and call the send_message
method if it is true. Nothing complex going on here.
Next, flesh out the send_message
method. What it needs to do is receive the body of the message and forward if to the server where it will be stored to the database. Note that this method will not output anything to the page—it should happen inside the received
callback.
# ...
send_message: (message) ->
@perform 'send_message', message: message
@
means this
in CoffeeScript. 'send_message'
argument is the name of the method to call on the server-side which we will create in a minute.
Lastly, code the received
callback to clear the textarea and render a new message:
# ...
received: (data) ->
if data['message']
$new_message_body.val('')
$messages.append data['message']
That's it, we have finished coding the client-side! The server-side awaits, so proceed to the next section.
ActionCable: Server-Side
If you have played The Witcher series, you know that the sword of destiny has two edges. So as ActionCable. Therefore, let's take of the server-side now.
Create a new app/channels/chat_channel.rb file that will process the messages sent from the client-side:
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_channel"
end
def unsubscribed
end
def send_message(data)
end
end
There are two callbacks here that are run automatically: subscribed
(that runs as soon as the new client subscribes to the channel using App.cable.subscriptions.create
code we've written a moment ago) and unsubscribed
. send_message
is the method that is called by the following line of code in our chat.coffee file:
@perform 'send_message', message: message
Note, by the way, that the files inside the app/channels directory are not auto-reloaded (even in development environment), so you must restart the server after modifying them.
The data
local variable contains a hash so we can access the message's body quite easily to save it to the database:
# ...
def send_message(data)
Message.create(body: data['message'])
end
There is a problem, however: we don't have access to the Clearance's current_user
method from inside the channel's code, therefore it is not possible to enforce authentication and associate the created message to a user.
To fix this problem, the current_user
should be defined manually. We are going to employ the methods similar to the ones provided in the Clearance's session.rb file:
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_current_user
reject_unauthorized_connection unless self.current_user
end
private
def find_current_user
if remember_token.present?
@current_user ||= user_from_remember_token(remember_token)
end
@current_user
end
def cookies
@cookies ||= ActionDispatch::Request.new(@env).cookie_jar
end
def remember_token
cookies[Clearance.configuration.cookie_name]
end
def user_from_remember_token(token)
Clearance.configuration.user_model.find_by(remember_token: token)
end
end
end
The following code simply tries to find a currently logged in user by a remember token stored in the cookie (the cookie's name is taken from the Clearance configuration). The user is then assiged to the self.current_user
. If, however, the user cannot be found, we reject connection effectively disallowing to communicate using the channel. The connect
method is called automatically each time someone tries to subscribe to a channel, so there nothing else we need to do here.
Now return to the ChatChannel
and tweak the send_message
method a bit:
# ...
def send_message(data)
current_user.messages.create(body: data['message'])
end
At this point our ActionCable setup is finished. Later you can add other channels using the same principle. There is, however, one last thing to do (yeah, there is always "one last thing", isn't it?). After the message is stored in the database, it should be broadcasted to all users who are subscribed to the channel. The client code will then run the received
callback and render the new message. So, let's do it now!
Callback and (Very) ActiveJob
We are going to employ a model callback to broadcast a newly created message. However, I'd like to perform this task in background, therefore using the ActiveJob for this task seems like a good idea as well.
Here is the code for the Message
model:
# models/message.rb
class Message < ApplicationRecord
belongs_to :user
validates :body, presence: true
after_create_commit :broadcast_message
private
def broadcast_message
MessageBroadcastJob.perform_later(self)
end
end
First of all, I've added a very basic validation rule to ensure the body is present. Next, there is a new after_create_commit
callback that runs only after the commit was performed. Inside the corresponding method we are queueing the broadcasting job while passing self
as an argument. self
in this case points to the created message. Using the background job here is convenient because later you may extend it and, for example, send notification emails to the users saying that there is a new message waiting for them.
The background job itself is quite simple:
# app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast 'chat_channel', message: render_message(message)
end
private
def render_message(message)
MessagesController.render partial: 'messages/message', locals: {message: message}
end
end
We queue the job with a default priority. Inside the perform
action we broadcast the message rendered by the MessagesController
. Note that the same partial created earlier is utilized here.
The MessagesController
does not exist, so create it now:
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
end
And that's it! Our messages are now saved and broadcasted properly, so you can boot the server, navigate to the main page of the site and try to chat with yourself. Note that after you load the page, a request to the /cable will be performed (you may observe it using Firebug or a similar tool):
To make process a bit more interesting you may open two separate browser windows and note that messages appear in both of them nearly instantly:
Inside the terminal you should see an output like this:
Conclusion
This ends the first part of the tutorial. We have crafted the real-time chatting application that can now be extended quite easily. Throughout the article you have learned how to:
- Integrate Clearance gem
- Code the client-side for ActionCable
- Code the server-side for ActionCable
- Enforce server-side authentication
- Use ActiveJob to broadcast messages
In the second part we will finalize this application and allow the users to upload files via ActionCable with the help of the Shrine gem and FileReader Web API.
So, stay tuned and see you soon!