Customization

CableReady is zero-config, meaning that there is no elaborate setup ritual. Still, there are several ways to tweak how things work, especially when it comes to integrations with other libraries.

Custom Operations

You can add your own operations to CableReady by creating an initializer:

config/initializers/cable_ready.rb
CableReady.configure do |config|
config.add_operation_name :jazz_hands
end

The syntax for this process did change recently. We apologize for any inconvenience.

Then you need to add your operation's implementation to the CableReady client, ideally before you import your Stimulus controllers and/or ActionCable channel subscribers. Note that while the Ruby add_operation method expects a snake-cased Symbol, JavaScript methods are camelCased.

app/javascript/packs/application.js
import CableReady from 'cable_ready'
CableReady.DOMOperations.jazzHands = operation => {
console.log('Jazz hands!', operation)
}

Now you can call your custom operation like any other. Any options passed to the operation when it is queued will be sent as part of the broadcast to the client.

cable_ready["visitors"].jazz_hands(thumbs: 2).broadcast

You can find inspiration for your own operations by checking out how the "factory default" operations were implemented. The setCookie and innerHtml methods are an excellent starting point.

Multi-element custom operations

If you need for your custom operation to support multi-element selectors, you will need to import the processElements function.

app/javascript/packs/application.js
import CableReady from 'cable_ready'
import { processElements } from 'cable_ready/javascript/utils'
CableReady.DOMOperations.jazzHands = operation => {
processElements(operation, element => {
console.log('Jazz hands!', element, operation)
})
}

You can now call your custom operation with select_all: true and you will see a console log message for every matching element.

cable_ready["visitors"].jazz_hands(selector: ".hand", select_all: true, thumbs: 2).broadcast

shouldMorph and didMorph

The morphdom library supports providing a callback function to decide whether a DOM element should be morphed; if this callback returns false for an element, neither that element nor any of that element's children will be morphed.

There is also a corresponding callback function that will run only if an element did get morphed. So this is not "before and after", but "should I and did I?" By adding our own callback functions, we can add support for interacting with other libraries which would otherwise be difficult or impossible.

shouldMorph and didMorph callbacks only impact use of the morph operation.

shouldMorph Callbacks

CableReady's onBeforeElUpdated callback, shouldMorph, sequentially executes an array of functions called shouldMorphCallbacks. It comes factory installed with two callbacks that you can probably leave alone: verifyNotMutable and verifyNotPermanent. If you're not using StimulusReflex, you could experiment with slice to remove verifyNotPermanent for a small performance boost. 🤷

These callbacks need to return true if the element should be morphed, or else return false to skip it. All callbacks must return a boolean value, even if the purpose of of the callback is to perform some kind of meta-transformation on the elements, as you'll see with the Alpine example in a moment.

Your shouldMorph callback function will receive three parameters: the options passed to the morph method, fromEl, which is the element before it is (potentially) morphed, and toEl, which is the element fromEl will (potentially) be morphed into.

The primary use case of a shouldMorph callback is to skip elements that meet a certain criteria - or don't. This could be handy if you need to protect part of the DOM:

app/javascript/packs/application.js
import CableReady from 'cable_ready'
const skipBanner = (options, fromEl, toEl) => {
if (fromEl.id === 'ad-banner') return false
return true
}
CableReady.shouldMorphCallbacks.push(skipBanner)

A sneaky second use of shouldMorph callbacks is to assume the function will return true, but use it as an opportunity to prepare the incoming element by copying something from the outgoing element. This is important for AlpineJS users, who need to clone Alpine's internal state machine so that components are properly initialized:

app/javascript/packs/application.js
import CableReady from 'cable_ready'
const enableAlpine(options, fromEl, toEl) {
if (fromEl.__x) { window.Alpine.clone(fromEl.__x, toEl) }
return true
}
CableReady.shouldMorphCallbacks.push(enableAlpine)

didMorph Callbacks

CableReady's onElUpdated callback, didMorph, sequentially executes an array of functions called didMorphCallbacks. These callbacks will only fire for elements which were successfully morphed.

Your didMorph callback function will receive two parameters: the options passed to the morph method, and el, which is the element after it has been morphed.

In the following example, users of the Shoelace web component UI toolkit can add the hydrated CSS class to every morphed element with a tagname that starts with "SL-".

app/javascript/packs/application.js
import CableReady from 'cable_ready'
const fixShoelace(detail, el) {
if (el.tagName.startsWith('SL-')) el.classList.add('hydrated')
}
CableReady.didMorphCallbacks.push(fixShoelace)

performAsync

Do you love CableReady's perform method, but wish that it returned a Promise? You are in luck!

app/javascript/channels/example_channel.js
import CableReady from 'cable_ready'
import consumer from './consumer'
consumer.subscriptions.create('ExampleChannel', {
received (data) {
if (data.cableReady)
CableReady.performAsync(data.operations)
.then(payload => {
console.log(payload)
})
.catch(err => {
console.log(err)
})
}
})

emitMissingElementWarnings

By default, CableReady will generate a warning in your Console Log if you attempt to execute an operation on a selector that does not exist. Sometimes, this isn't desirable and you just want to silently ignore missing elements. This can be achieved by passing an object with emitMissingElementWarnings: false as the second parameter to perform.

An example of this behavior at work is in the Optimism channel subscriber class, which can occasionally generate validation warnings for fields that don't exist in your View:

app/javascript/channels/optimism_channel.js
import CableReady from 'cable_ready'
import consumer from './consumer'
consumer.subscriptions.create('OptimismChannel', {
received (data) {
if (data.cableReady)
CableReady.perform(data.operations, {
emitMissingElementWarnings: false
})
}
})