Le cycle de vie d'un contrôleur
Un contrôleur Stimulus n'est pas juste un objet avec des méthodes connect() et disconnect(). Le MutationObserver interne détecte l'ajout ou le retrait de l'attribut data-controller, mais aussi les changements sur les targets et les values. Chaque valeur déclarée dans static values génère automatiquement un callback nomValueChanged(nouvelleValeur, ancienneValeur), appelé une première fois à la connexion. C'est ce mécanisme, plus que connect(), qui doit porter la logique d'initialisation dépendante d'un état.
Exemple : un compteur réactif aux valeurs
// counter_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["output"]
static values = { count: { type: Number, default: 0 } }
countValueChanged(value) {
this.outputTarget.textContent = value
}
increment() {
this.countValue++
}
}
Ici, countValueChanged synchronise le DOM à chaque mutation de l'attribut data-counter-count-value, y compris si un autre script ou un autre contrôleur modifie cet attribut directement. C'est la base d'une architecture où l'état vit dans le DOM, pas dans des variables JS éphémères.
Les Outlets : coupler sans importer
Avant Stimulus 3, faire communiquer deux contrôleurs obligeait souvent à passer par des événements custom ou à interroger le DOM manuellement. Les outlets résolvent ce problème proprement :
static outlets = ["counter"]
reset() {
this.counterOutlets.forEach(c => c.countValue = 0)
}
Déclarer data-controller="parent" avec data-parent-counter-outlet pointant vers un sélecteur CSS suffit : Stimulus résout la référence, gère la connexion/déconnexion dynamique, et expose des callbacks counterOutletConnected(controller, element) symétriques au cycle de vie du contrôleur ciblé.
Pièges fréquents à la relecture
- Actions avec options inline : la syntaxe
data-action="click->controller#method:prevent"évite unevent.preventDefault()manuel, souvent oublié dans les revues de code. - Targets multiples : préférer
xxxTargets(pluriel) à une boucle manuelle surquerySelectorAll, qui casse l'encapsulation du contrôleur. - Classes API (
static classes) : permet de nommer les classes CSS appliquées côté HTML plutôt que de les coder en dur dans le JS, ce qui facilite le theming sans toucher au contrôleur.
Avec Turbo, ces mécanismes prennent tout leur sens : un contrôleur doit rester idempotent face à des reconnections répétées lors des navigations morphing, d'où l'intérêt de tout piloter par values plutôt que par état interne.