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:
# config/routes.rb
root 'home#index'
Add the following code to your home/index.html.erb
file
<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
body {
padding: 0;
margin: 0;
}
h2 {
margin: 0;
}
CSS for home.scss
file
#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:
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
// 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
// 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:
# app/channels/chat_room_channel
def subscribed
stream_from "chat_room_channel"
end
// 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.
// 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.
// 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;
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.
<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.
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
// 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.
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.
# 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
You are announced when you join the room
Simple back and forth between 2 subscribers. And the user is announced when they leave the channel
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
An Introduction to WebSockets - Treehouse
Creating a Chat Using Rails' Action Cable
Top comments (1)
Nice post! I wanted to ask if there is a way to save the messages with ActiveRecord or if you are familiar with rebroadcasting a message via actioncable? Say someone wanted to scroll up through the chat history, is there a way for this to be available for someone joining the chat later? I was looking through rebroadcasing a message, but was getting stuck.