Skip to content →

Grabbing ActionCable with Stimulus.js

ActionCable is a great framework included in Rails that adds interactivity with WebSockets, and Stimulus.js gives you the ability to write concise sprinkles, and easily hook them into particular pages.

Where to begin?

Start with the ActionCable sample app provided by Rails. You can clone actioncable-examples, and follow the setup instructions in the README. You should get everything working, including installing the required dependencies, before you start making changes to the Javascript. This way, you will have a consistent starting point for the tutorial.

You can update the ruby version, rails, and a few of the libraries, and add in Webpacker and Stimulus.js, and the actioncable library from NPM. I tried to change as little of the ruby code as possible, in an effort to highlight how easy it can be to refactor existing front end Javascript to use Stimulus.

This Evil Martians article can help with setting up ActionCable in webpacker.

Adding the Controller

Each message is going to have a Stimulus controller associated with it. This means that as the controller is loaded, it will need to set up the ActionCable connection, and then handle incoming messages and append the new comment. You will set up a comments target where the controller will add the incoming comments. The Stimulus controller’s life cycle will help handle listening on a particular message’s comments, and unfollowing that message when the controller is disconnected.

The HTML

You’ll need to update app/views/messages/show.html.erb with the necessary controller and message data:

<div data-controller="messages" data-messages-id="<%= @message.id %>">
  <h1><%= @message.title %></h1>
  <p><%= @message.content %></p>
  <%= render 'comments/comments', message: @message %>
</div>

In the comments partial, app/views/comments/_comments.html.erb, add the target annotation:

<section id="comments" 
  data-channel="comments" 
  data-message-id="<%= message.id %>" 
  data-target="messages.comments">

The Controller

The messages_controller.js handles setting up the ActionCable channel initially, and connecting to the channel every time the commentator visit a different message page.

First import the required dependencies:

import { Controller } from "stimulus"
import createChannel from "cables/cable";

Then, set up the comments target:

export default class extends Controller {
  static targets = [ "comments" ]

The controller’s initialize() method is going to set up the ActionCable channel. connected() in the ActionCable subscription will call the controller’s listen function, which connects the message on the page with future comments that are pushed up to the page. received() handles data from the websocket. It creates a DOM element from the string sent over the wire, and verifies that the user of the comment is not the user who just posted the comment before appending the it. It might make more sense to send the user id as a separate value as well, but it’s not necessary with DomParser.

  initialize() {
    let commentsController = this;
    this.commentsChannel = createChannel( "CommentsChannel", {
      connected() {
        commentsController.listen()
      },
      received(data) {
        let html = new DOMParser().parseFromString( data['comment'] , 'text/html');
        const commentHTML = html.body.firstChild;
        if (getCurrentUserId() != commentHTML.getAttribute('data-user-id'))  {
          commentsController.commentsTarget.insertAdjacentElement('beforeend', commentHTML );
        }
      }
    });

  }

The controller’s connect() method also calls listen(). The need for two different listen() calls has to do with a condition where on a page refresh, the ActionCable connection isn’t done loading by the time the controller’s connect() function is called. But on subsequent page loads using Turbolinks, the ActionCable subscription is technically still connected, so ActionCable won’t call it’s version of connect() again, and the controller will call listen().

  connect() {
    this.listen()
  }

When the controller is removed from the page, likely because the commentator is moving to another page, the controller stops following the message’s comments:

  disconnect() {
    this.commentsChannel.perform('unfollow')
  }

Here is the listen() function. It calls perform on the ActionCable subscription, which is sent over the socket to our ruby server. Stimulus’ data attributes feature get the message id easily.

  listen() {
    if (this.commentsChannel) {
      this.commentsChannel.perform('follow', { message_id: this.data.get('id') } )
    }
  }
}

This function gets the current user’s id that is stored in the head of the page. This is based off of another post, Where do I store my state in Stimulus?

function getCurrentUserId() {
  const element = document.head.querySelector(`meta[name="current-user"]`)
  return element.getAttribute("id")
}

Conclusion

I hope this helps you see how Stimulus can interact with other Javascript components in our app. ActionCable neatly hides a lot of the complexities of WebSockets, and Stimulus neatly integrates with ActionCable to manage connecting and disconnecting to different channels when we need them. This will help us by cutting down on extra data being sent over the wire when our app doesn’t need it.

All the code can be found here on Github: https://github.com/johnbeatty/actioncable-examples

Comments or Questions? Find me on twitter @jpbeatty

Want To Learn More?

Try out some more of my Stimulus.js Tutorials.

Published in ruby on rails Stimulus JS Tutorial

Comments are closed.