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_changes
on 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-controller
data 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)