Skip to content
On this page

Calling Reflexes ​

What is a Reflex, really? Is it a transactional UI update that takes place over a persistent open connection to the server? Is it a new tool on your belt that operates adjacent to and in tandem with concepts like REST and Ajax? Is it the smug feelings associated with successfully achieving a massive productivity arbitrage? Is it the boundless potential for unironic good in every child?

A thousand times, yes.

There are three kinds of Reflex... ​

StimulusReflex features three distinct modes of operation, and you can use all three of them together in your application:

  • Page Morph, which is the default, performs a full-page update
  • Selector Morph is for replacing the content of an element
  • Nothing Morph, for executing functions that don't update your page

Every Reflex starts off life as a Page Morph. You can change it to a different kind of Morph inside of your Reflex action; there's no way to set a Morph type on the client.

You can learn more about the control flow of each Morph by consulting this flowchart.

The rest of this page generally assumes that you're working with a Page Morph. Selector and Nothing Morphs are described in detail on their own page:

Declaring a Reflex in HTML with data attributes ​

The fastest way to enable Reflex actions by using the data-reflex attribute. The syntax follows Stimulus format: [event]->[ReflexClass]#[action]

html
<button data-reflex="click->Comment#create">Create</button>

You can use additional data attributes to pass variables as part of your Reflex:

html
<button
  data-reflex="click->Comment#create"
  data-post-id="<%= @post.id %>"
>Create</button>

It's a recommended best practice to put an id attribute on any element that has a data-reflex attribute on it. id is unique in a valid DOM, and this is how StimulusReflex locates the controller which called the Reflex after a morph operation.

If you have multiple identical elements calling Reflex actions, no life-cycle mechanisms (afterReflex callbacks, success events etc) will be run.

INFO

Thanks to the magic of MutationObserver, a browser feature that allows StimulusReflex to know when the DOM has changed, StimulusReflex can pick up data-reflex attributes on all HTML elements - even if they are dynamically created and inserted into your DOM.

This means that if you parse a client-side markup format that has declarative Reflexes contained within, they will be connected to StimulusReflex in less than a millisecond.

Trigger a Reflex on a custom event ​

While click input and change are the most common browser events to catch, Reflexes can be initiated in response to any event supported by the Stimulus event syntax.

This means that custom events, such as ajax:success, are valid. It also means that you can listen for events on window and document, making resize@window->Foo#resize a valid declared Reflex.

INFO

The Stimulus event shorthand syntax is not supported.

Declaring multiple Reflex events on an element ​

Do you want to trigger different Reflex actions for different events? We have you covered!

You can specify multiple Reflex operations by separating them with a space:

html
<img src="cat.jpg" data-reflex="mouseenter->Cat#approach mouseleave->Cat#escape">

INFO

There are two intentional limitations to this technique:

All Reflex actions must target the same controller. In the above example, it won't work properly if the mouseleave points to Dog#escape because, obviously, cats and dogs don't mix.

Also, you can only specify one action per event; this means data-reflex="click->Cat#eat click->Cat#sleep" will not work. In this example, the second action would be discarded.

Inheriting data-attributes from parent elements ​

You might design your interface such that you have a deeply nested structure of data attributes on parent elements. Instead of writing code to travel your DOM and access those values, you can use the data-reflex-dataset="combined" directive to scoop all data attributes up the hierarchy and pass them as part of the Reflex.

html
<div data-post-id="<%= @post.id %>">
  <div data-category-id="<%= @category.id %>">
    <button data-reflex="click->Comment#create" data-reflex-dataset="combined">Create</button>
  </div>
</div>

This Reflex action will have post_id and category_id accessible:

ruby
class CommentReflex < ApplicationReflex
  def create
    puts element.dataset.post_id
    puts element.dataset.category_id
  end
end

If a data attribute appears several times, the deepest one in the DOM tree is taken. In the following example, data-id would be 2.

html
<div data-id="1">
  <button data-id="2" data-reflex="Example#whatever" data-reflex-dataset="combined">Click me</button>
</div>

Calling a Reflex in a Stimulus controller ​

In addition to declared Reflexes, you can also initiate a Reflex with JavaScript:

html
<button data-controller="foo"
        data-action="foo#bar"></button>

Note that there is no relation between the name or action on the Stimulus controller, and the Reflex being called in the event handler:

javascript
import ApplicationController from './application_controller.js'

export default class extends ApplicationController {
  bar() {
    this.stimulate('Example#test')
  }
}

This is possible because ApplicationController imports the StimulusReflex Controller and calls StimulusReflex.register(this). As a result, ApplicationController and all Stimulus Controllers that extend it gain a method called stimulate.

When you use declarative Reflex calls via data-reflex attributes in your HTML, the stimulate method is called for you. 🀯 You will learn all about this process in Understanding Reflex Controllers.

stimulate is extremely flexible ​

this.stimulate(string target, [DOMElement element], [Object options], ...[JSONObject argument])

target [required] (exception: see "Requesting a Refresh" below): a string containing the server Reflex class and method, in the form Example#increment.

element [optional]: a reference to a DOM element which will provide both attributes and scoping selectors. Frequently pointed to event.target in JavaScript. Defaults to the DOM element of the controller in scope.

options [optional]: an optional object containing at least one of reflexId, selectors, resolveLate, serializeForm or attrs. Can be used to override the ID of a given Reflex or override the selector(s) to be used for Page or Selector morphs. Advanced users might wish to modify the attributes sent to the server for the current Reflex.

argument [optional]: a splat of JSON-compliant JavaScript datatypes - array, object, string, numeric or boolean - will be received by the Reflex action as ordered arguments.

Receiving arguments ​

When calling this.stimulate() with JavaScript, you have the option to send arguments to the Reflex action method on the server. Options have to be JSON-serializable data types and are received in a predictable order. Objects that are passed as parameters are accessible using both symbol and string keys.

ruby
class CatReflex < ApplicationReflex
  def adopt(opinions, legs = 4)
    puts opinions["gender"]
    puts opinions[:gender]
  end
end

INFO

Note: the method signature has to match. If the Reflex action is expecting two arguments and doesn't receive two arguments, it will raise an exception.

Note that you can only provide parameters to Reflex actions by calling the this.stimulate() method with arguments; there is no equivalent for Reflexes declared with data attributes.

Combined data attributes with this.stimulate() ​

data-reflex-dataset="combined" also works with the this.stimulate() function:

html
<div data-folder-id="<%= folder.id %>" data-controller="folders">
  <button data-action="click->folders#edit" data-reflex-dataset="combined">
    Edit
  </button>
</div>

By default, this.stimulate() treats the DOM element that the controller is placed on as the element parameter. Instead, we use event.target to make the clicked button element be the source of the Reflex action. All combined data attributes will be picked up, and all callbacks and events will emit from the button.

javascript
import ApplicationController from './application_controller.js'

export default class extends ApplicationController {
  edit(event) {
    this.stimulate("Folder#edit", event.target)
  }
}

Requesting a "refresh" ​

If you are building advanced workflows, there are edge cases where you may want to initiate a Reflex action that does nothing but re-render the view template and morph any new changes into the DOM. While this shouldn't be your primary tool, it's possible for your data to be mutated by destructive external side effects. 🧟

javascript
this.stimulate()

Calling stimulate with no parameters invokes a special global Reflex that allows you to force a re-render of the current state of your application UI. This is the same thing that the user would see if they hit their browser's Refresh button, except without the painfully slow round-trip cycle.

It's also possible to trigger this global Reflex by passing nothing but a browser event to the data-reflex attribute. For example, the following button element will refresh the page content every time the user presses it:

html
<button data-reflex="click">Refresh</button>

Understanding StimulusReflex Controllers ​

You've already read that StimulusReflex is based on the Stimulus JavaScript library. When you're using Stimulus, you put Controllers on your DOM elements by setting data-controller="foo". In the following example, we attach an instance of the Controller class defined in foo_controller.js to a div:

html
<div data-controller="foo"></div>

The StimulusReflex client is literally just a complex Stimulus Controller. When you first load your page, the first thing it does is scan your DOM for data-reflex attributes:

html
<div>
  <button data-reflex="click->Foo#remove">
    Remove this button
  </button>
</div>

When it finds a data-reflex attribute, it adds a Stimulus Controller called stimulus-reflex and a __perform action to the element:

html
<div>
  <button
    data-reflex="click->Foo#remove"
    data-controller="stimulus-reflex"
    data-action="click->stimulus-reflex#__perform"
  >
    Remove this button
  </button>
</div>

These three attributes contain everything required to call the remove Reflex action on the Foo Reflex class when the user clicks the button. The button is now a Reflex Controller Element, because the StimulusReflex Controller responsible for the Reflex is attached to it. As a result, all life-cycle events will be emitted from it.

INFO

StimulusReflex scans all content inserted into the DOM for data-reflex attributes, regardless of whether that content is there when the page loads or if it comes from a Reflex, an Ajax fetch, or your own local JavaScript logic. You don't have to do anything special to ensure that your UI is Reflex-enabled.

What's really interesting about this is that you'll notice we don't have to add the foo Stimulus controller to the button element in order to be able to call Foo Reflexes. We are not doing it magically in the background, either; there doesn't need to be a foo StimulusReflex Controller on the element in order for a declared Reflex to call FooReflex#remove on the server.

There might not even be a foo_controller.js, and that's okay.

If we click the button, we'll see that the StimulusReflex Controller used to handle the Reflex was stimulus-reflex as expected:

You should interpret this as "my Reflex was handled by the ApplicationController" - that is, application_controller.js - which was installed during setup. Any generic Reflex callbacks defined in the ApplicationController itself will be run, but this is usually limited to spinners and other "meta" UI effects.

If you want to provide handlers for life-cycle events, you will need to create a StimulusReflex Controller class with the same name as your Reflex:

html
<button
  data-reflex="click->Foo#remove"
  data-controller="foo"
>
  Remove this button
</button>
javascript
import ApplicationController from './application_controller'

export default class extends ApplicationController {
  // Remember, Nothing Morphs end on the "after" stage
  afterRemove(element, reflex, noop, reflexId) {
    console.log('Button has been deleted!')
  }
}

When you click the button, it calls the Foo Reflex - and removes the button from the DOM:

ruby
class FooReflex < ApplicationReflex
  def remove
    cable_ready.remove(selector: 'button').broadcast

    morph :nothing
  end
end

And you can see that the StimulusReflex Controller responsible for the Reflex was foo:

So, that's pretty cool, right? πŸ•ΆοΈ It knows to use foo instead of stimulus-reflex.

The thing is... where's our console message? It never happened, because we destroyed the Reflex Controller Element (the button) that was holding the instance of foo that was responsible for the Reflex. That includes the life-cycle events that make callbacks possible.

Now, it's very common to use data-reflex and data-controller on the same element. There's nothing inherently wrong with doing so - in fact, it's a solid go-to strategy for handling callbacks - unless your Reflex does something that results in the Reflex Controller Element being destroyed (think: innerHTML) or otherwise disconnected from your DOM.

INFO

The primary reason StimulusReflex, Phoenix LiveView and Laravel LiveWire all use the morphdom library for updates is to avoid destroying large chunks of your DOM when there are very good reasons not to do so - such as not _Keyser SΓΆze-_ing your Stimulus controllers.

It's notable that Turbo Streams chooses to use innerHTML over morphdom by default, which could ultimately result in a great many frustrated Stimulus developers. But there is also a way to use morphdom with Turbo Streams.

It's just a reality of UI design that sometimes when you present a table of rows that represent model records, clicking on the "Delete" button makes the row it lives on go away. We need the ability to delegate the responsibility for the Reflex and its life-cycle events to one of the Reflex Controller Element's ancestors; specifically, an ancestor that will survive whatever DOM mutations are caused by the Reflex. This brings us back full-circle to the original foo example:

html
<div data-controller="foo">
  <button data-reflex="click->Foo#remove">
    Remove this button
  </button>
</div>

With the ancestor div safely out of harm's way, we now see the desired console message:

As you can see, it's now possible to remove the Reflex Controller Element (the button) without losing your ability to have your StimulusReflex Controller still generating life-cycle events.

If your StimulusReflex Controller is getting SΓΆzed, you need to move further up your DOM.

Why no automatic foo Controller? ​

Now that you are a StimulusReflex Controller expert πŸ§™β€β™‚οΈ, you understand the reasons that the StimulusReflex library does not and can not automatically add a foo controller instance to an element with data-reflex="click->Foo#remove attribute:

  • Every Reflex Controller Element would be forced to also hold its own StimulusReflex Controller instance, maybe
  • That Controller might be foo or it might be stimulus-reflex depending on whether a foo_controller.js exists, which is an ugly ambiguity
  • There could be scenarios where you don't want a Reflex to run life-cycle callbacks
  • Delegating the StimulusReflex Controller element to an ancestor would be impossible 😱

It's not just that we prioritize flexibility over magic. To force automatic StimulusReflex Controllers would make most of the techniques described in this chapter impossible.

Released under the MIT License.