Go to Hackademy website

Stimulus : Mettez des paillettes dans votre HTML

Clément Alexandre

Posté par Clément Alexandre dans les catégories

Cet article est publié sous licence CC BY-NC-SA

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 via this.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 faire element.dataset.graphiqueDonnees, ce qui aurait créé une forme de couplage dans le code JavaScript.
Attention, ce data-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 (exemple ici).

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, comme c’est indiqué ici.

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 !


L’équipe Synbioz.
Libres d’être ensemble.