Article écrit par Clément Alexandre
Dans les présentations de Stimulus les mots qui reviennent le plus fréquemment sont sprinkle (le saupoudrage) et sparkle (le scintillement).
Ça serait dommage de passer à côté d’une telle poésie !
Quand on n’a pas besoin (ou pas les moyens) de réaliser une application Single Page doit-on systématiquement céder aux sirènes des frameworks tels que Vue React ou Angular pour ajouter un peu d’interactivité à notre interface ?
Ne pourrait-on pas se passer de data-binding de virtual DOM de système de templating ?
A modest framework for the HTML you already have.
Là où la plupart des frameworks JavaScript sont conçus avec une approche de composants à intégrer aux templates HTML Stimulus propose au contraire d’aider à greffer vos fonctionnalités JavaScript sans altérer le HTML.
En quelques mots on va ajouter des attributs à nos balises pour décrire les comportements que l’on souhaite mettre en œuvre.
Oubliez les templates JSX et les styles CSS embarqués dans le JavaScript en composants. Ici on doit toujours organiser son front-end « classiquement » et utiliser d’autres astuces pour qu’il soit bien maintenable.
C’est assez transparent lorsqu’on travaille en progressive enhancement c’est-à-dire ajouter de l’interactivité à quelque chose qui fonctionne déjà sans JavaScript.
Si vous êtes allergique aux data-attributes bien fournis tant pis pour nos statistiques de blog : FUYEZ !
Au début il y avait jQuery
Malgré tout le mal que l’on entend légitimement sur jQuery il aura eu le mérite d’uniformiser le comportement des navigateurs en proposant d’habiles raccourcis pour sélectionner les éléments DOM écouter les événements ou encore faire de jolis effets visuels. C’est également l’un des premiers vecteurs ayant permis la diffusion de milliers de plugins ancêtres des composants à intégrer dans les frameworks « modernes » (composants qui utilisent d’ailleurs parfois jQuery en sous-main ; la boucle est bouclée).
Si on souhaite ne se tenir qu’à un pas du JavaScript vanille on va voir que Stimulus peut séduire assez facilement.
On reprend les bases créons un écouteur
Attention cette section est une caricature (tirée par les cheveux) faisant appel à votre imagination pour faire référence à une vraie problématique que vous avez ou pourriez rencontrer.
On a décidé d’être roots et d’oublier tous les frameworks du moment.
Naturellement on souhaite créer un élément qui nous salue chaleureusement lors d’un clic :
document.addEventListener("DOMContentLoaded" function(event) { for (let greet of document.getElementsByClassName("greet")) greet.addEventListener("click" function() { alert("Bonjour !"); }) } });
Petit problème : si un élément <div class="greet">
est créé après le chargement de la page il ne sera pas réactif au clic.
C’est par exemple le cas si une portion interactive de la page a été chargée de manière asynchrone en AJAX ou via les Turbolinks. Il faut alors prévoir de redéclarer les écouteurs au moment où la portion de page est ajoutée.
À l’heure où les voitures autonomes n’écrasent presque plus les piétons on aimerait bien se concentrer sur d’autres problématiques.
Bonne nouvelle L’API DOM expose une solution pour dynamiser tout ça : il s’agit du MutationObserver
(introduit en 2012).
En quelques mots on va observer la création de nouveaux nœuds dans le DOM et ainsi pouvoir créer les écouteurs de clic à ce moment-là.
Mauvaise nouvelle c’est un peu pénible.
var callback = function(mutationsList) { for (let mutation of mutationsList) { if (mutation.type == "childList") { for (let el of mutation.addedNodes) { if (el.classList.contains("greet")) { el.addEventListener("click" function() { alert("Bonjour !"); }) } } } } }; var observer = new MutationObserver(callback); observer.observe(document { childList: true });
Au niveau des perspectives de complexité dans un projet plus complet le remède semble presque pire que le mal.
On me rappelle dans l’oreillette que jQuery sait très bien faire ce genre de choses avec la méthode $(document).on(eventName elementSelector handler)
. Mais pourquoi donc chercher les complications si jQuery et ses 265 kB peuvent le faire ?
Peut-être parce que le reste de la bibliothèque ne nous intéresse pas et que cette implémentation précise n’est pas la plus optimale de la bibliothèque.
Stimulus à la rescousse
Si je devais décrire mon usage de Stimulus : je l’utilise là où j’aurais mis du jQuery il y a quelques années ou bien lorsque j’ai envie de créer des petits modules JavaScript non intrusifs et bien rangés.
Parce qu’à part mettre à disposition son architecture il faut admettre que Stimulus ne fait pas grand-chose. Si vous cherchez à réaliser de beaux effets de transition il ne vous sera d’aucune pas d’une grande aide.
En revanche il se prête bien à « abstraire » et organiser l’usage d’autres bibliothèques tout en évitant de retrouver des id
ou des class
destinées au JavaScript dans le HTML. On verra ça un peu plus bas dans l’article.
On reprend le code !
Voici un contrôleur Stimulus (nous verrons comment préparer l’environnement avec Webpack juste après) :
// fichier "controllers/hello_controller.js" import { Controller } from "stimulus" export default class extends Controller { connect() { this.element.addEventListener("click" => { alert("Bonjour !"); }); } }
Désormais tous les éléments HTML répondant à l’attribut data-controller="hello"
présents dans le DOM (et ceux créés ultérieurement) ouvriront un popup vous saluant lors d’un clic. En effet Stimulus va donc discrètement utiliser MutationObserver
pour que tout fonctionne comme on s’y attend.
Vous remarquerez que notre contrôleur n’est pas nommé ni enregistré dans Stimulus alors qu’il est automagiquement associé au data-controller="hello"
.
C’est rendu possible par l’usage d’un helper Webpack qui va inférer le nom du contrôleur depuis le nom du fichier dans lequel il est déclaré. C’est ce qui est proposé dans le mode de fonctionnement par défaut que j’ai recopié ci-dessous :
// src/application.js import { Application } from "stimulus" import { definitionsFromContext } from "stimulus/webpack-helpers" const application = Application.start() const context = require.context("./controllers" true /.js$/) application.load(definitionsFromContext(context))
Tous les contrôleurs présents dans le dossier controllers
seront donc chargés enregistrés et prêts à l’emploi sans autre action de notre part. C’est piégeux pratique : on peut ajouter ou supprimer un fichier contrôleur sans qu’il n’y ait d’incidence sur le reste du code JavaScript.
N.B. : Il est également possible de se passer de Webpack nous le verrons en fin d’article.
Pour en revenir à nos moutons il est à noter que je n’ai volontairement pas encore utilisé le gestionnaire d’événements qui s’utiliserait ainsi :
// fichier "controllers/hello_controller.js" import { Controller } from "stimulus" export default class extends Controller { greet() { alert("Bonjour !") } } // de paire avec une balise // <div data-controller="hello" data-action="click->hello#greet" />
Emballage
Je vous accorde que tout ça n’a rien de forcément très impressionnant. Mais c’est pratique !
Imaginons maintenant qu’on souhaite faire appel à une bibliothèque qui crée un graphique.
On pourra apprécier de pouvoir isoler la logique et les dépendances propres à cette fonctionnalité dans un contrôleur.
Un petit aperçu :
// fichier controllers/graphique_controller.js import Chart from "chart.js" import { Controller } from "stimulus" export default class extends Controller { static targets = [ "canvasElement" ] connect() { let contex = this.canvasElementTarget.getContext("2d"); this.chart = new Chart(contex) this.chart.data = JSON.parse(this.data.get("donnees")) this.chart.update() } }
Dans notre fichier HTML
<div data-controller="graphique" data-graphique-donnees="[1.2 2.3 3.8 2 4]"> <legend>Ma progression avec Stimulus</legend> <canvas data-target="graphique.canvasElement"/> </div>
Ici
data-graphique-donnees
est une convention parce que le contrôleur s’appelle graphique. On retrouve les données viathis.data.get("donnees")
dans le contrôleur associé (on s’assure ainsi au passage que le contrôleur n’accède qu’aux données qui le concernent). En prime le contrôleur n’a pas besoin de connaître son nom : dans un autre contexte il aurait fallu faireelement.dataset.graphiqueDonnees
ce qui aurait créé une forme de couplage dans le code JavaScript.
Attention cedata-attribute
n’est pas dynamique : si on change son contenu avec du JavaScript il faudra implémenter une méthode pour mettre le graphique à jour.
Hop ! Le reste de l’application n’a pas conscience de l’usage de la bibliothèque Chart.js
on sait facilement où retrouver notre fonctionnalité et on pourrait changer Chart.js
par HighCharts
sans rien modifier d’autre (il serait en réalité judicieux de créer l’élément canvas
dans la méthode connect()
).
On pourrait bien sûr parvenir à ce résultat en créant un petit module à la main mais l’idée ici est d’apporter un peu de « normalisation ».
Les « vraies » fonctionnalités
Stimulus fournit des fonctionnalités très basiques.
Parmi elles comme on l’a vu des raccourcis pour la sélection d’éléments (via les targets).
Il propose également de simplifier la gestion des événements (avec data-action="click->mon-controller#monAction"
) et quelques autres raccourcis notamment pour accéder aux data-attributes sans sortir des platebandes du contrôleur.
Sachez qu’on peut lier plusieurs contrôleurs à un seul élément :
<div data-controller="graphique hello">...</div>
Également les contrôleurs peuvent s’imbriquer entre eux y compris de manière récursive.
Par exemple dans l’exemple posté ici :
<div data-controller="collapsible"> <div data-action="click->collapsible#toggleBody">Outer Collapsible</div> <div data-target="collapsible.body"> <div>Outer Content</div> <div data-controller="collapsible"> <div data-action="click->collapsible#toggleBody">Inner Collapsible</div> <div data-target="collapsible.body"> <div>Inner Content</div> </div> </div> </div> </div>
On peut réduire distinctement l’inner content et l’outer content : l’événement click
dans le contrôleur enfant n’est à cet effet pas remonté au contrôleur parent (et inversement).
Faire communiquer les contrôleurs entre eux n’est pas forcément souhaité ici bien que ça soit possible.
Par exemple un contrôleur parent peut intercepter un événement personnalisé lancé par un contrôleur enfant.
On peut également obtenir une référence vers un contrôleur et déclencher ses méthodes depuis l’extérieur avec la méthode application.getControllerForElementAndIdentifier(monElement "mon-controller")
.
La référence vers
application
s’obtient ici :import { Application } from "stimulus" ; application = Application.start()
Ou via this.application
depuis un contrôleur.
Par où commencer ?
Même si Webpack n’est pas requis il simplifie pas mal la vie en permettant de découvrir tous les contrôleurs sans avoir à les déclarer (il suffit de les ranger dans un dossier et « d’importer » ce dernier).
Si vous utilisez Webpack vous pouvez donc directement cloner le dépôt starter et suivre les instructions.
Si vous utilisez un autre gestionnaire d’assets rien n’est perdu il s’agira simplement d’enregistrer chaque contrôleur à la main (comme dans la plupart des autres frameworks JavaScript).
Enfin il est même possible de complètement se passer de gestionnaire d’assets.
On fait très vite le tour de la documentation de Stimulus l’outil en lui-même étant très simple. La prise en main est donc quasi instantanée.
Poids et compatibilité
Telle quelle la bibliothèque pèse 57 6 kB. Minifiée et gzippée c’est un poids-plume de moins de 6 kB.
La bibliothèque est compatible avec les navigateurs > IE11. Il est possible de la rendre compatible avec IE11 au coût d’un polyfill de MutationObserver
pour quelques kilo-octets supplémentaires.
C’est à vous
Un… deux… trois… pâtissez saupoudrez !