DEV Community

loading...

Stimulus 2.0 Value change callbacks. What are they good for?

borama profile image Matouš Borák ・6 min read

A few days ago, Stimulus 2.0 has finally been released. One of the key new features is the Values API that is supposed to replace the original ”Data API“ and become the main means of managing state in Stimulus controllers (besides Targets). The new API is documented in the Handbook as well as the Reference and I’ve also written about this feature before.

One thing about the Values API that I couldn’t wrap my head around were the Value changed callbacks. These are callback methods in the controllers that are called whenever the DOM data- attribute for the corresponding Value changes. I had no problem understanding them technically but couldn’t think of any real−world use case that would substantially benefit from this pattern. But the Basecamp people must have had a good reason to add this, they are not particularly known for frivolously adding new features!

So I gave it a deeper thought and eventually came up with a few usage patterns that make sense to me. So here they are:

Preventing code repetition

One of the patterns is actually mentioned in the Handbook itself. Stimulus 2 automatically generates getters and setters for all Values defined in the controller. Thanks to setters, we can now write this.someValue = 1234 and the corresponding data- attribute will get automatically updated in the DOM, saving the state for us.

Now, what if we need to run some JavaScript based on the Value state, perhaps to update the UI accordingly? We could run the code right after each setting of the Value, like this:

// The not very DRY pattern (Stimulus 2)
this.someValue = 1234
this.updateUIDueToSomeValue()
...
this.someValue = 5678
this.updateUIDueToSomeValue()
Enter fullscreen mode Exit fullscreen mode

but that results in a lot of repeated code that is easy to mess up.

Note that in Stimulus 1, we had to write a setter ourselves so we had a natural place to add such code:

// The deprecated pattern (Stimulus 1)
this.someValue = 1234
...
this.someValue = 5678

set someValue(value) {
  this.data.set("someValue", value)
  this.updateUIDueToSomeValue()
}
Enter fullscreen mode Exit fullscreen mode

In Stimulus 2, the setters are already ”baked-in“ outside our code and that’s where the change callbacks come to help: a Value change callback is a specially−named method that will be called with every change of the Value:

// The preferred pattern (Stimulus 2)
this.someValue = 1234
...
this.someValue = 5678

someValueChanged() {    // <-- this is called automatically, twice
  this.updateUIDueToSomeValue()
}
Enter fullscreen mode Exit fullscreen mode

Using change callbacks we can get our controllers to a similarly DRY shape as we were able to in Stimulus 1.

Responding to updates from legacy JavaScript code

This is all nice but change callbacks don’t just listen to Value updates in the controller code. They also trigger upon changes of the corresponding data- attributes in the DOM, i.e. they listen to external updates of the Values! This feature can be useful in many ways.

Suppose that our Stimulus controller has to interact with a page widget that is otherwise governed by some legacy JavaScript code. Be it some external JS library or a complex custom legacy JS, we’re talking about code that is not possible or easy to rewrite to Stimulus. We don’t need to make this JS code to talk to our controller directly, what we need is to make it update the data- attributes in the DOM instead.

There is a basic JS fiddle that demonstrates this (it’s a follow-up of a ”currency converter“ I’ve shown before). The legacy code is approximated here as a JS timer which, being triggered 5 seconds after the page load, changes the currency rates that the Stimulus controller calculates with. The relevant code snippet for this is the following:

// change to new currency rates after 5s
newInsaneRates = {
  ...
}

setTimeout(function() {
  document.getElementById('converter')
          .setAttribute('data-conversion-rates-value', 
                         JSON.stringify(newInsaneRates))
}, 5000)
Enter fullscreen mode Exit fullscreen mode

Just try running the fiddle, put a ”price“ in the input and watch the converted prices recalculate automatically after 5s. The trick here is that the timer code does not have to communicate directly with the controller, in fact, it does not even have to know that such a controller exists! All it has to do is update a data- attribute in the DOM and the controller’s change callback takes care of the rest.

Responding to asynchronous page updates from back-end

So far we’ve seen the change callbacks triggered by front-end events but this does not have to be the case − with a bit of help, callbacks can respond equally well to page updates originating from the back-end.

A notable example is StimulusReflex, the framework that uses ActionCable web sockets for asynchronous communication between front-end and back-end. Here, the actual means of data transfer between front- and back-end is not that important, more interesting is that StimulusReflex uses morphdom, an ingenious little library that can transform a part of the current page DOM efficiently into an updated HTML that it takes as input. The new HTML is typically rendered on the back-end and sent over the wire by StimulusReflex to the browser where the morphdom library morphs it into the current page (without reloading the page).

This way, the back-end can update a particular data- attribute of a Stimulus-controlled element, the attribute is morphed into the current client-side DOM and Stimulus automatically triggers the change callback for the corresponding Value. In effect, the back-end can control front-end behavior using Stimulus and its change callbacks.

By the way, DHH promises a ”New magic“ to be released soon that should bring rich client-side interactivity without having to write a ton of JavaScript (supposedly using a similar concept to StimulusReflex). If my guess (based on what we can see on the hey.com site) is right, the new magic will work comparably to StimulusReflex, only perhaps a bit less efficiently, as it probably will not use morphing but will replace whole updated elements. We’ll see soon!

Inter−controller communication

Sometimes it is useful to trigger a Stimulus controller action from another controller. Since Stimulus first came out, there have been a number of suggestions how to handle this interaction: triggering custom events, locating the controller via its element or exposing the controller in its element tag.

Now, it just came to my mind that the Values API (together with change callbacks) could be used for inter-controller communication, too. If any code, inside or outside a Stimulus controller, can affect a data- attribute value, it can also trigger behavior of the controller handling that value. Somehow it feels like triggering a (change) event in the target controller with a value parameter passed in.

Debugging controller behavior

Finally, it might as well be you who alters a controller-governed value, right in the Developer tools console! For a real−world example and if you have a Hey account, just try searching for something in the Imbox, open up the Dev Tools and search through the HTML for the data-search-selected-index-value attribute. You can now change its value and observe the effect - the bluish background selection will move among the results according to your input!

Hey.com Data API change

Conclusion

To conclude, Stimulus 2 change callbacks follow the classic Rails pattern of callbacks, this time in the client-side code. They watch updates on an element attribute and act according to its value. As with all callbacks on general, this leads to some indirection in the flow of the front-end code and a developer reading the corresponding HTML must be aware of the Stimulus conventions to know where to look for the related behavior. But hey, this is the Rails way!

If you’d like to read more stuff like this, follow me here or on my Twitter. Thanks!

Discussion (3)

pic
Editor guide
Collapse
leastbad profile image
leastbad

I am not being contrarian when I say that the valueChanged callbacks is my favourite feature of Stimulus 2.

One thing that might have escaped you so far is that the callbacks have two special properties. First, they fire before connect, meaning that you can use them to initialize your controller instance before it is even attached. That's already worth the price of admission. Second, the callback fires once if an initial value has been set, meaning that you can use it to "kick off" a behaviour, perhaps by setting a member variable that gets inspected by code in the connect method. Together, that makes this behaviour WILDLY powerful because it unlocks a whole category of new possibilities and next-level interactions.

I am clearly biased, but one of my favourite use cases so far is my stimulus-hotkeys controller.

What it does is use the values API to accept a JSON object that maps keyboard shortcuts to arbitrary methods on arbitrary Stimulus controllers, allowing you to create Gmail-style power-user shortcuts.

That's already cool, but with CableReady#set_dataset_property I can present users with the ability to specify their own custom key-mappings in realtime. If they change a mapped value, CableReady will take the current JSONB value for that user's key bindings and dynamically replace the data attribute on the hotkeys controller element. Thanks to the magic of the valueChanged callback, the changes take effect immediately.

My best advice is to shift from thinking about what valueChanged improves to what is possible that wasn't before.

Collapse
borama profile image
Matouš Borák Author

Hi leastbad, thanks for your comment! Unfortunately I have a very limited time to explore this topic further right now so I just wanted to tell you that if I sounded critical about the callbacks, I didn’t mean to, I am actually very excited about Stimulus 2, including the change callbacks. The fact that you can trigger a JS behavior solely via plain HTML is bewildering!

Also, this article was a purely theoretical exercise, and I hope I’ll diminish my ignorance a little bit once I get my feet wet on this in a real-world scenario. I’ll definitely explore the stuff you mentioned further. Thanks again!

Collapse
leastbad profile image
leastbad

It would be impossible to feel criticized, as I'm just a fellow user/traveller.