Rails Designer
Posted on November 28, 2024
This article was originally published on Rails Designer
🤩 Black Friday Alert! 📣 Rails Designer is now available with a discount up to 50% 🤑
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. In this article I want to explore those feature, I think you might not 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 return true or false).
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:
- update a counter for the number of search results (coming via turbo stream request);
- reorder (by alphabet) a list of people when they are dynamically added (assuming a turbo stream append/prepend);
- hide or show loading states based on a target connected (via a turbo stream).
This 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 actions 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 elements data-action. No good!
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 is 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. 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 let me know below.
Posted on November 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.