Rails 6 ActionCable Navigation & Turbolinks
Tags: rails, javascript, html
Published: 2020-11-03 12:26:25

# Table of Contents:

* [ActionCable LifeCycle](#actioncable-lifecycle)
    * [`connection.rb`](#-raw-connectionrb-endraw-)
        * [`#connect`](#-raw-connect-endraw-)
        * [`#disconnect`](#-raw-disconnect-endraw-)
    * [`channel.rb`](#-raw-channelrb-endraw-)
    * [`comments_channel.rb`](#-raw-commentschannelrb-endraw-)
        * [`#subscribed`](#-raw-subscribed-endraw-)
        * [`#receive(data)`](#-raw-receivedata-endraw-)
        * [`#unsubscribed`](#-raw-unsubscribed-endraw-)
    * [`javascript/channels/comments_channel.js`](#-raw-javascriptchannelscommentschanneljs-endraw-)
        * [`#connected()`](#-raw-connected-endraw-)
        * [`#received(data)`](#-raw-receiveddata-endraw-)
        * [`#appendComment(data)`](#-raw-appendcommentdata-endraw-)
    * [Views](#views)
        * [JavaScript TurboLinks](#javascript-turbolinks)
        * [`link_to`](#-raw-linkto-endraw-)
        * [Visiting a URL](#visiting-a-url)

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](https://guides.rubyonrails.org/working_with_javascript_in_rails.html#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.

```ruby
# 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`.

```ruby
# 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.

```ruby
# app/channels/application_cable/connection.rb
def disconnect
  close(reason: nil, reconnect: true)
end
```

The `close` method can be found here in the [repo](https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actioncable/lib/action_cable/connection/base.rb#L98). 

```ruby
# 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.

```ruby
# 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.

```ruby
# 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.

```js
// 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.

```js
// 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](https://github.com/turbolinks/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"`

```ruby
<%= link_to project.name, project, data: {turbolinks: "false"} %>
```

**Visiting a URL will also cause a page load.**