One thing you'll find yourself doing when working with CableReady is rendering HTML in places you might not previously have done so.
It's possible to use Rails for a while before you realize that the render
method might be useful for more than just partials. Perhaps you need to render some JSON or a text string in your controller, but sooner or later you become aware that Rails is calling render
for you at the end of most Rails controller actions.
You can call render
from almost anywhere, using ApplicationController.render
... which just is long enough to get annoying if you type it a lot.
Experience has taught us that shorter calls lead to code that is easier to read and reason about, so we often delegate the render
method to ApplicationController
so that we don't have to type "ApplicationController" over and over.
In the sections below, you'll learn how to configure ApplicationController
, ApplicationRecord
and ApplicationJob
so that they make working with CableReady a breeze.
First, we recommend that you include CableReady in your ApplicationController
:
app/controllers/application_controller.rbclass ApplicationController < ActionController::Baseinclude CableReady::Broadcasterend
Controller actions that handle Ajax requests, as well as web hooks and OAuth endpoints are great places to call CableReady. It's also common to broadcast CableReady operations to groups of users and/or resources inside controller actions.
If you perform a CableReady broadcast during a standard page controller action, it will send the broadcast immediately; before the action has completed, before the view template has been rendered and before the HTML has been sent to the client. This can lead to developers becoming convinced (incorrectly) that the broadcast did not work.
If you need the user executing the controller action to see the broadcast, you should use an ActiveJob that has been delayed for a few seconds using the set method. There's also a good example of using Stimulus to provide an elegant solution to group update issues.
Fans of Hotwire Turbo Streams will be excited to know that it is easy to use CableReady with standard Rails controller actions. Here's how to do it:
<%= link_to "Console message", "users/#{current_user.id}/message", method: :patch %>
config/routes.rbpatch 'users/:id/message', to: 'users#message', constraints: lambda { |request| request.xhr? }
app/controllers/users_controller.rbclass UsersController < ApplicationControllerdef messagecable_ready[UsersChannel].console_log(message: "Hi!").broadcast_to(current_user)head :okendend
Not too shabby, right?
Using ActiveJob - especially when it's backed by the awesome Sidekiq - is arguably one of the two best and most common ways developers broadcast CableReady operations, along with Reflexes.
Make sure that CableReady::Broadcaster
is included in your ApplicationJob
, and delegate render
to ApplicationController
:
app/jobs/application_job.rbclass ApplicationJob < ActiveJob::Baseinclude CableReady::Broadcasterdelegate :render, to: :ApplicationControllerend
Here's a genuinely contrived example of using a Job to drive CableReady:
app/views/home/index.html.erbWhat could possibly happen?<br><div id="content"></div>
app/controllers/home_controller.rbclass HomeController < ApplicationControllerdef indexExampleJob.set(wait: 5.seconds).perform_later current_user.idendend
If anyone starts lecturing you about the urgent and unquestionable need for the separation of business logic from presentation, tell them that you have work to do.
app/jobs/example_job.rbclass ExampleJob < ApplicationJobinclude CableReady::Broadcasterqueue_as :defaultdef perform(user_id)user = User.find(user_id)cable_ready[UsersChannel].inner_html(selector: "#content",html: "Hello World this is the background job.").broadcast_to(user)endend
Make sure that CableReady::Broadcaster
is included in your ApplicationRecord
, and delegate render
to ApplicationController
:
app/models/application_record.rbclass ApplicationRecord < ActiveRecord::Baseself.abstract_class = trueinclude CableReady::Broadcasterdelegate :render, to: :ApplicationControllerdef sgidto_sgid(expires_in: nil).to_sendend
We also recommend that you add a sgid
method to your models, to make it easier to work with Secure Global IDs when handling broadcasting to resources. By default, Rails uses the current time to set sgids to expire after a month by default. This means that every time you'd run to_sgid
, you would get a different result, which is not useful for our purposes - we need repeatable values.
Some people love them, and some people hate them. Regardless of your feelings about model callbacks, it's hard to ignore how CableReady dances inside of an ActiveRecord callback:
class Post < ApplicationRecordafter_update docable_ready[PostsChannel].morph(selector: dom_id(self),html: render(self)).broadcast_to(self)endend
If things aren't quite so straight-forward with your partial rendering, you can still do this:
class Post < ApplicationRecordafter_update docable_ready[PostsChannel].morph(selector: dom_id(self),html: render(partial: "navbar/posts", locals: { post: self })).broadcast_to(self)endend
If the location of your partial needs to be dynamic based on the context, you can re-assign it:
class Post < ApplicationRecordafter_update docable_ready[PostsChannel].morph(selector: dom_id(self),html: render(self)).broadcast_to(self)enddef to_partial_path"navbar/posts"endend
All excitement aside, we'd still recommend using those callbacks to queue up ActiveJobs instead of calling CableReady directly. But hey... the more you know, right?
Another promising use of CableReady inside of your models is state machine transition callbacks:
app/models/post.rbstate_machine initial: :pending doevent :accept dotransition [:pending] => :activeendafter_transition on: :accept do |post|cable_ready[PostsChannel].morph(selector: dom_id(post),html: render(post)).broadcast_to(post)endend
Make sure that CableReady::Broadcaster
is included in your ApplicationCable
, and delegate render
to ApplicationController
:
app/channels/application_cable/channel.rbmodule ApplicationCableclass Channel < ActionCable::Channel::Baseinclude CableReady::Broadcasterdelegate :render, to: :ApplicationControllerendend
In a new twist, let's empower this channel to receive data from the clients:
app/channels/sailors_channel.rbclass SailorsChannel < ApplicationCable::Channeldef subscribedstream_from "sailors"enddef receive(data)cable_ready["sailors"].console_log(message: "A sailor yells: #{data}").broadcastendend
This controller can send text back up to the server when the greet
method is fired:
app/javascript/controllers/sailors_controller.jsimport { Controller } from 'stimulus'import CableReady from 'cable_ready'export default class extends Controller {connect () {this.channel = this.application.consumer.subscriptions.create('SailorsChannel', {received (data) {if (data.cableReady) CableReady.perform(data.operations)}})}greet (event) {this.channel.send(event.target.value)}disconnect () {this.channel.unsubscribe()}}
Finally, let's wire up the input element's change event to the greet
method:
index.html.erb<input type="text" data-controller="sailors" data-action="change->sailors#greet">
StimulusReflex users must not include CableReady::Broadcaster
in their Reflex classes, as it makes special versions of the CableReady methods available.
If you would like to read more about using StimulusReflex with CableReady, please consult "Using CableReady inside a Reflex action".