Ruby
Article

Create a Chat App with Rails 5, ActionCable, and Devise

By Ilya Bodrov-Krukowski

Rails 5 has introduced a bunch of new great features, but one of the most anticipated ones is of course ActionCable. ActionCable seamlessly integrates WebSockets into your application and offers both client-side JS and server-side Ruby frameworks. This way, you can write real-time features in the same styles as the rest your application, which is really cool.

Some months ago I wrote a series of articles describing how to build mini-chat with Rails using AJAX, WebSockets powered by Faye and Server-sent events. Those articles garnered some attention, so I decided to pen a new part of this series instructing how to use ActionCable to achieve the same goal.

This time, however, we will face a bit more complicated task and discuss the following topics:

  • Preparing application and integrating Devise
  • Introducing chat rooms
  • Setting up ActionCable
  • Coding client-side
  • Coding server-side with the help of background jobs
  • Introducing basic authorization for ActionCable
  • Preparing application to be deployed to Heroku

The source code can be found at GitHub.

The working demo is available at sitepoint-actioncable.herokuapp.com.

Preparing the Application

Start off by creating a new application. Support for ActionCable was added only in Rails 5, so you will have to use this version (currently 5.0.0.rc1 is the latest one):

$ rails new CableChat -T

Now add a couple of gems:

Gemfile

[...]
gem 'devise'
gem 'bootstrap', '~> 4.0.0.alpha3'
[...]

Devise will be used for authentication and authorization (you may read this article to learn more) and Bootstrap 4 – for styling.

Run

$ bundle install

Add Bootstrap’s styles:

stylesheets/application.scss

@import "bootstrap";

Run the following commands to install Devise, generate a new User model, and copy views for further customization:

$ rails generate devise:install
$ rails generate devise User
$ rails generate devise:views
$ rails db:migrate

Now restrict access to all pages of the site to authenticated users only:

application_controller.rb

[...]
before_action :authenticate_user!
[...]

Chat Rooms

The next step is to add support for chat rooms, so generate the following model:

$ rails g model ChatRoom title:string user:references
$ rails db:migrate

A chat room should have a creator, so make sure you establish a one-to-many relation between chat_rooms and users:

models/chat_room.rb

[...]
belongs_to :user
[...]

models/users.rb

[...]
has_many :chat_rooms, dependent: :destroy
[...]

Code a controller to list and create chat rooms:

chat_rooms_controller.rb

class ChatRoomsController < ApplicationController
  def index
    @chat_rooms = ChatRoom.all
  end

  def new
    @chat_room = ChatRoom.new
  end

  def create
    @chat_room = current_user.chat_rooms.build(chat_room_params)
    if @chat_room.save
      flash[:success] = 'Chat room added!'
      redirect_to chat_rooms_path
    else
      render 'new'
    end
  end

  private

  def chat_room_params
    params.require(:chat_room).permit(:title)
  end
end

Now a bunch of really simple views:

views/chat_rooms/index.html.erb

<h1>Chat rooms</h1>

<p class="lead"><%= link_to 'New chat room', new_chat_room_path, class: 'btn btn-primary' %></p>

<ul>
  <%= render @chat_rooms %>
</ul>

views/chat_rooms/_chat_room.html.erb

<li><%= link_to "Enter #{chat_room.title}", chat_room_path(chat_room) %></li>

views/chat_rooms/new.html.erb

<h1>Add chat room</h1>

<%= form_for @chat_room do |f| %>
  <div class="form-group">
    <%= f.label :title %>
    <%= f.text_field :title, autofocus: true, class: 'form-control' %>
  </div>

  <%= f.submit "Add!", class: 'btn btn-primary' %>
<% end %>

Messages

The main star of our app is, of course, a chat message. It should belong to both a user and a chat room. To get there, run the following:

$ rails g model Message body:text user:references chat_room:references
$ rails db:migrate

Make sure to establish the proper relations:

models/chat_room.rb

[...]
belongs_to :user
has_many :messages, dependent: :destroy
[...]

models/users.rb

[...]
has_many :chat_rooms, dependent: :destroy
has_many :messages, dependent: :destroy
[...]

models/message.rb

[...]
belongs_to :user
belongs_to :chat_room
[...]

So far, so good. Messages should be displayed when a user enters a chat room, so create a new show action:

chat_rooms_controller.rb

[...]
def show
  @chat_room = ChatRoom.includes(:messages).find_by(id: params[:id])
end
[...]

Note the includes method here used for eager loading.

Now the views:

views/chat_rooms/show.html.erb

<h1><%= @chat_room.title %></h1>

<div id="messages">
  <%= render @chat_room.messages %>
</div>

views/messages/_message.html.erb

<div class="card">
  <div class="card-block">
    <div class="row">
      <div class="col-md-1">
        <%= gravatar_for message.user %>
      </div>
      <div class="col-md-11">
        <p class="card-text">
          <span class="text-muted"><%= message.user.name %> at <%= message.timestamp %> says</span><br>
          <%= message.body %>
        </p>
      </div>
    </div>
  </div>
</div>

In this partial three new methods are employed: user.name, message.timestamp and gravatar_for. To construct a name, let’s simply strip off the domain part from the user’s email (of course, in a real app you’d want to allow them entering a name upon registration or at the “Edit profile” page):

models/user.rb

[...]
def name
  email.split('@')[0]
end
[...]

timestamp relies on strftime to present message’s creation date in a user-friendly format:

models/message.rb

[...]
def timestamp
  created_at.strftime('%H:%M:%S %d %B %Y')
end
[...]

gravatar_for is a helper to display user’s gravatar:

application_helper.rb

module ApplicationHelper
  def gravatar_for(user, opts = {})
    opts[:alt] = user.name
    image_tag "https://www.gravatar.com/avatar/#{Digest::MD5.hexdigest(user.email)}?s=#{opts.delete(:size) { 40 }}",
              opts
  end
end

The last two things to do here is to style the messages container a bit:

#messages {
  max-height: 450px;
  overflow-y: auto;
  .avatar {
    margin: 0.5rem;
  }
}

Add routes:

config/routes.rb

[...]
resources :chat_rooms, only: [:new, :create, :show, :index]
root 'chat_rooms#index'
[...]

Finally, preparations are done and we can proceed to coding the core functionality of our chat.

Adding ActionCable

Client Side

Before proceeding, install Redis on your machine if you do not already have it. Redis is available for nix, via Homebrew and for Windows, as well.

Next, tweak the Gemfile:

Gemfile

[...]
gem 'redis', '~> 3.2'
[...]

and run

$ bundle install

Now you may modify the config/cable.yml file to use Redis as an adapter:

config/cable.yml

[...]
adapter: redis
url: YOUR_URL
[...]

Or simply use adapter: async (the default value).

Also, modify your routes.rb to mount ActionCable on some URL:

config/routes.rb

[...]
mount ActionCable.server => '/cable'
[...]

Check that inside the javascripts directory there is a cable.js file with the contents like:

javascripts/cable.js

//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);

This file must be required inside application.js:

javascripts/application.js

[...]
//= require cable
[...]

Consumer is a client of a web socket connection that can subscribe to one or multiple channels. Each ActionCable server may handle multiple connections. Channel is similar to an MVC controller and is used for streaming. You may read more about ActionCable’s terminology here.

So, let’s create a new channel:

javascripts/channels/rooms.coffee

App.global_chat = App.cable.subscriptions.create {
    channel: "ChatRoomsChannel"
    chat_room_id: ''
  },
  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) ->
    # Data received

  send_message: (message, chat_room_id) ->
    @perform 'send_message', message: message, chat_room_id: chat_room_id

Here, we basically subscribe a consumer to the ChatRoomsChannel and pass the current room’s id (at this point we do not really pass anything, but that’ll be fixed soon). The subscription has a number of self-explaining callbacks: connected, disconnected, and received. Also, the subscription defines the main function (send_message) that invokes the method with the same name of the server and passes the necessary data to it.

Of course, we need a form to allow users to send their messages:

views/chat_rooms/show.html.erb

<%= form_for @message, url: '#' do |f| %>
  <div class="form-group">
    <%= f.label :body %>
    <%= f.text_area :body, class: 'form-control' %>
    <small class="text-muted">From 2 to 1000 characters</small>
  </div>

  <%= f.submit "Post", class: 'btn btn-primary btn-lg' %>
<% end %>

The @message instance variable should be set inside the controller:

chat_rooms_controller.rb

[...]
def show
  @chat_room = ChatRoom.includes(:messages).find_by(id: params[:id])
  @message = Message.new
end
[...]

Of course, you might use the basic form tag instead of relying on the Rails form builder, but this allows us to take advantage of things like I18n translations later.

Let’s also add some validations for messages:

models/message.rb

[...]
validates :body, presence: true, length: {minimum: 2, maximum: 1000}
[...]

Another problem to tackle here is providing our script with the room’s id. Let’s solve it with the help of HTML data- attribute:

views/chat_rooms/show.html.erb

[...]
<div id="messages" data-chat-room-id="<%= @chat_room.id %>">
  <%= render @chat_room.messages %>
</div>
[...]

Having this in place, we can use room’s id in the script:

javascripts/channels/rooms.coffee

jQuery(document).on 'turbolinks:load', ->
  messages = $('#messages')
  if $('#messages').length > 0

    App.global_chat = App.cable.subscriptions.create {
        channel: "ChatRoomsChannel"
        chat_room_id: messages.data('chat-room-id')
      },
      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) ->
        # Data received

      send_message: (message, chat_room_id) ->
        @perform 'send_message', message: message, chat_room_id: chat_room_id

Note the jQuery(document).on 'turbolinks:load' part. This should be done only if you are using Turbolinks 5 that supports this new event. You might think about usng jquery-turbolinks to bring the default jQuery events back, but unfortunately it is not compatible with Turbolinks 5.

The logic of the script is pretty simple: check if there is a #messages block on the page and, if yes, subscribe to the channel while providing the room’s id. The next step is to listen for the form’s submit event:

javascripts/channels/rooms.coffee

jQuery(document).on 'turbolinks:load', ->
  messages = $('#messages')
  if $('#messages').length > 0

    App.global_chat = App.cable.subscriptions.create
    # ...

    $('#new_message').submit (e) ->
      $this = $(this)
      textarea = $this.find('#message_body')
      if $.trim(textarea.val()).length > 1
        App.global_chat.send_message textarea.val(), messages.data('chat-room-id')
        textarea.val('')
      e.preventDefault()
      return false

When the form is submitted, take the message’s body, check that its length is at least two and then call the send_message function to broadcast the new message to all visitors of the chat room. Next, clear the textarea and prevent form submission.

Server Side

Our next task will be to introduce a channel on our server. In Rails 5, there is a new directory called channels to host them, so create a chat_rooms_channel.rb file there:

channels/chat_rooms_channel.rb

class ChatRoomsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_rooms_#{params['chat_room_id']}_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def send_message(data)
    # process data sent from the page
  end
end

subscribed is a special method to start streaming from a channel with a given name. As long as we have multiple rooms, the channel’s name will vary. Remember, we provided chat_room_id: messages.data('chat-room-id') when subscribing to a channel in our script? Thanks to it, chat_room_id can be fetched inside the subscribed method by calling params['chat_room_id'].

unsubscribed is a callback that fires when the streaming is stopped, but we won’t use it in this demo.

The last method – send_message – fires when we run @perform 'send_message', message: message, chat_room_id: chat_room_id from our script. The data variable contains a hash of sent data, so, for example, to access the message you would type data['message'].

There are multiple ways to broadcast the received message, but I am going to show you a very neat solution based on the demo provided by DHH (I’ve also found this article with a slightly different approach).

First of all, modify the send_message method:

channels/chat_rooms_channel.rb

[...]
def send_message(data)
  current_user.messages.create!(body: data['message'], chat_room_id: data['chat_room_id'])
end
[...]

Once we receive a message, save it to the database. You don’t even need to check whether the provided chat room exists – by default, in Rails 5, a record’s parent must exist in order to save it. This behavior can be changed by setting optional: true for the belongs_to relation (read about other changes in Rails 5 here).

There is a problem though – Devise’s current_user method is not available for us here. To fix that, modify the connection.rb file inside the application_cable directory:

channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
      logger.add_tags 'ActionCable', current_user.email
    end

    protected

    def find_verified_user # this checks whether a user is authenticated with devise
      if verified_user = env['warden'].user
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

Having this in place, we achieve even two goals at once: the current_user method is now available for the channel and unauthenticated users are not able to broadcast their messages.

The call to logger.add_tags 'ActionCable', current_user.email is used to display debugging information in the console, so you will see output similar to this:

[ActionCable] [test@example.com] Registered connection (Z2lkOi8vY2FibGUtY2hhdC9Vc2VyLzE)
[ActionCable] [test@example.com] ChatRoomsChannel is transmitting the subscription confirmation
[ActionCable] [test@example.com] ChatRoomsChannel is streaming from chat_rooms_1_channel

Under the hood Devise uses Warden for authentication, so env['warden'].user tries to fetch the currently logged-in user. If it is not found, reject_unauthorized_connection forbids broadcasting.

Now, let’s add a callback that fires after the message is actually saved to the database to schedule a background job:

models/message.rb

[...]
after_create_commit { MessageBroadcastJob.perform_later(self) }
[...]

In this callback self is a saved message, so we basically pass it to the job. Write the job now:

jobs/message_broadcast_job.rb

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
                                 message: 'MESSAGE_HTML'
  end
end

The perform method does the actual broadcasting, but what about the data we want to broadcast? Once again, there are a couple of ways to solve this problem. You may send JSON with the message data and then on the client side use a templating engine like Handlebars. In this demo, however, let’s send HTML markup from the messages/_message.html.erb partial we created earlier. This partial can be rendered with the help of a controller:

jobs/message_broadcast_job.rb

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "chat_rooms_#{message.chat_room.id}_channel",
                                 message: render_message(message)
  end

  private

  def render_message(message)
    MessagesController.render partial: 'messages/message', locals: {message: message}
  end
end

In order for this to work, you’ll have to create an empty MessagesController:

messages_controller.rb

class MessagesController < ApplicationController
end

Back to the Client Side

Great, now the server side is ready and we can finalize our script. As long as we broadcast HTML markup, it can be simply placed right onto the page without any further manipulations:

javascripts/channels/rooms.coffee

[...]
App.global_chat = App.cable.subscriptions.create {
    channel: "ChatRoomsChannel"
    chat_room_id: messages.data('chat-room-id')
  },
  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) ->
    messages.append data['message']

  send_message: (message, chat_room_id) ->
    @perform 'send_message', message: message, chat_room_id: chat_room_id
[...]

The only thing I don’t really like here is that, by default, the user sees old messages, whereas the newer ones are being placed at the bottom. You could either use the order method to sort them properly and replace append with prepend inside the received callback, but I’d like to make our chat behave like Slack. In Slack, newer messages are also placed at the bottom, but the chat window automatically scrolls to them. That’s easy to achieve with the following function that is called once the page is loaded:

javascripts/channels/rooms.coffee

jQuery(document).on 'turbolinks:load', ->
  messages = $('#messages')
  if $('#messages').length > 0
    messages_to_bottom = -> messages.scrollTop(messages.prop("scrollHeight"))

    messages_to_bottom()

    App.global_chat = App.cable.subscriptions.create
    # ...

Let’s also scroll to the bottom once a new message has arrived (because by default it won’t be focused):

javascripts/channels/rooms.coffee

[...]
received: (data) ->
  messages.append data['message']
  messages_to_bottom()
[...]

Great! Check out the resulting script on GitHub.

Pushing to Heroku

If you wish to push your new shiny chat to Heroku, some additional actions have to be taken. First of all, you will have install a Redis addon. There are many addons to choose from: for example, you could use Rediscloud. When the addon is installed, tweak cable.yml to provide the proper Redis URL. For Rediscloud it is stored inside the ENV["REDISCLOUD_URL"] environment variable:

config/cable.yml

production:
  adapter: redis
  url: <%= ENV["REDISCLOUD_URL"] %>
[...]

The next step is to list the allowed origins to subscribe to the channels:

config/environments/production.rb

[...]
config.action_cable.allowed_request_origins = ['https://your_app.herokuapp.com',
                                               'http://your_app.herokuapp.com']
[...]

Lastly, you have to provide the ActionCable URL. As long as our routes.rb has mount ActionCable.server => '/cable', the corresponding setting should be:

config/environments/production.rb

[...]
config.action_cable.url = "wss://sitepoint-actioncable.herokuapp.com/cable"
[...]

Having this in place, you can push your code to Heroku and observe the result. Yay!

Conclusion

In this article we’ve discussed how to set up ActionCable and code a mini-chat app with support for multiple rooms. The app includes both the client and server sides, while providing a basic authorization mechanism. We also employed a background job to power up the broadcasting process and discussed steps required to deploy the application to Heroku.

Hopefully, you found this article useful and interesting. Have you already tried using ActionCable? Did you like it? Share your opinion in the comments. Follow me on Twitter to be the first one to know about my articles, and see you soon!

  • Ja

    Hey man, quick thing. For the little devise code bits. Rails db:migrate doesn’t always work for people. Probably want to change that to $ rake db:migrate

    • stephanromanov

      *rails db:migrate* is the correct RoR v5 way ;-)

    • Ilya Bodrov

      Yeah, in Rails 5 the rake and rails commands can be used interchangably.

  • Vladimir Tsymbal

    Hi. Need to fix article. For example you created job in file jobs/messages_broadcast_job.rb

    MessageBroadcastJob won’t be autoloaded . For this need to rename file to message_broadcast_job.rb

  • Luis De Castro

    Great guide! One thing I noticed that I needed to change in order for it to work was in the jQuery submit portion where you are passing in the data to the send_message function, I needed to change the 2nd parameter to “message.data(‘chatRoomId’)” instead of ‘chat-room-id’. I kept getting an undefined when send_message was being called so did a little digging and found that it was parsing it differently.

    Double check this in case I’m the only one getting this error.

    Overall thanks!

    • Ilya Bodrov

      Thank you! Yeah, that might be a problem. Which jQuery version are you using?

      • Luis De Castro

        Yup! that was the issue! I did not specify which version of jQuery in my gemfile.
        Thanks!

        • Ilya Bodrov

          That’s interesting enough. I remember something about older version jQuery having issues with data- attributes but not 100% sure… Will try to research more.

  • Jo

    Hello @disqus_hOxF05PWBh:disqus thank you for your post. I’m new to programming and faced, till now two problems. First in chat_rooms/show.htlm.erb I had to change this to this {:class => “form-horizontal”, :role => “form”}, url: chat_rooms_path) do |f| %> and it worked. Now I’m facing an error to post message. To post my message I have this error “ActionController::ParameterMissing in ChatRoomsController#create => param is missing or the value is empty: chat_room”. What did I missed here? Thanks for your help

    • Ilya Bodrov

      Most likely you’ve missed something. The whole point of including `url: ‘#’` was to prevent form being submitted as usual and instead processing it with JS, so bringing the initial url back is incorrect, at least if you are following this demo. Double check your code, don’t forget that the source code is published at GitHub https://github.com/bodrovis/Sitepoint-source/tree/master/Chat_with_ActionCable_and_Devise

      • Jo

        Thanks for your answer. Just checked file by file and the error persist :(

        • Ilya Bodrov

          Well, no one else has reported this error so far and the demo does work as well… You can contact me directly via e-mail and hopefully we’ll solve this problem.

          • arklove

            I have the same error:
            ProjectsChannel is streaming from projects_1_channel
            Started POST “/projects/1” for ::1 at 2016-07-13 03:06:16 +0300

            ActionController::RoutingError (No route matches [POST] “/projects/1”):

            This will redirect me to /projects/1#

          • arklove

            Finally, I reviewed your code and now seems to work, but now I receive this error.

            [ActionCable] [***@gmail.com] ProjectsChannel is streaming from projects__channel
            [ActionCable] [***@gmail.com] ProjectsChannel#send_message({“message”=>”hey”})
            [ActionCable] [***@gmail.com] (0.1ms) begin transaction
            [ActionCable] [***@gmail.com] (0.0ms) rollback transaction
            [ActionCable] [***@gmail.com] Could not execute command from {“command”=>”message”, “identifier”=>”{“channel”:”ProjectsChannel”}”, “data”=>”{“message”:”hey”,”action”:”send_message”}”}) [ActiveRecord::RecordInvalid – Validation failed: Project must exist]:

          • Shiv Shankara

            what was your mistake? I have the same no route matches[POST] error

          • Matthew Stevenson

            How did you fix this error? I have searched through the github source and not found the discrepancy with my files.

          • Matthew Stevenson

            ran git diff on every file to compare to the github source and found a couple typos. submet != submit and currnet_user != current_user.

  • abmahoodi

    Hi, I deploy this example to my VPS with puma and nginx web server.
    my nginx config is the following:

    upstream websocket {
    server 127.0.0.1:28080;
    }

    server {
    location /cable {
    proxy_pass http://websocket/;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection “Upgrade”;
    }
    }
    I have a proxy server on my client and I get this error in the browser:
    Establishing a tunnel via proxy server failed

  • Mohamad Fadil

    Hi , I was wondering how do I post pictures/images to the chat room with ActionCable ?

  • Andrey Zhmaylo

    Hi @Ilya Bodrov. Thank you for your post. It helped me. But now I have some problem. Perhaps someone can tell me what the cause of the problem. When I click on button, that create a message, nothing happens. In console I have last string: [ActionCable] [name10@nnnn.net] ChatsChannel is streaming from chats_3_channel
    Method “send_message” must be called next string, but it is’t. I don’t see where is problem. What could it be? Thank you.

    • Ilya Bodrov

      That’s most interesting. Seems like you have some improperly called method… Could you contact me via e-mail and share some relevant code?

      • Andrey Zhmaylo

        Sorry. Find problem. I am not carefully considered one of the files, and incorrectly specified one of parameters.

      • Andrey Zhmaylo

        Hello. I send some question to your email. I hope you receive it.

  • Ferran Cabrer i Vilagut

    Thank you very much for this master lesson. As intermediate RoR programmer I’ve had some difficulties to follow and fully understand all. After deploying in our OVZ, Debian8-Apache-Passenger, we have this first error in chat-room show: undefined method `title’ for nil:NilClass. Any help would be appreciated.

    • Ilya Bodrov

      Let’s discuss this via e-mail or Skype :)

  • Sebastian Adam

    Thanks so much for the tutorial. Easy to follow along. One discrepancy between the lesson above and your github repo is the inclusion of in show.html.erb in your github. Without this line I kept encountering a routing error:

    ActionController::RoutingError (No route matches [POST] “/chat_ro
    oms/1”)

    All in all, much easier than using WebSockets and Sinatra!

    • Ilya Bodrov

      This field is required to mark with chat room do we want to send this message to. Otherwise our code has no idea where the message belongs :)

    • Matthew Stevenson

      I am getting that same error despite adding that line. Any idea where else to look?

  • Ilya Bodrov

    Oh, I’m sorry, for reason haven’t received notification about this message. You may just use seeds.rb to populate database with a room once your app is initially deployed. Of course, callbacks can be used as well but I’d stay away from them in this scenario

  • Maximilian Fornaçon

    It would be awesome to know how to delete a message. :)

    • Ilya Bodrov

      Pretty much the same as it is being created, just use destroy instead of create.

      • Maximilian Fornaçon

        Could you give further instructions on this please.

        • Ilya Bodrov

          Well. Add a “destroy” link, bind a click event to it. Then initialize a new action inside ActionCable, like @perform ‘delete_message’, message_id_here
          Then create method with the same name for the channel. This method should delete the message. Then simply broadcast a notification saying that the message is deleted. As soon as this message is deleted, remove it from the page using remove() jQuery method.

  • Damian House

    Thank you for making such a great tutorial. I have been able to get the application working both locally and hosted on Heroku using RedisCloud. I would like to be able to provide different styling for messages created by other users. A more iMessage feel to the chat room. This is not a problem when the page first loads to use some logic like ` `. However, on an appended message I’m having much more difficulty making this happen. I would love any suggestions/recommendations for how to go about this.

    • Ilya Bodrov

      Why so? You always can know current user’s id that way or another, for example by passing it from the server to the script.

      • Damian House

        Thank you for the reply. I finally got it! I changed broadcasting that whole partial to just the message and applied different styling based on user by creating matching logic from the chat room show page to the JS.

        • Ilya Bodrov

          Good job!

        • Арсений Жарков

          I have the same problem. Tell me please how you decided it?

  • Ilya Bodrov

    Of course, that’s simply a record in the database

  • Zahid Mehmood

    Sir i have implemented this code all are working well but when i new message it does not show without refresh of tab. please give me some instruction for this.

    • Ilya Bodrov

      Seems like the message is not broadcasted. Check your rails terminal for any errors. Also make sure that when broadcasting, you’ve called your channel correctly. Also, you may use pry-rails gem to stop code execution right inside the job and call broadcast method manually, then see the result. If it does work, then employ console.log inside the CoffeeScript file for the received callback and see if this callback actually runs.

      • Zahid Mehmood

        Sir i checked that there is no error in rails terminal and channel is properly called.job is working properly.

      • disqus_bb2ExBPvfJ

        I have the same problem, I cloned code from Github so there aren’t any mistakes. I have to refresh the page to see new messages. Any hints? :)

  • Adsidera

    Hello, I could setup the App, I can create a new chatroom, but whenever I try to send a message, in a new or already created chatroom, I receive the error “No routes matches [POST] “/chat_rooms/(id)” .Isnt it maybe because there is no clear edit/update method defined in the chat_room controller?

    • Ilya Bodrov

      Most likely events are not processed. Check browser’s console for errors, check that submit event of the form does fire.

      • Adsidera

        thanks, I will do it :)

        • Pedro Araujo

          Hi there, did you find a way around this error? Could you please describe it?

      • Adsidera

        there are no errors when I check the event listeners of the browser’s console… :(

        • Ilya Bodrov

          Use console.log in several places inside Coffee file, place it after document is loaded, when even is processed etc. It’s very hard to say for me what’s going on but still I am pretty sure that it is related to events

    • vishal saroha

      go to application.js.keep the //= require lines in order as shown on github repo.i.e. //= require jquery2 is followed by //= require jquery_ujs and then comes //= require cable. i hope this works for you.

  • Julian Chan

    hi
    I didn’t fork from your git hub but i rather i tried to follow your above instructions. However, I am unable to render @chat_rooms….
    Please teach me.

    • Ilya Bodrov

      Hello! That’s a very vague question. What is your error? What is your code? Are you sure you named everything properly? I believe this is something simple but without knowing at least some details I can’t help

  • Julian Chan

    Hi, my current error is
    ActionController::InvalidAuthenticityToken in Devise::SessionsController#create

    • Ilya Bodrov

      Well, seems that this has nothing to do with this article. Without seeing the code I can’t say for sure what’s the problem

  • Julian Chan

    Can I email u my github for u to take a look?

    • Ilya Bodrov

      Yeah, sure

  • https://github.com/artemgurzhii/ Artem Gurzhii

    Thanks for such a great post. And if someone not using jQuery, there is implementation in vanilla JS.
    `document.addEventListener(‘turbolinks:load’, () => {

    const messages = document.querySelector(‘#messages’);
    const new_message = document.querySelector(‘#new_message’);
    let textarea = new_message.querySelector(‘#message_body’);
    const updateMessages = () => messages.scrollTop = messages.scrollHeight;

    if (messages) {
    updateMessages();
    App.global_chat = App.cable.subscriptions.create({
    channel: ‘ChatRoomsChannel’,
    chat_room_id: messages.dataset.chatRoomId
    }, {
    connected() {},
    disconnected() {},
    received(data) {
    messages.innerHTML += data[‘message’];
    return updateMessages();
    },
    send_message(message, chat_room_id) {
    return this.perform(‘send_message’, {
    message,
    chat_room_id
    });
    }
    });
    return new_message.addEventListener(‘submit’, e => {
    if (message_body.value.trim().length > 1) {
    App.global_chat.send_message(textarea.value, messages.dataset.chatRoomId);
    textarea.value = ”;
    }
    e.preventDefault();
    });
    }
    });
    `

    • Ilya Bodrov

      My thanks! Though it is ES6, so people who don’t use it yet should watch out :)

  • Арсений Жарков

    Thank you so much!

  • Cruz Núñez

    I was wondering how to send the room id to the channel and your tutorial gave me the answer. Thank you.

    • Ilya Bodrov

      No problems!

  • Alejandro Garcia

    This is the error i get after posting something!
    No route matches [POST] “/chat_rooms/1”

    • Ilya Bodrov

      Many people experience this and most likely something is wrong with your events. Use browser’s console to debug.

      • vishal saroha

        Thanks for the tutorial sir. i was facing the same problem.then find out that problem is with the order of lines (//= require) in the application.js file.

    • Hunain Kapadia

      Hello, have you had any luck with this error? I keep getting the same error too.

    • vishal saroha

      go to application.js.keep the //= require lines in order as shown on github i.e. //= require jquery2 is followed by //= require jquery_ujs and then comes //= require cable. i hope this works for you.

    • Imran

      lolz – just saw after posting answer that i made comment after a whole month :D – but please bear with me so that it could help some other new guy –

  • Mayank Prajapati

    Hi everyone,
    I have a problem here. I have 4 users who want to chat at the same time. Till the time websocket connection are fine chatting goes well, but as soon as one one of the user’s websocket gets disconnected and reconnects after some. time that user loses all the messages sent by others in that duration of time. Is it possible to get all missed messages as soon as websocket reconnects. currently on re connection only new message appears not old one.

    • Ilya Bodrov

      Are you sure your messages are persisted in the database?

  • Robert Dalin

    Great Article, I’m having trouble having the messages show without refreshing the browser. I believe the problem has to do with my rails application not connecting with redis. Do you have some additional resources I could look at to help me figure out how to connect actioncable to redis? Here is the console information I am seeing when I submit a message:

    Performed MessageBroadcastJob from Async(default) in 35.57ms # I suspect it should be saying Redis instead of Async.

  • Andreas

    This is a very good tutorial! i just started to get into RoR and this was very helpful! :) i just have one question, everything is working up to the point where i add chat rooms. The chat rooms are added but i can’t see them, its just a black dot there and nothing else. any idea what i might have done wrong or what might have happened? :)

    • Ilya Bodrov

      You are welcome! Are you sure you are rendering everything properly? Like don’t forget that in order to display something you must use `<%=` not `<%`. If that does not happen, could you share you repo maybe? I'll take a look.

      • Andreas

        Thank you! that fixed the problem^^ had some line that was just <% and not <%=. Thanks for the help! ^^ but right now I've encountered a new problem, when i try to send a message i get an error.
        ActionController::RoutingError (No route matches [POST] "/chat_rooms/1"): but I'm gonna check my code a little bit more ^^

        • Ilya Bodrov

          Check the JS/CoffeeScript. It seems like an event is not processed because the form must not be send as usual and you should not be redirected to another page when sending a message

  • Samantha Cabral

    Hi, this is a great tutorial! Do you have any ideas on how to test this? I was trying to do a feature test with RSPEC, but then I get the ” No route matches [POST] “/” ” error. When I test with the browser, it all works great! Thanks!

    • Ilya Bodrov

      You are welcome! Your specs must include JS support in order for everything to work (selenium gem enables such support). Unfortunately, tests tagged with JS are much slower as they are not using Rack for testing.

  • Víctor Casas San Miguel

    Thank you for sharing so motivational reading about ActionCable. But… so long and clear instructions would be optimal if you complete them with the right way to push the app to Heroku, because it took me half an hour to run it locally and an hour to run it on Cloud9, but I see I wasted a whole day yesterday trying to figure out how to deploy it to Heroku… :-(

    • Víctor Casas San Miguel

      Ok, I cloned the github project and pushed it to heroku and it worked this time, but… this time I didn’t forget to run the migrations on heroku, what it seems it did the trick for my issue:

      Thanx for sharing! :-)
      (I’ll check the first try again, running migrations on heroku, of course, and I’l tell you the result, but only if it doesn’t work)

      • Víctor Casas San Miguel

        The first tried code, copied from this article, finally worked on heroku, as it did the cloned code from github repository, as referred before!
        I have to adapt Gemfile (adding sqlite3 and pg gems for groups development and production) and remove the initializer redis.rb, that it was innecesary. Happy coding with Rails!

    • Ilya Bodrov

      You are welcome! I understand your concern but unfortunately this article is not meant to explain how to push Rails app to c9 or Heroku. I have a few dozens of articles on Rails only on this website and most of them come with working demo. It would be quite tedious to present the same instructions for deploying them to Heroku/DigitalOcean/Linode and ultimately quite pointless. There are many great guides out there that explain the deployment process step-by-step and actually Heroku’s official website have great docs as well. Sometimes I do present some deployment instructions but only if the application has to be published to production in some unusual way or requires special setup. Still, if you need some help you can always contact me directly!

  • Bun Houth

    How about server requirement?
    AWS EC2 1GB Ram is enough?

    • Ilya Bodrov

      Yes, for medium not-so-popular sites

      • Bun Houth

        Thanks you

Recommended
Sponsors
Get the latest in Ruby, once a week, for free.