Stimulus is advertised as a modest framework for the HTML you already have. It still packs quite a few features that you (didn’t) know (or have forgotten about).
Existential properties
Every API in stimulus (targets, classes, values and outlets) has the existential attribute option. Meaning you can check if an attribute is available.
hasButtonTarget
;hasButtonClass
;hasLabelValue
;hasActionsOutlet
.
You can do your logic based off of that boolean value (it returns true or false).
update() {
if (!this.hasButtonTarget) return;
this.buttonTarget.classList.add(this.hasButtonClass ? this.buttonClass : "btn");
}
Connected and disconnected callbacks for targets
You most likely know about the connect and disconnect lifecycle methods for Stimulus classes. But there are also [name]TargetConnected()
and [name]TargetDisconnected()
method. These are called when a target becomes connected or disconnected.
This has many use cases, when using with, for example, Turbo Streams as responses:
- update a counter for the number of search results;
- reorder (by alphabet) a list of people when they are dynamically added;
- hide or show loading states based on a target connected.
This is how it is used:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["item", "count"];
// …
itemTargetConnected() {
this.#updateItemCount();
}
itemTargetDisconnected() {
this.#updateItemCount();
}
// private
#updateItemCount() {
this.countTarget.textContent = `(${this.itemTargets.length})`;
}
}
Note that the [name]TargetDisconnected()
method gets fired before the disconnect()
lifecycle method.
I wrote an article on Connected and Disconnected Target Callbacks with Stimulus if you want to learn more.
Passing params to actions
Sometimes you need to pass some attribute to a certain method. That’s easy and works like this:
<div data-controller="theme">
<button data-action="theme#update" data-theme-value-param="dark">
Lights Off
</button>
</div>
Then the update method will look like this:
update({ params: { value } }) {
this.#setClass(value);
}
It follows this structure data-[identifier]-[param-name]-param
. It can take any type for a value from a String, Number to an Object and a Boolean. Refer to the docs for more.
Prevent default
Talking about actions. There are various action-options available. :prevent
is one of them. You probably have used event.preventDefault()
in your methods. Stimulus provides a shortcut for it. Let’s take this example:
<input
type="text"
data-controller="input"
data-action="keypress->input#validate:prevent"
placeholder="Numbers only"
>
export default class extends Controller {
validate(event) {
if (/[0-9]/.test(event.key)) {
event.target.value += event.key
}
}
}
Without :prevent
(preventDefault()), invalid characters (non-numbers) would still appear in the input even after the validation check.
Stop propagation
There is also :stop
. This is a shortcut for event.stopPropagation()
within the controller’s action/method. This prevents the event from bubbling up through the DOM. An example is probably easier:
<div data-controller="dropdown" data-action="click@window->dropdown#hide">
<button data-action="dropdown#toggle:stop">
Toggle Dropdown
</button>
<div data-dropdown-target="menu">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Logout</a>
</div>
</div>
Without :stop
the click event would bubble up the DOM and reach the hide action defined in the parent element’s data-action. No good!
👀 Events bubbling up? Prevent Default? Stop propagation? Do you sorta know what it does, but not exactly? Pre-order JavaScript for Rails Developers.
Self
Another custom action option is self
. It will only fire when on the element itself, not its children.
<div data-controller="dropdown">
<div data-action="click->dropdown#toggle:self">
Menu Header
<ul class="menu-items">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</div>
In above example toggle
will only be fired when Menu Header
is clicked, not when .menu-items
(or any of its children) is clicked.
Check out this article if you are interested in creating your own custom action options? Or see stimulus-fx a collection of custom action options I built. ☺️
Plural CSS classes
If you want to add/remove or toggle a CSS class. The classes API is great for that. Stimulus allows you to add multiple CSS classes at once, you just need to know the convention and a bit of JavaScript. It works like this:
export default class extends Controller {
static classes = ["scrolling"];
scroll() {
this.element.classList.add(...this.scrollingClasses);
}
}
I wrote an article that goes into details about toggling multiple CSS classes with Stimulus.
Nested Scopes
Each controller in Stimulus operates within its own isolated scope. This means it can only interact with targets defined in its immediate context. Parent controllers cannot access targets from nested child controllers, and vice versa.
Wanna see code? I heard you like tabs in your tabs:
<div data-controller="tabs">
<div data-tabs-target="panel">
<!-- This panel is found by the parent tabs controller -->
</div>
<div data-controller="tabs">
<div data-tabs-target="panel">
<!-- This panel is only found by the nested tabs controller -->
</div>
</div>
</div>
This might be something you stumbled upon by accident (or frustration?), but it still a good feature to know about.
shouldLoad
method
Do you want certain features to only work on mobile devices, or maybe you have some special functionality just for touch devices? You can conditionally load a controller with shouldLoad
. Simply like this:
export default class extends Controller {
static get shouldLoad() {
return window.innerWidth <= 768 && "ontouchstart" in window
}
}
This controller won’t be registered and loaded at all unless the above conditions match. So there won’t be any creation of the controller instance. This is different from, say adding return false
to your connect lifecycle method, where an instance is created.
afterLoad
method
Sometimes you need to run some setup code right after a controller is registered, regardless of whether any elements are using it yet (connected). afterLoad
is perfect for this.
Again, a code snippet might help:
export default class extends Controller {
static afterLoad(identifier, application) {
// anything you need to get done here
}
}
Honestly, I have thought about afterLoad()
for a fair bit and haven’t come up with a real use-case for it. I’ve simply included as it’s semi-related to shouldLoad()
and it’s good to know it’s there. So if you find a use-case for it, please contact me.
That’s all the features of Stimulus you (didn’t) know.
What is your score out of these 10?