Simple chatroom with Rails 6 and ActionCable
Tags: ruby, rails, beginners, tutorial
Published: 2020-09-18 17:18:11

---
title: Simple chatroom with Rails 6 and ActionCable
published: true
description: 
tags: #ruby #rails #beginners #tutorial
//cover_image: https://direct_url_to_image.jpg
---

Hello everyone,

This week, we will be building a simple chatroom with Rails 6 and ActionCable. We are going to learn how ActionCable works and how to use it in Rails. You should have a basic understanding of Ruby and Rails to be able to follow along. We won't be authenticating the user nor saving the messages to the database so its going to be like an anonymous chat room were none of the messages are persisted. 

### Introduction
According to the ruby guides: 
>Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework.

WebSockets provide a persistent connection between a client and server that both parties can use to start sending data at any time. I will include links to read more on WebSockets in the Resources section. Below are some points and explanations we need to note from the rubyguide.

* A consumer is a client of a WebSocket Connection 
* A consumer can be subscribed to multiple cable channel
* When a consumer is subscribed to a channel, they act as a subscriber
* The connection between the subscriber and the channel is called a subscription
* Each channel can then again be streaming zero or more broadcastings
* A broadcasting is a pubsub link where anything transmitted by the broadcaster is sent directly to the channel subscribers who are streaming that named broadcasting

In summary, a client initiates a WebSocket connection. This connection is like a dedication line through which you can send and receive data between the client and the server. Through ActionCable, you subscribe to a channel and you are now called a `subscriber`. Other people also create a connection and subscribe to the channel. When a subscriber sends a message, it is then broadcasted to everyone subscribed to that channel.

### Setting up the app
In the console, let's run the rails command to create a new app with postgresql database:

```
$ rails new chat_room --database=postgresql
```
Cd into the folder and create the database

```
$ cd chat_room
$ rails db:create
```
Next, let's design the page. I don't want to use a CSS library so we can focus on what's important.

Let's create a home controller with an index action.
```
$ rails g controller Home index
```
In the route file, let's change the get request to be the root path:
```ruby
# config/routes.rb

root 'home#index'
```
Add the following code to your `home/index.html.erb` file
```html
<div id="main">
  <h2>Chat room</h2>
  <div>
    <div id="messages">
      <p class="received">Hello</p>
      <p class="sent">Hi</p>
      <p class="received">How are you doing?</p>
      <p class="sent">I am doing alright</p>
    </div>
    <form id="send_message">
      <input type="text" id="message" name="message">
      <button>Send</button>
    </form>
  </div>
</div>
```

Some CSS for `application.css`
```css
body {
  padding: 0;
  margin: 0;
}

h2 {
  margin: 0;
}
```

CSS for `home.scss` file
```css
#main {
  height: 100vh;
  background-color: bisque;
  overflow: auto;

  h2 {
    text-align: center;
    margin-top: 20px;
    margin-bottom: 20px;
  }

  >div {
    height: 90vh;
    background-color: black;
    width: 80%;
    margin: auto;
    border-radius: 5px;
    overflow: auto;
    position: relative;

    div#messages {
      height: 90%;
      width: 95%;
      color: white;
      margin: 14px auto;
      overflow: auto;
      display: flex;
      flex-direction: column;

      p {
        display: inline-block;
        padding: 10px;
        border-top-left-radius: 10px;
        border-top-right-radius: 10px;
        margin-bottom: 5px;
        margin-top: 5px;
        max-width: 70%;
        width: max-content;

        &.received {
          background-color: chocolate;
          border-bottom-right-radius: 10px;
        }
        
        &.sent {
          border-bottom-left-radius: 10px;
          background-color: darkred;
          align-self: flex-end;
        }
      }
    }

    form#send_message {
      width: 95%;
      margin: auto;

      input {
        height: 30px;
        width: 90%;
        border-radius: 10px;
        border: 0;
      }

      button {
        height: 35px;
        width: 8.8%;
        border-radius: 20px;
        border: 0;
        background-color: tomato;
        color: white;
      }
    }
  }
}
```

And we should have this not-so-beatiful UI below:
![UI for the sample page just created](https://dev-to-uploads.s3.amazonaws.com/i/hvpzo3o2agqygwdruh85.png)

### Implementing room channel
Let's add jquery to the app and set that up
```
$ yarn add jquery
```

Update your webpack config to include jquery in its environment

```javascript
// config/weboack/environment.js

const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)
```
Require jquery to your `application.js` file
```javascript
// app/javascript/packs/application.js

require("jquery")
```

Next, we generate our channel
```
$ rails g channel chat_room
```
This should generate some files including `chat_room_channel.rb` and `chat_room_channel.js`. These are the 2 files we will be making our changes to.

Just to test that our channels are setup properly, let's add some minor code to our files:

```ruby
# app/channels/chat_room_channel
def subscribed
  stream_from "chat_room_channel"
end
```

```javascript
// app/javascript/channels/chat_room_channel.js

connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to the chat room!");
  },
```

Starting up your server and visiting `localhost:3000`, you should see our message logged to the console.

Let's chat a little.
Update your `chat_room_channel.js` to include a new speak method that broadcasts and update to the `received` method that displays our message. We also need to export the `ChatRoomChannel` so we can access the speak method in other javascript files.

```javascript
// app/javascript/channels/chat_room_channel.js
import consumer from "./consumer"

const chatRoomChannel = consumer.subscriptions.create("ChatRoomChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to the chat room!");
  },

  disconnected() {
    // Called when the subscription has been terminated by the server
  },

  received(data) {
    $('#messages').append('<p class="received"> ' + data.message + '</p>')
  },

  speak(message) {
    this.perform('speak', { message: message })
  }
});

export default chatRoomChannel;
```

In our `application.js`, we import the channel and add an event listener to the form. When the form is submitted, it gets the value of the input field and passes it to the `speak` method. The event listener also removes the content of the input field.

```javascript
// app/javascript/packs/application.js

// other code
import chatRoomChannel from "../channels/chat_room_channel";

$(document).on('turbolinks:load', function () {
  $("form").on('submit', function(e){
    e.preventDefault();
    let message = $('#message').val();
    if (message.length > 0) {
      chatRoomChannel.speak(message);
      $('#message').val('')
    }
  });
})
```

Reload your page, and you should be seeing your messages;
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/474gk7hx6ri0n9r2dt5a.png)

But we have a little problem. Every message is showing up on the left as received even for the message sender. We need to be able to differentiate between the sender and the receiver. Since we are not using authentication or saving to the database, we need to be able to identify each person somehow. 

I have a simple solution. We need to add a modal for everyone to add a name that will be saved to sessionStorage. I am using sessionStorage because i want the session to be cleared out once the user closes the tab and localStorage won't do that. We will also announce the user when they join or leave the channel.

Let's update our `index.html.erb` file to include the modal and remove the static messages we added earlier.

```erb
<div id="main">
  <h2>Chat room</h2>
  <div id="chat_body">
    <div id="messages">
    </div>
    <form id="send_message">
      <input type="text" id="message" name="message">
      <button>Send</button>
    </form>
  </div>
  <div id="modal">
    <div>
      <h4>Add a name</h4>
      <form id="set_name">
        <input type="text" id="add_name" name="add_name">
        <button>Submit</button>
      </form>
    </div>
  </div>
</div>
```

Let's also update the `chat_room_channel.js` file to announce when a user joins or leaves a room. We are also going to update the `received` method so we can format how each message will be displayed.

```javascript
import consumer from "./consumer"

const chatRoomChannel = consumer.subscriptions.create("ChatRoomChannel", {
  connected() {
    console.log("Connected to the chat room!");
    $("#modal").css('display', 'flex');
  },

  disconnected() {

  },

  received(data) {
    if (data.message) {
      let current_name = sessionStorage.getItem('chat_room_name')
      let msg_class = data.sent_by === current_name ? "sent" : "received"
      $('#messages').append(`<p class='${msg_class}'>` + data.message + '</p>')
    } else if(data.chat_room_name) {
      let name = data.chat_room_name;
      let announcement_type = data.type == 'join' ? 'joined' : 'left';
      $('#messages').append(`<p class="announce"><em>${name}</em> ${announcement_type} the room</p>`)
    }
  },

  speak(message) {
    let name = sessionStorage.getItem('chat_room_name')
    this.perform('speak', { message, name })
  },

  announce(content) {
    this.perform('announce', { name: content.name, type: content.type })
  }
});

export default chatRoomChannel;
```
Next is the updated css file
```css
// Place all the styles related to the Home controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/
#main {
  height: 100vh;
  background-color: bisque;
  overflow: auto;

  h2 {
    text-align: center;
    margin-top: 20px;
    margin-bottom: 20px;
  }

  >div#chat_body {
    height: 90vh;
    background-color: black;
    width: 80%;
    margin: auto;
    border-radius: 5px;
    overflow: auto;
    position: relative;

    div#messages {
      height: 90%;
      width: 95%;
      color: white;
      margin: 14px auto;
      overflow: auto;
      display: flex;
      flex-direction: column;

      p {
        display: inline-block;
        padding: 10px;
        border-top-left-radius: 10px;
        border-top-right-radius: 10px;
        margin-bottom: 5px;
        margin-top: 5px;
        max-width: 70%;
        width: max-content;

        &.received {
          background-color: chocolate;
          border-bottom-right-radius: 10px;
        }
        
        &.sent {
          border-bottom-left-radius: 10px;
          background-color: darkred;
          align-self: flex-end;
        }

        &.announce {
          align-self: center;
          font-style: italic;
          color: cyan;

          em {
            font-weight: 700;
            color: mediumorchid;
          }
        }
      }
    }

    form#send_message {
      width: 95%;
      margin: auto;

      input {
        height: 30px;
        width: 90%;
        border-radius: 10px;
        border: 0;
      }

      button {
        height: 35px;
        width: 8.8%;
        border-radius: 20px;
        border: 0;
        background-color: tomato;
        color: white;
      }
    }

  }
  div#modal {
    height: 100vh;
    position: absolute;
    top: 0;
    background-color: #000000bf;
    width: 100%;
    z-index: 2;
    display: flex;
    display: none;

    >div {
      width: 300px;
      background: white;
      margin: auto;
      padding: 30px;
      text-align: center;
      height: 150px;
      border-radius: 10px;

      input {
        height: 30px;
        border-radius: 10px;
        border: 2px dotted rebeccapurple;
        width: 100%;
        margin-bottom: 10px;
      }

      button {
        height: 35px;
        border-radius: 20px;
        border: 0;
        background-color: #673AB7;
        color: white;
        width: 80px;
      }
    }
  }
}
```

We also need to update our `application.js` to let us know when someone leaves and when someone joins.

```javascript
import chatRoomChannel from "../channels/chat_room_channel";

$(document).on('turbolinks:load', function () {
  $("form#set_name").on('submit', function(e){
    e.preventDefault();
    let name = $('#add_name').val();
    sessionStorage.setItem('chat_room_name', name)
    chatRoomChannel.announce({ name, type: 'join'})
    $("#modal").css('display', 'none');
  });

  $("form#send_message").on('submit', function(e){
    e.preventDefault();
    let message = $('#message').val();
    if (message.length > 0) {
      chatRoomChannel.speak(message);
      $('#message').val('')
    }
  });

  $(window).on('beforeunload', function() {
    let name = sessionStorage.getItem('chat_room_name')
    chatRoomChannel.announce({ name, type: 'leave'})
  });
})
```
Finally, our `chat_room_channel.rb` file needs to be updated to include the `announce` method.

```ruby
# app/channels/chat_room_channel.rb

class ChatRoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    ActionCable.server.broadcast "chat_room_channel", message: data["message"], sent_by: data["name"]
  end

  def announce(data)
    ActionCable.server.broadcast "chat_room_channel", chat_room_name: data["name"], type: data["type"]
  end
end
```

Modal to add your name
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/0cczx0wmpk4jb6pau0j6.png)

You are announced when you join the room
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/wifmbp5mz4el5o5dm2un.png)

Simple back and forth between 2 subscribers. And the user is announced when they leave the channel
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cdiaklcqt9z60d0rho3t.png)

And this brings us to the end of this tutorial. 

Let me know if you have any questions in the comment section.

Until next week.

Link to repo https://github.com/Nkemjiks/chat_room

### Resources
[Action Cable Overview - RubyGuide](https://guides.rubyonrails.org/action_cable_overview.html)
[An Introduction to WebSockets - Treehouse](https://blog.teamtreehouse.com/an-introduction-to-websockets)
[Creating a Chat Using Rails' Action Cable](https://www.pluralsight.com/guides/creating-a-chat-using-rails-action-cable)