loading...
Cover image for Rails 6 ActionCable Navigation & Turbolinks

Rails 6 ActionCable Navigation & Turbolinks

ethanmgustafson profile image Ethan Gustafson ・5 min read

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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");
  }
}
Enter fullscreen mode Exit fullscreen mode

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"} %>
Enter fullscreen mode Exit fullscreen mode

Visiting a URL will also cause a page load.

Discussion

pic
Editor guide