Creating a “Drawing With Friends” Web App w/ Action Cable and Rails 5

Dean Watts
Jun 9, 2016 · 7 min read

The next version of Rails is around the corner, and with it comes support for WebSockets via ActionCable, something that has been described as “the highlight of Rails 5.” But how powerful is ActionCable? And what is it exactly?

WebSockets

For an indepth description of what WebSockets are, and how to properly set up a Rails 5 app with basic chat room functionality, I highly recommend my instructor Sophie’s blogpost on the subject. It got featured on Heroku’s blog and can be found here:

To summarize, WebSockets function differently from the normal HTTP web protocol (where a client makes a request for data from the server, and the server responds with the data). WebSockets allow, after an initial connection from a client, for your server to continually send and receive data to the client.

An analogy would be that conventional HTTP requests function like a 17th century ship sending mail between Britain and the Americas, while WebSockets function like sending messages in a 10 way Skype call with people from all over the world.

Time for the web to get out of the 17th century

WebSockets are powerful, and fortunately, relatively easy to include in your Rails 5 applications! In this post I will be walking through the creation of a simple “Drawing With Friends” application that utilizes the power of WebSockets and AJAX.

The Goal

This was captured on my laptop, while I was drawing on my phone.

The gif shown above, while appearing unimpressive, is actually a recording of my laptop’s screen as I drew on my website from my phone. It looks like it is happening live and on the same device! This is what we will be building today.

The App Setup

A lot of this will be worded similarly to Sophie’s blogpost above, as if it wasn’t for that post I would have no idea how to accomplish anything with WebSockets.

To start, I am assuming you have a Rails 5 Application created and ready to go. To be sure, open your Gemfile and make sure it has the following gems:

gem ‘rails’, ‘>= 5.0.0.rc1’, ‘< 5.1’
gem 'puma', '~> 3.0'
gem 'redis', '~> 3.0'

Don’t forget to bundle install!

Our app is going to be very simple, and will not be utilizing the Model View Controller Pattern to its fullest. In fact, we will only be using 1 controller, 1 view, and no models!! However, once understood, you can take this very basic example and expand and implement the concepts into very powerful web apps.

The Socket End Points

So let’s start by setting up our socket and our pages routes.

# /config/routes.rbRails.application.routes.draw do
mount ActionCable.server => ‘/cable’
get “/”, to: “line#index”
post “/updateline”, to: “line#show”
end

The mount action connects ActionCable to the “/cable” route, so now ActionCable will listen for requests for clients on “/cable”. The GET “/” route will render our index page with a drawing canvas, and the POST “/updateline” route is where we will be sending our client’s data as they draw on our app.

So now we have an empty pipe, waiting for our users, but no way for them to connect to it!

In order to do this we need to create a new javascript file. This javascript file will create the connection to the ActionCable WebSocket, and subscribe to any responses that the Cable may send our client’s way. I called mine “liveline.js”

# /app/assets/javascripts/channels/liveline.js
//= require cable
//= require_self
//= require_tree .
this.App = {};App.cable = ActionCable.createConsumer();App.messages = App.cable.subscriptions.create('LineChannel', {
received: function(data) {
#PARSE DATA LOGIC HERE
}
});

We then need to tell our main application.js file that this new one exists:

# /app/javascripts/application.js...
//= require_tree ./channels
//= require_tree .

Woah woah woaaaaah! I just dropped a lot of code on you. No worries! I’ll explain.

So the first few lines are sprockets to include the related and necessary cable files, this allows us to utilize ActionCable methods in our file. We then create an App object, and set up a “cable” property from the ActionCable.createConsumer() method. The next few lines subscribe to a “LineChannel” stream, and upon receiving some data execute a function where some of our logic will go.

This does not look like our routes.rb file! We aren’t telling it to go to a “/cable” route! This is because in our development and our production environments, “/cable” means very different things thanks to redis. But don’t worry! We just need to specify the url in our config file, and add the data to our head.

# /config/development.rb...Rails.application.configure do 
config.action_cable.url = "ws://localhost:3000/cable"
end
...=======================================================# app/vippews/layouts/application.html.erb<head>
...
<%= action_cable_meta_tag %>
...
</head>

Great! We now have a connection between our clients and our cable!

A satisfied client receiving messages from a WebSocket.

The Channel Between the End Points

So we have a connection, but now we need to connect an incoming connection to the data we broadcast to our clients. We can set up the highway that the data will be sent down in 5 simple lines!

# /app/channels/application_cable/line_channel.rbclass LineChannel < ApplicationCable::Channel
def subscribed
stream_from ‘lines’
end
end

And then to send the trucks (data) down the ‘lines’ highway in our controller. Remember the “/updateline” route we made that got sent to the “show” method in the line controller?

# /app/controllers/line_controller.rbdef show
ActionCable.server.broadcast ‘lines’,
#DATA FROM PARAMS[]
head :ok
end

And now we have our completed connection! Time to draw things!

Some clients connected to our WebSocket

The Canvas

Let’s start with the canvas. This is the easiest part of the whole app.

# /app/views/line/index.html.erb<canvas id=”paper” width=”1900" height=”1000"></canvas>

That’s it! That’s all we need. We now have a 1900x1000 canvas on our page with an id of “paper”.

Let’s get that paper to do stuff, let’s do some javascript. I put the following code in liveline.js, but you can put it anywhere that gets imported.

# /app/assets/javascripts/channels/liveline.js...
$(function(){
//vars that should be globally accessed, the document, the canvas, and the canvas context. doc = $(document),
canvas = $(‘#paper’),
ctx = canvas[0].getContext(‘2d’);
//local vars, previous coords for the client, a random color, a boolean stating if the client is drawing or not, and the time since we last told the server we were drawing. var prev = {};
var color = '#'+(Math.random()*0xFFFFFF<<0).toString(16);
var drawing = false;
var timeSinceLastSend = $.now();
//event to fire if mouse down or touch on canvas element
canvas.on(“mousedown touchstart”, function(e){
e.preventDefault();
//get coords if mouse down
var x = e.pageX;
var y = e.pageY;
//get coords if touch
if ( e.originalEvent.changedTouches ) {
e = e.originalEvent.changedTouches[0];
x = e.pageX;
y = e.pageY;
}
//set drawing to true and the coordinates for the prev object
//the prev object is used for the start of the drawing line
drawing = true;
prev.x = x;
prev.y = y;
});
//event to fire if the mouse is released/left or touched up
doc.bind('mouseup mouseleave touchend',function(){
drawing = false;
});
//event to fire as the mouse moves or finger is dragged
doc.on(‘mousemove touchmove’,function(e){
//if we are drawing, and its been over 10ms since last update
if(drawing && $.now() — timeSinceLastSend > 10){
//get mouse coords
var x = e.pageX;
var y = e.pageY;
//if touching, get touch coords
if ( e.originalEvent.changedTouches ) {
e = e.originalEvent.changedTouches[0];
x = e.pageX;
y = e.pageY;
}
//create ajax request to /updateline
//data is prev coordinates and current coordinates and color
$.ajax({
method: “POST”,
url: “/updateline”,
data: {
‘fromx’: prev.x,
‘fromy’: prev.y,
‘tox’: x,
‘toy’: y,
‘color’: color
}
});
//reset time since last send
timeSinceLastSend = $.now();
}
//draw the line and reset prev
if(drawing && x && y){
drawLine(prev.x, prev.y, x, y, color);
prev.x = x;
prev.y = y;
}
});
});
//function to draw a line
function drawLine(fromx, fromy, tox, toy, color){
ctx.beginPath();
ctx.strokeStyle = color
ctx.moveTo(fromx, fromy);
ctx.lineTo(tox, toy);
ctx.stroke();
}

Try it out! You should be able to draw on your screen. However, we are still missing the last step. The code above sends the data to the server, so now we need to do the final touches to send the data to all the clients on our WebSocket. So back in our liveline.js file, make this change to the ActionCable code we added earlier:

# /app/assets/javascripts/channels/liveline.jsApp.messages = App.cable.subscriptions.create(‘LineChannel’, {
received: function(data) {
drawLine(data.fromx, data.fromy, data.tox, data.toy, data.color)
}
});

And in our controller lets give it some data!

# /app/controllers/line_controller.rbdef show
ActionCable.server.broadcast ‘lines’,
fromx: params[:fromx],
fromy: params[:fromy],
tox: params[:tox],
toy: params[:toy],
color: params[:color]
head :ok
end

Woohoo! We should now have a working WebSocket drawing app! Test it out by firing up the rails server, opening a few windows, and drawing around!

Cool!

I hope this was helpful in understanding the awesomeness of WebSockets, and how easy it is to work with ActionCable and AJAX.

Production Environment

NOTE: To deploy this app onto heroku/a production environment you need to set up a few production steps. Please read the blog below for an indepth tutorial on the rest of the steps:

Dean Watts

Written by

Software Developer in Manhattan. Passion to learn and teach about the power of programming.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade