A simple working demo of Action Cable
The source code for this article is available at - Ashish Garg Github
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.
- Create a rails application
rails new ActionCableUploader
- For authentication add gem - clearance and jquery
gem 'clearance', '~> 1.16'
gem 'jquery-rails'
Add //= require jquery3 in application.js
- Install clearance
bundle install
rails generate clearance:install
Here User model will get created.
- For chat messages create a Message model
rails g model Message user:belongs_to body:text
- Define the associations
# models/user.rb
has_many :messages, dependent: :destroy
# models/message.rb
belongs_to :user
- Install migration
rails db:migrate
- Now we need add a page a from where new message will be submitted and all messages will be displayed
rails g controller chat index
- Edit the routes
root 'chats#index'
- Edit the Chat controller
class ChatsController < ApplicationController
before_action :require_login
def index
end
end
Here before_action :require_login is used for authentication.
- For showing the logged in user details edit the layout file
<!-- 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>
- Next we need a form to type the chat messages. There for update the chat index view file
<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 %>
- Create a new partial - views/messages/_message.html.erb
<div class="message">
<strong><%= message.user.email %></strong> says:
<%= message.body %>
<br>
<small>at <%= l message.created_at, format: :short %></small>
<hr>
</div>
- Edit the index action of chat controller
# chats_controller.rb
def index
@messages = Message.order(created_at: :asc)
end
Action Cable Client side
Till here this all was the basic rails application functionality. From here Action cable feature starts.
- Edit the environment file
config.action_cable.url = 'ws://localhost:3000/cable'
config.action_cable.allowed_request_origins = [ 'http://localhost:3000', 'http://127.0.0.1:3000' ]
- Edit routes for mounting action cable in the application
mount ActionCable.server => '/cable'
- Update the layout file for adding the meta tag
<!-- 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 add a js file in the assets - app/assets/javascripts/channels/chat.js
jQuery(document).on('turbolinks:load', function () {
$messages = $('#messages');
console.log($messages);
$new_message_form = $('#new-message');
$new_message_body = $new_message_form.find('#message-body');
if ($messages.length > 0) {
return App.chat = App.cable.subscriptions.create({
channel: "ChatChannel"
}, {
connected: function () {
},
disconnected: function () {
},
received: function(data) {
if (data['message']) {
$new_message_body.val('');
return $messages.append(data['message']);
}
},
send_message: function(message) {
return this.perform('send_message', {
message: message
});
}
});
}
});
Here Channel subscription is taking place. Callbacks [connected, disconnected, received and send_message] will be used to actually forward the messages to the server.
- Now we need to listen to the form submit event, prevent the default action and call the send_message method.
jQuery(document).on('turbolinks:load', function() {
if ($messages.length > 0) {
return $new_message_form.submit(function(e) {
var $this, message_body;
$this = $(this);
message_body = $new_message_body.val();
if ($.trim(message_body).length > 0) {
App.chat.send_message(message_body);
}
e.preventDefault();
return false;
});
}
});
Action Cable Server side
- Create a new file - app/channels/chat_channel.rb 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)
Message.create(body: data['message'])
end
end
There are two callbacks here that are run automatically: subscribed and unsubscribed. send_message is the methos that is called bt the following line of code in our chat.js
this.perform('send_message', {
message: message
});
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.
- Now modify the 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
- Now we need to broadcast the newly received message. We can perform this task in the background using Active job.
# 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
- Create messages controller
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
end
- Create background job
# 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