Stimulus.js : Outlets, Values et cycle de vie des contrôleurs
Values : au-delà des simples data-attributes
L'API values ne se contente pas de lire des chaînes : elle type et sérialise automatiquement les données selon le type déclaré (String, Number, Boolean, Array, Object). Chaque changement déclenche un callback nomValueChanged, ce qui permet de réagir à une mise à jour sans réécrire de logique de diff.
// slider_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { index: { type: Number, default: 0 } }
indexValueChanged(value, previousValue) {
this.element.style.setProperty("--slide", value)
}
next() {
this.indexValue++
}
}
Point clé : la valeur par défaut se déclare avec un objet { type, default }, pas juste le type nu, si vous voulez éviter les undefined côté HTML.
Outlets : communiquer entre contrôleurs sans bus d'événements
Avant les Outlets, faire parler deux contrôleurs obligeait à passer par des événements DOM ou à interroger le application global. Les static outlets résolvent ça proprement : un contrôleur déclare une dépendance vers un autre, identifié par sélecteur CSS et attribut data-*-outlet.
export default class extends Controller {
static outlets = ["cart-item"]
removeSelected() {
this.cartItemOutlets.forEach(item => {
if (item.selectedValue) item.remove()
})
}
}
Les callbacks cartItemOutletConnected(outlet, element) et ...Disconnected permettent de réagir dynamiquement à l'apparition ou la disparition d'un contrôleur cible, ce qui est particulièrement utile avec du contenu injecté via Turbo Streams.
Cycle de vie : ne pas confondre connect et targetConnected
- connect() : appelé une fois que le contrôleur est attaché à l'élément et que le DOM est prêt.
- nomTargetConnected(target) : appelé à chaque fois qu'une cible entre dans le DOM, y compris après le montage initial — essentiel pour des listes qui se remplissent de façon asynchrone.
- disconnect() : à utiliser pour nettoyer des timers, observers ou abonnements, car Stimulus ne le fait pas pour vous.
Actions avancées
Les descripteurs d'action supportent des filtres clavier (keydown.enter->form#submit) et des options passées en paramètres via data-form-id-param, récupérables dans le handler via { params }. Cela évite de repasser par des data-* lus manuellement avec dataset.
<button data-action="click->cart#add"
data-cart-product-id-param="42">Ajouter</button>
add({ params: { productId } }) {
this.dispatch("added", { detail: { productId } })
}
Le couple dispatch + CustomEvent reste la bonne pratique pour notifier l'extérieur d'un contrôleur, à réserver aux cas où les Outlets seraient trop couplés.