loading...

StimulusJS with Rails Action Cable and a bit of Sidekiq

dstull profile image Doug Stull Updated on ・4 min read

Goal

Show a simple implementation of Rails Action Cable with the modest js framework named StimulusJS.

For this tutorial, I will be using a simple demo Rails application, which you can find the source code for here.

I am going to gloss over the particulars of Sidekiq here, and focus on the StimulusJS and Action Cable pieces. I believe that will be most valuable here, as the other pieces have been covered many times on numerous blogs and tutorials. However, I may revisit the other items at a later date.

I'll break this down into 2 steps:

  • Create Sidekiq worker and Action Cable Channel
  • Setup StimulusJS

Create Sidekiq worker and Action Cable Channel

For this part, I want to show how an index page listing cars that have many drivers can be updated when a driver's name changes or the particular car they belong to.

To accomplish that for this example, I decided to make the Sidekiq job trigger off and after_touch callback on the Car model.

Car/Driver Models - the Active Record Models

class Car < ApplicationRecord
  has_many :drivers, dependent: :destroy

  after_touch :update_driver_names

  def drivers_list
    drivers.pluck(:name).join(',')
  end

  private

  def update_driver_names
    CarsWorker.perform_async(id)
  end
end

class Driver < ApplicationRecord
  belongs_to :car, touch: true

  delegate :name, to: :car, prefix: true
end

In the above file, I am triggering the after_touch callback named update_driver_names on the Car model by adding touch: true to the Driver model.
The update_driver_names method reaches out to Sidekiq and calls an async job called CarsWorker.perform_async, sending the id of the Car that the Driver has assigned.

CarsWorker (cars_worker.rb) - the Sidekiq Worker

class CarsWorker
  include Sidekiq::Worker

  def perform(car_id)
    # some contrived work...
    car                = Car.find car_id
    new_driver_changes = car.driver_changes + 1
    car.update_attribute(:driver_changes, new_driver_changes)

    car_drivers = car.drivers_list
    ActionCable.server.broadcast('cars', drivers: car_drivers, car_id: car_id, driver_changes: new_driver_changes)
  end
end

In the above file, I am:

  1. Incrementing the driver_changes on the driver's car and committing that on the car.
  2. Finding all the drivers for that car and broadcasting out to the Action Cable channel the new drivers list as a string in car_drivers, along with the number of driver_changes.

Here is an example of the transmission from the CarsChannel:

CarsChannel transmitting {"drivers"=>"Jacalyn Bauchblah,Wes Goodwin,Guy Keeling,Miss Pasquale Doyle,Candy Welch", "car_id"=>23, "driver_changes"=>4} (via streamed from cars)

CarsChannel (cars_channel.rb) - the Action Cable Channel definition

class CarsChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'cars'
  end
end

The above is merely the standard boilerplate channel definition that is defined here.

Setup StimulusJS

For this part I will show the HTML erb pieces and the StimulusJS setup.

Cars Index (cars/index.html) - the HTML piece

<p id="notice"><%= notice %></p>

<div class="page-header" data-controller="cars">
  <h1>Cars</h1>
</div>

<table class="table table-hover">
  <thead>
  <tr>
    <th>Name</th>
    <th>Drivers</th>
    <th>Driver Changes</th>
    <th>Make</th>
    <th>Color</th>
    <th>Model</th>
    <th colspan="3"></th>
  </tr>
  </thead>

  <tbody>
  <% @cars.each do |car| %>
    <tr id="car_id_<%= car.id %>">
      <td><%= car.name %></td>
      <td class="cars--drivers"><%= car.drivers_list %></td>
      <td class="cars--driver-changes"><%= car.driver_changes %></td>
      <td><%= car.make %></td>
      <td><%= car.color %></td>
      <td><%= car.model %></td>
      <td><%= link_to 'Show', car %></td>
      <td><%= link_to 'Edit', edit_car_path(car) %></td>
      <td><%= link_to 'Destroy', car, method: :delete, data: { confirm: 'Are you sure?' } %></td>
    </tr>
  <% end %>
  </tbody>
</table>

<br>

<%= link_to 'New Car', new_car_path %>

The important part to point out in the above is the data-controller data attribute. Setting this to the name of the StimulusJS controller, cars, will then cause it to reach out and invoke the cars_controller.js connect function upon page render; subscribing the user to the cars action cable channel. There is documentation on the StimulusJS website explaining how that part works.

Cars Controller (cars_controller.js) - the StimulusJS Controller

import { Controller } from 'stimulus';
import createChannel from '../exports/cable';

export default class extends Controller {
  connect() {
    this.initChannel();
  }

  initChannel() {
    createChannel('CarsChannel', {
      received(data) {
        const carRow = $(`#car_id_${data.car_id}`);
        const driverChanges = carRow.find('.cars--driver-changes');
        const drivers = carRow.find('.cars--drivers');
        driverChanges.text(data.driver_changes);
        drivers.text(data.drivers);
      },
    });
  }
}

In the above, we:

  1. Importing the basic cable setup from exports/cable.js
  2. Init the Channel from the connect function, which is fired whenever we land on the cars index page due to the data-controller data attribute.
  3. Update the cars index page when a messages is received on the CarsChannel.

Cable Javascript (cable.js) - the basic Action Cable JS setup that is imported when needed

import cable from 'actioncable';

let consumer;

export default function (...args) {
  if (!consumer) {
    consumer = cable.createConsumer();
  }

  return consumer.subscriptions.create(...args);
}

The above is the initial/standard Action Cable setup. I went the 'extra mile' here and also used yarn to install actioncable, ensuring it was the same version as Rails. This helps keep me completely out of the asset pipeline/sprockets area.

In Closing...

I particularly wanted to get completely out of the Rails asset pipeline/sprockets setup and be page specific about my channel subscription.

I hope this short demo is helpful to someone. I searched many places to gather the bits and pieces of how to string this together, and felt I should share with the community that I have benefited so much from myself. I had only a few hours to throw this together before I had to get back to being a parent :) ...perhaps later I will add a blog on how I went about implementing testing all of this from soup to nuts.

The basis for some of this work came from this wonderful blog by Evil Martians.

Posted on by:

Discussion

markdown guide