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:
- Incrementing the 
driver_changeson the driver's car and committing that on the car. - 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 ofdriver_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:
- Importing the basic cable setup from 
exports/cable.js - Init the Channel from the connect function, which is fired whenever we land on the cars index page due to the 
data-controllerdata attribute. - 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.
    
Top comments (0)