Life-cycle
Server-Side Reflex Callbacks
StimulusReflex gives you a set of callback events to control how your Reflex actions function. These usual suspects will be familiar to Rails developers:
before_reflex
,around_reflex
,after_reflex
- All callbacks can receive multiple symbols representing Reflex actions, an optional block and the following options:
only
,except
,if
,unless
class ExampleReflex < ApplicationReflex
# will run only if the element has the step attribute, can use "unless" instead of "if" for opposite condition
before_reflex :do_stuff, if: proc { |reflex| reflex.element.dataset.step }
# will run only if the reflex instance has a url attribute, can use "unless" instead of "if" for opposite condition
before_reflex :do_stuff, if: :url
# will run before all reflexes
before_reflex :do_stuff
# will run before increment reflex, can use "except" instead of "only" for opposite condition
before_reflex :do_stuff, only: [:increment]
# will run around all reflexes, must have a yield in the callback
around_reflex :do_stuff_around
# will run after all reflexes
after_reflex :do_stuff
# Example with a block
before_reflex do
# callback logic
end
# Example with multiple method names
before_reflex :do_stuff, :do_stuff2
# Example with halt
before_reflex :run_checks
def increment
# reflex logic
end
def decrement
# reflex logic
end
private
def run_checks
throw :abort # this will prevent the Reflex from continuing
end
def do_stuff
# callback logic
end
def do_stuff2
# callback logic
end
def do_stuff_around
# before
yield
# after
end
end
Aborting a Reflex
It is possible that you might want to abort a Reflex and prevent it from executing. For example, the user might not have appropriate permissions to complete an action, or perhaps some other side effect like missing data would cause an exception if the Reflex was allowed to continue.
An aborted Reflex will trigger the halted
life-cycle stage instead of the after
and finalize
stages on the client. You can learn more about how aborted Reflexes behave in different scenarios in Understanding Stages.
To abort a Reflex, call throw :abort
inside of a before_reflex
callback on your Reflex class.
INFO
Please note that all statements run before a Reflex is aborted have already happened; there is no "roll-back" of database updates, CableReady broadcasts or other changes. Even setting a Reflex Morph type or Reflex Payload will survive an abort, so long as they happen before throw :abort
occurs.
Client-Side Reflex Callbacks
StimulusReflex gives you the ability to inject custom JavaScript at distinct moments around the life-cycle of a Reflex. Developers can use a combination of callback methods, event handlers and Promise resolution to improve the user experience and handle edge cases.
before
prior to sending a Reflex over the WebSocketsuccess
after the server-side Reflex processes successfullyerror
after the server-side Reflex raises an errorhalted
Reflex canceled by developer withthrow :abort
in abefore_reflex
callbackafter
follows eithersuccess
orerror
immediately before DOM manipulationsfinalize
occurs immediately after all DOM manipulations are complete
Using life-cycle stages is not a requirement.
Think of them as power tools that can help you build more sophisticated results. 👷
Order of operations
The order of operations for life-cycle management techniques is:
- Promise resolution
- Callback methods
- Event handlers
There is no "best" way to handle life-cycle stages, and most developers use a blend of the available tools depending on the situation and their preferred development style. While the APIs are different, we've worked hard to make sure that each mechanism has access to all of the Reflex data available at each stage of the process.
Understanding Stages
Most of the time, it's reasonable to expect that your Reflexes will follow a predictable cycle: before
-> success
-> after
-> finalize
.
There are, however, several important exceptions to the norm.
- Reflexes that are aborted on the server have a short cycle:
before
->halted
- Reflexes that have errors:
before
->error
->after
-> [finalize
] - Nothing Morphs end early:
before
-> [success
] ->after
- Event handlers for the
after
stage will fire beforesuccess
anderror
. - Nothing Morphs have no CableReady operations to wait for, so there is nothing to
finalize
. - Nothing Morphs support
success
callback methods but do not emitsuccess
events. - A Reflex with an error will not have a
finalize
stage.
Callback Methods
If you define a method with a name that matches what the library searches for, it will run at just the right moment. If there's no method defined, nothing happens. StimulusReflex will only look for these methods in Stimulus controllers that extend ApplicationController
or have called StimulusReflex.register(this)
in their connect()
function.
WARNING
Unlike ActiveSupport callbacks, if you define the same callback in a parent class (such as ApplicationController
) and a class that extends it, only the one in the extended class will execute.
There are two kinds of callback methods: generic and custom. Generic callback methods are invoked for every Reflex action on a controller. Custom callback methods are only invoked for specific Reflex actions.
Generic Life-cycle Methods
StimulusReflex controllers automatically support generic life-cycle callback methods. These methods fire for every Reflex action handled by the controller.
beforeReflex
reflexSuccess
reflexError
reflexHalted
afterReflex
finalizeReflex
INFO
While this is perfect for basic Reflexes with a small number of actions, most developers quickly switch to using Custom Life-cycle Methods, which allow you to define different callbacks for every Reflex action.
In this example, we update each anchor's text before invoking the server side Reflex:
<div data-controller="example">
<a href="#" data-reflex="Example#masticate">Eat</a>
<a href="#" data-reflex="Example#defecate">Poop</a>
</div>
import ApplicationController from './application_controller.js'
export default class extends ApplicationController {
beforeReflex(anchorElement) {
const { reflex } = anchorElement.dataset
if (reflex.match(/masticate$/)) anchorElement.innerText = 'Eating...'
if (reflex.match(/defecate$/)) anchorElement.innerText = 'Pooping...'
}
}
Custom Life-cycle Methods
StimulusReflex controllers can define up to six custom life-cycle callback methods for each Reflex action. These methods use a naming convention based on the name of the Reflex. The naming follows the pattern <actionName>Success
and matches the camelCased name of the action.
The Reflex Example#poke
will cause StimulusReflex to check for the existence of the following life-cycle callback methods:
beforePoke
pokeSuccess
pokeError
pokeHalted
afterPoke
finalizePoke
<div data-controller="example">
<a href="#" data-reflex="click->Example#poke">Poke</a>
<a href="#" data-reflex="click->Example#purge">Purge</a>
</div>
import ApplicationController from './application_controller.js'
export default class extends ApplicationController {
beforePoke(element) {
element.innerText = 'Poking...'
}
beforePurge(element) {
element.innerText = 'Purging...'
}
}
Adapting the Generic example, we've refactored our controller to execute the before
callback methods for each anchor individually.
It's not required to implement all life-cycle methods.
Pick and choose which life-cycle callback methods make sense for your application. The answer is frequently none.
Method Names
Life-cycle callback methods apply a naming convention based on your Reflex actions. For example, the Reflex ExampleReflex#do_stuff
will search for the following camel-cased life-cycle callback methods.
beforeDoStuff
doStuffSuccess
doStuffError
doStuffHalted
afterDoStuff
finalizeDoStuff
Method Signatures
Both generic and custom life-cycle callback methods share the same arguments:
beforeReflex(element, reflex, noop, reflexId)
reflexSuccess(element, reflex, noop, reflexId)
reflexError(element, reflex, error, reflexId)
reflexHalted(element, reflex, noop, reflexId)
afterReflex(element, reflex, noop, reflexId)
finalizeReflex(element, reflex, noop, reflexId)
element - the DOM element that triggered the Reflex this may not be the same as the controller's this.element
reflex - the name of the server side Reflex
error/noop - the error message (for reflexError), otherwise null
reflexId - a UUID4 or developer-provided unique identifier for each Reflex
Life-cycle Events/Event Handlers
If you need to know when a Reflex method is called, but you're working outside of the Stimulus controller that initiated it, you can subscribe to receive DOM events.
DOM events are limited to the generic life-cycle; developers can obtain information about which Reflex methods were called by inspecting the detail object when the event is captured.
Events are dispatched on the same element that triggered the Reflex. Events bubble but cannot be cancelled.
Event Names
stimulus-reflex:before
stimulus-reflex:success
stimulus-reflex:error
stimulus-reflex:halted
stimulus-reflex:after
stimulus-reflex:finalize
Event Metadata
When an event is captured, you can obtain all of the data required to respond to a Reflex action:
document.addEventListener('stimulus-reflex:before', event => {
event.target // the element that triggered the Reflex (may not be the same as controller.element)
event.detail.reflex // the name of the invoked Reflex
event.detail.reflexId // the UUID4 or developer-provided unique identifier for each Reflex
event.detail.controller // the controller that invoked the stimuluate method
event.target.reflexData[event.detail.reflexId] // the data payload that will be delivered to the server
event.target.reflexData[event.detail.reflexId].params // the serialized form data for this Reflex
})
event.target
is a reference to the element that triggered the Reflex, and event.detail.controller
is a reference to the instance of the controller that called the stimulate
method. This is especially handy if you have multiple instances of a controller on your page.
INFO
Knowing which element dispatched the event might appear daunting, but the key is in knowing how the Reflex was created. If a Reflex is declared using a data-reflex
attribute in your HTML, the event will be emitted by the element with the attribute.
You can learn all about Reflex controller elements on the Calling Reflexes page.
jQuery Events
In addition to DOM events, StimulusReflex will also emits duplicate jQuery events which you can capture. This occurs only if the jQuery library is present in the global scope eg. available on window
.
These jQuery events have the same name and details
accessors as the DOM events.
Promise Resolution
In addition to life-cycle methods and events, StimulusReflex allows you to write promise resolver functions:
this.stimulate('Comments#create')
.then(this.handleSuccess)
.catch(this.handleError)
You can get a sense of the possibilities:
this.stimulate('Post#publish').then(promise => {
const { data, element, event, payload, reflexId } = promise
// * data - the data sent from the client to the server over the web socket to invoke the reflex
// * element - the element that triggered the reflex
// * event - the source event
// * payload - optional return data passed from the Reflex method
// * reflexId - a unique identifier for this specific reflex invocation
}).catch(promise => {
const { data, element, event, payload, reflexId, error } = promise
// * error - the error message from the server
})
You can get the reflexId
of an unresolved promise:
const snail = this.stimulate('Snail#secrete')
console.log(snail.reflexId)
snail.then(trail => console.log)
Configuring Promise resolution timing
Any Promise can only be resolved once, at which time your callback will run if defined. By default, StimulusReflex will resolve the Promise associated with a Reflex action during the after
life-cycle stage. This means your callback will execute after the server has executed the Reflex action but before any DOM modifications are initiated. In some cases, this is too soon to be useful.
You can initiate a Reflex that will resolve its Promise during the finalize
life-cycle stage, after all CableReady operations have completed. At this point, all DOM modifications are complete and it is safe to initiate animations or other effects.
To request that a Reflex resolve its Promise during the finalize
stage instead of after
, pass resolveLate: true
as one of the possible optional arguments to the stimulate
method.
this.stimulate('Example#foo', { resolveLate: true }).then(() => {
console.log('The Reflex has been finalized.')
})
DANGER
Trying to create an element to be morphed by a Reflex in the Promise is not a viable strategy, as the finalize
stage is not waiting for the Promise to complete. Take care to design your application such that you're always targeting elements that exist. 🦉
StimulusReflex Library Events
In addition to the Reflex life-cycle mechanisms, the StimulusReflex client library emits its own set of handy DOM events which you can hook into and use in your applications.
stimulus-reflex:action-cable:connected
stimulus-reflex:action-cable:disconnected
stimulus-reflex:action-cable:rejected
stimulus-reflex:ready
In previous versions, ActionCable was assumed; in future versions of the library, other transport mechanisms will be available. Legacy library events are now deprecated and will be removed in the future:
stimulus-reflex:connected
stimulus-reflex:disconnected
stimulus-reflex:rejected
All events fire on document
, except stimulus-reflex:ready
which is being dispatched on the StimulusReflex controller element itself.
connected
fires when the ActionCable connection is established, which is a precondition of a successful call to stimulate
- meaning that you can delay calls until the event arrives. It will also fire after a disconnected
subscription is reconnected.
disconnected
fires if the connection to the server is lost; the detail
object of the event has a willAttemptReconnect
boolean which should be true
in most cases.
rejected
is fired if you're doing authentication in your Channel and the subscription request was denied.
ready
is slightly different than the first three, in that it has nothing to do with the ActionCable subscription. Instead, it is called after StimulusReflex has scanned your page, looking for declared Reflexes to connect. This event fires every time the document body is modified, and was created primarily to support automated JS test runners like Cypress. Without this event, Cypress tests would have to wait for a few seconds before "clicking" on Reflex-enabled UI elements.
jQuery Events
In addition to DOM events, StimulusReflex will also emits duplicate jQuery events which you can capture. This occurs only if the jQuery library is present in the global scope eg. available on window
.
These jQuery events have the same name and details
accessors as the DOM events.