Rails 6 ActionCable Navigation & Turbolinks
Ethan Gustafson
Posted on November 3, 2020
Table of Contents:
This is part two of navigating ActionCable. In the last blog, I configured ActionCable connections and channels. Streams could start, on the condition that there is a project param
id
in the URL.
But there was a big issue: Streams wouldn't start unless a user purposefully reloaded the page on a projects#show
route. Users should be able to visit that route and have the stream start immediately.
What's going on? A stream must start based on whether or not it found a project instance. No Project.find_by_id
method was called between page visits. Page visits didn't send requests to the server.
When are ActionCable methods called, and how can we make sure that those methods run when we need them to?
ActionCable LifeCycle
When a page loads, that is when ActionCable begins calling its methods. A request is sent to the server. A page-load is different than a page visit.
A page visit is when a user visits a link and no page load happens. The navigated page appears in the DOM, but the entire page didn't load from scratch. This is what a single page application does.
Rails uses JavaScript Turbolinks. Turbolinks allow a Rails application to perform as a single page application without the client-side JavaScript framework. Because of this, ActionCable methods will not run when we need them to. To get past that, we can turn off Turbolinks or purposely trigger page-loads.
connection.rb
When a user opens their browser and navigates to the website, that is when the server will start firing off Action Cable methods. There are two main methods in it: connect
and disconnect
. A private third method is used to find the current_user
.
connect
This is where the Connection current user is set. This connection becomes the parent to all channel subscriptions the current user subscribes to. When a user navigates to the website, ActionCable will begin the process of creating the connection between client and server.
# app/channels/application_cable/connection.rb
def connect
self.current_user = find_verified_user
end
Since I am using devise
, I'm finding the current user through warden
.
# app/channels/application_cable/connection.rb
def find_verified_user
if verified_user = env['warden'].user
verified_user
else
# You can find the reject_unauthorized_connection method
# here -> https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/connection/authorization.rb
reject_unauthorized_connection
end
end
disconnect
In this method, you would do any cleanup work when the connection is cut.
# app/channels/application_cable/connection.rb
def disconnect
close(reason: nil, reconnect: true)
end
The close
method can be found here in the repo.
# rails/actioncable/lib/action_cable/connection/base.rb
# Close the WebSocket connection.
def close(reason: nil, reconnect: true)
transmit(
type: ActionCable::INTERNAL[:message_types][:disconnect],
reason: reason,
reconnect: reconnect
)
websocket.close
end
channel.rb
We don't need to do anything in this file.
comments_channel.rb
This is the channel I generated. This is where users can subscribe to streams. Channels generated inherit from class ApplicationCable::Channel
.
subscribed
If there is a project, start a stream, else reject the subscription.
# app/channels/comments_channel.rb
def subscribed
project = Project.find_by_id(params[:id])
if project
stream_for project
else
reject
end
end
receive(data)
This method is used when you rebroadcast a message. I will not be doing any rebroadcasting in my application, so this method is blank.
You would send data from the javascript channel back to the ruby channel. That data will go to the receive method, where it will be broadcasted to other users. It will also be broadcasted to the user who sent the message to be rebroadcasted.
unsubscribed
This is where you do any cleanup when a subscriber unsubscribes. By using the stop_all_streams
, all streams with the channel will be cut.
# app/channels/comments_channel.rb
def unsubscribed
# stop_all_streams -> Unsubscribes all streams associated with this channel from the pubsub queue
stop_all_streams
end
javascript/channels/comments_channel.js
This is where you will manipulate the DOM with data sent from the server.
connected()
If there is work you would like to implement when the user is connected to a stream, this is where you'll put it.
For example, when a user is connected to the stream, I display a message on the screen stating that they are connected. In ten seconds, the message disappears.
// app/javascript/channels/comments_channel.js
connected() {
// Called when the subscription is ready for use on the server
var count = 9;
const projectsNav = document.querySelector("#projects-nav");
// connectedMessage appears as the first child element of the project nav links header
const connectedMessage = document.createElement("p");
connectedMessage.id = "welcome-message";
connectedMessage.innerHTML = `Welcome to this project's stream! Comments will display in real time. Removing in ${count}...`;
// The insertAdjacentElement() method of the Element interface inserts a given element node at a given position relative to the element it is invoked upon
projectsNav.insertAdjacentElement("afterend", connectedMessage);
var countDown = setInterval(() => {
connectedMessage.innerHTML = `Welcome to this project's stream! Comments will display in real time. Removing in ${count}...`;
count === 0 ? clearInterval(countDown) : count--;
}, 1000);
setTimeout(() => {
connectedMessage.remove();
}, 10000);
}
received(data)
When data is sent from the server, it is captured here. You can do whatever you wish with this data. In my received
function, I implement a switch
statement using the data's action
from the server that determines which function will run next.
// app/javascript/channels/comments_channel.js
received(data) {
// Called when there's incoming data on the websocket for this channel
switch (data.action) {
case "create":
let containerDiv = document.createElement("div");
containerDiv.id = `comment_${data.id}`;
this.createComment(containerDiv, data);
break;
case "update":
this.updateComment(data);
break;
case "destroy":
this.deleteComment(data.id);
break;
case "error":
this.handleError(data);
break;
default:
console.log("No match found");
}
}
appendComment(data)
This is a method I created that handles appending new data to the DOM. The only methods ActionCable provides are connected()
, disconnected()
, and received()
Views
We are able to purposefully trigger page loads by turning off Turbolinks on anchors.
JavaScript TurboLinks
JavaScript Turbolinks enable a Rails application to act as a single page application, where page visits will swap out the body
and merge the head
so that full-page loads don't happen.
link_to
link_to
allows options of disabling the Turbolink on an a
tag. This ensures a page load occurs.
Turbolinks can be disabled on a per-link basis by annotating a link or any of its ancestors with
data-turbolinks="false"
<%= link_to project.name, project, data: {turbolinks: "false"} %>
Visiting a URL will also cause a page load.
Posted on November 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.