The author has substantial empathy for any volunteers tasked with documenting abstract framework concepts, including the dozens of people who have contributed in varying degrees to the Rails Guide for ActionCable. Still, as a tool for learning how to use ActionCable, it frustrates the reader along several axis and hasn't received the polish other Guides in the Rails framework have benefitted from.
The most unfortunate aspect of the ActionCable guide was the decision to frame the entire mental model around the apparent goal of implementing [part of] a chat system. ActionCable is for building chat systems in the exact same way that Rails is for building blogs. In fact, this paragraph is very intentionally the only time chat will be mentioned in the entirety of the CableReady documentation. It is realistic to conclude that the near-complete fixation on chat retarded the evolution of real-time interfaces and techniques in Rails by a period measured in years. π€¦ββοΈ
Connection
represents the fully-abstracted raw websocket protocol. Properly configured, one WS connection can support an unbound number of Channels, and it will work hard to keep you connected even if your bandwidth is spotty. Connections are also where most developers implement authentication.
Channel
is a theme-specific conduit for exchanging messages via the Connection. These conduits are referenced by the developer using either a string or a constant. Channel is designed with a "hub-and-spoke" distribution model in which there is no concept of direct, client-to-client message passing. Implemented as a sibling pair of Ruby and JavaScript classes, Channel provides the flexible conceptual chassis upon which real-time applications can be built in Rails.
Subscription
is a wire made out of intention, stretched between the firehose "stream" interfaces of the Channel and the densely connected tree your client-side code taps like a spigot. Subscriptions might not be free, but they certainly are quite cheap.
The thing about Channels and Subscriptions is that once you've established them, they only take up as much room as the content that you pass down them. They are a lattice of pneumatic tubes that only exist in the moment they are needed, and not a moment before or after.
To double-murder a metaphor, Channels are to classes what Subscriptions are to instances.
... and now you know how ActionCable works!
CableReady was created with a deep and informed belief that web applications that maintain state on the server are fundamentally easier to design, build and maintain.
However, one of the stranger edge-cases that must be handled in a websockets-enabled world is the potential for a server update to overwrite the value of a text input while the user is typing into it. It's a jarring example because it's an end-result that was almost completely impossible to achieve in the Ajax era. Despite our wildest brainstorms, we have yet to identify even a single scenario where a user would consider having their effort undone to be positive.
As a result, CableReady's popular morph operation comes pre-installed with a shouldMorph callback called verifyNotMutable
that actively prevents the server from overwriting input, textarea and select elements while they are active (have focus).
Since forms are rarely designed to be edited by multiple concurrent users π± it's unlikely that you'll have to spend time thinking about this issue. If you're one of the lucky ones, you can use CableReady and StimulusReflex to establish a field-level locking system, or at the very least, update CSS or nearby indicators to show that a particular input is locked, contested or potentially out of date.
By default, the selector
option provided to DOM-mutating operations expects a CSS selector that resolves to one single DOM element.
If multiple elements are returned, only the first one is used.
Many DOM Mutation and Element Property Mutation operations support a select_all
option which instructs CableReady to operate on one or many DOM elements returned by the selector
query.
This technique is quite powerful because it can scoop up elements from multiple locations in the DOM based on their element type, id property, CSS class list or attributes. For example, you could grab every element with an instance of a Stimulus controller called sushi
:
cable_ready.text_content(select_all: true, selector: "[data-controller='sushi']")
Each element will emit their own "before" and "after" events as part of the same operation.
Any information you change in these events could modify the behavior of the operation for the other elements that were selected. πππ
CableReady also supports the use of XPath query expressions to locate elements based on their location relative to the document root in the DOM hierarchy. πΊοΈ
To interpret a selector as an XPath query, you need to do two things:
Pass xpath: true
as a parameter argument when enqueueing the operation.
Provide an XPath query that matches the desired element, and then ensure its validity until the operation has completed.
Here's a function that can produce an XPath expression for any DOM element:
const elementToXPath = element => {if (element.id !== '') return "//*[@id='" + element.id + "']"if (element === document.body) return '/html/body'βlet ix = 0const siblings = element.parentNode.childNodesβfor (var i = 0; i < siblings.length; i++) {const sibling = siblings[i]if (sibling === element) {const computedPath = elementToXPath(element.parentNode)const tagName = element.tagName.toLowerCase()const ixInc = ix + 1return `${computedPath}/${tagName}[${ixInc}]`}βif (sibling.nodeType === 1 && sibling.tagName === element.tagName) ix++}}
An XPath-powered operation might look like:
cable_ready["stream"].text_content(selector: "/html/body/div[3]/div[1]/article[1]/section[5]/ul[1]/li[10]/div[1]/div[2]",xpath: true,text: "XPath is under-utilized, but beware of side-effects changing your DOM.").broadcast
You can grab the XPath selector for any element using your browser's Element Inspector. Activate the desired element, right-click and select "Copy", then "Copy full XPath".
XPath selectors cannot be used with the select_all
option, although if this is important to your application, let us know and we'll consider a more flexible implementation.
Most CableReady operations emit a DOM CustomEvent immediately before an operation is executed, and another immediately after.
They will be emitted from the selector
element if present, otherwise they will default to document
.
The event names follow a predictable pattern, as seen with cable-ready:before-inner-html
and cable-ready:after-morph
.
These events bubble and can be cancelled.
If jQuery is detected on the current page, jQuery events will be triggered immediately after the DOM events. These jQuery events have the same name and detail
accessors as their DOM event siblings.
A small number of operations, such as dispatch_event and console_log, do not emit events. It's up to you to make sure that any custom operations you create raise events, if desired.
Don't confuse Life-cycle events emitted by operations with the dynamically defined events you can send using dispatch_event operations. The ways you can capture them are the same, but Life-cycle events are part of the CableReady library behaviour whereas dispatched events are ad hoc and not constrained to CableReady operations.
You can create a callback function to handle the life-cycle events CableReady emits, and then register that callback as an event listener without any special tools.
Create named functions (and avoid anonymous functions) for your callbacks because it's impossible to remove an event listener with an anonymous callback.
const afterMorphHandler = event => { console.log(event.detail) }document.addEventListener('cable-ready:after-morph', afterMorphHandler)
Once you have captured an event, you can inspect the detail
object to access all of the options passed to CableReady when the operation was enqueued.
You can pass extra, arbitrary, JSON-compatible data when adding an operation. You can use operations to send extra information such as UUIDs and even rendered bits of HTML to the client.
cable_ready.set_cookie(cookie: "favorite_food=tripe",corn_pop: "bad dude").broadcast
On the client, you can access all parameters provided to an operation via the detail
object. Remember, all snake_case keys will be automatically converted to camelCase:
setCookieHandler = event => {console.log(event.detail.cornPop)}
In general, it's easier to track related concepts transactionally in one broadcast envelope than it is to assemble data from multiple broadcasts back into a coherent state.
Almost all operations emit cable-ready:before-{operation}
and cable-ready:after-{operation}
events. If you create an event handler to listen for "before" events, you can access and modify most of the parameters passed when queueing the operation on the server.
Continuing the set_cookie
example, the event handler is able to intercept the operation mid-flight and change the parameters.
setCookieHandler = event => {event.detail.cookie = 'favorite_food=yams'}
If your operation is processing multiple elements, each element will emit its own "before" and "after" events. If you change any values in the event.detail
object, this new value will be picked up by the other elements associated with the current operation that have not been processed, yet.
You could conceivable change a value during the "before" callback, then change it back to the original value during the "after" callback. This is almost certainly something you don't want to need to be able to do.β
Almost all operations accept a cancel
parameter that is designed to be interacted with in a "before" event handler on the client. cancel
must be false
or undefined
when the event handler returns for the operation to run.
setCookieHandler = event => {event.detail.cancel = true}
Cancelled operations still emit an "after" event, but their primary functionality will not occur.
For example, if you have a long-running Reflex operation, the user might click a cancel button and proceed with their business. When the Reflex finally completes, your event handler can cancel the operation and prevent whatever would have happened.
The server will have no idea that the operation was cancelled on the client. If this would create an inconsistency, you should send a cancel notification to the server, perhaps with a Nothing Reflex.
While most developers will never think about or interact with the cancel
parameter, it's an important tool to have available when building sophisticated client user interfaces.
As with modifying detail
data, if your operation is processing multiple elements, each element will emit its own "before" and "after" events. You could cancel an operation for a given element and then un-cancel it for later elements.
You could jump out of an airlock into space, too. Don't say we didn't warn you! π¨βπ
The DOM Mutation operations accept an optional focusSelector
parameter that allows you to specify a CSS selector to which element should be active (receive focus) after the operation completes.
If focusSelector
is not specified, the focus will go to the element that was active immediately before the operation was executed.
It is possible to perform an operation that removes the previously active element, leaving the focus in an ambiguous state. It's also possible to use the set_focus operation to manually set the focus at any time.
Since it's difficult to improve upon perfection, please consult the StimulusReflex documentation section on authenticating users in ActionCable.
There are times where it might be useful to send data directly to any clients subscribed to a given Channel stream identifier. It's even compatible with a CableReady performer since the data you send will (hopefully) not have a cableReady
key present.
ActionCable.server.broadcast("your-stream-identifier", data)
You can see this technique used in "Verify ActionCable".
If you need to send data to a constant-based stream, you just need to break down the fourth wall and construct your identifier manually. Here we will send data to current_user
using the UsersChannel
:
ActionCable.server.broadcast("users:#{current_user.to_gid_param}", data)
UsersChannel
becomes users
while ActiveRecord has a to_gid_param
.
Sometimes you just need to tell a subscriber that it's time to do the thing. You can send a broadcast
with no operations and still take advantage of the received
handler:
cable_ready["stream"].broadcast
consumer.subscriptions.create('ChewiesChannel', {received (data) {console.log('Data was received!')}})
As you can see in the upcoming section on connection identifiers, ActionCable Connections can designate that they are able to be identified_by
one or more objects. These can be strings or ActiveRecord model resources. It is only using one of these connection identifiers that you can forcibly disconnect a client connection entirely.
Forcing a websocket reconnection is mainly useful for upgrading account privileges after successfully authenticating. You could also disconnect former employees after they've been terminated.
This is going to look a lot like an ActiveRecord finder, but it's a trap! This is no such thing. The only thing it can look up are connection identifiers that have already been defined on the Connection class. You need a valid resource reference (i.e. a user that is actually connected) to get a match on the ActionCable remote_connections
mapping. Otherwise, the following will simply fail silently:
ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
The ActionCable Channel subscriber will immediately start attempting to reconnect to the server, with the usual connection retry rate fall-off curve, just as if you restarted your Puma process.
It's not clear whether this is a bug or a feature, but ActionCable will not allow you to disconnect a user if your Connection has any identifiers which haven't been assigned. Specifically, if you have configured your Connection to be identified_by
both current_user
and session_id
, it will raise an error if your user hasn't authenticated yet. That's no good!
Our suggestion is that you fix ActionCable with this initializer, which changes line 6 from all?
to any?
config/initializers/action_cable.rbmodule ActionCableclass RemoteConnectionsclass RemoteConnectiondef valid_identifiers?(ids)keys = ids.keysidentifiers.any? { |id| keys.include?(id) }endendendend
StimulusReflex is the sister library to CableReady. It's... really great, actually.
Library | Responsibility |
StimulusReflex | Translates user actions into server-side events that change your data, then regenerating your page based on this new data into an HTML string. |
CableReady | Takes the HTML string from StimulusReflex and sends it to the browser before using morphdom to update only the parts of your DOM that changed. |
β¬οΈ StimulusReflex is for sending commands. π‘ β¬οΈ CableReady is for receiving updates. π½
A Reflex action is a reaction to a user action that changes server-side state and re-renders the current page (or a subset of the current page) for that particular user in the background, provided that they are still on the same page.
A CableReady operation is a reaction to some server-side code (which must be imperatively called) that makes some change for some set of users in the background.
If you would like to read more about using StimulusReflex with CableReady, please consult "Using CableReady inside a Reflex action".