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)