Le terme Web Component regroupe trois technologies natives :
- Les Custom Elements
- Le Shadow DOM
- Les éléments HTML
<template>
et<slot>
Ces trois concepts seront introduits successivements par les trois exercices.
Les Custom Elements permettent de créer des éléments HTML sur mesure et de définir leur contenu et leur comportement.
- Créez un fichier
index.html
avec une structure basique. - Créez un fichier
src/current-time.js
et importez-le dans la balise<head>
du fichier HTML. Utilisez l'attributdefer
pour ne pas bloquer le parsing du fichier HTML. - Ouvrez votre application dans le navigateur grâce à l'extension Live Server de VSCode et vérifiez le bon lien entre les deux fichiers.
Nous allons créer un composant affichant la date actuelle :
- Dans le fichier JavaScript, déclarez une classe
CurrentTime
qui étend la classe nativeHTMLElement
. - À la suite de la classe, grâce à la fonction
customElements.define("nom-du-tag", NomDeLaClasse)
, associez votre classe au tagcurrent-time
. - Utilisez le tag
current-time
dans votre fichier HTML. - Déclarez la méthode
connectedCallback
dans votre classe. Au sein de cette méthode, utilisezthis.innerHTML
pour ajouter du contenu à votre tag.
Le contenu doit s'afficher dans votre page.
- Remplacez votre contenu de test par le vrai contenu, soit une div de classe
currentTime
, contenant :- un élément
<p>
de classecurrentTime__title
et contenant la chaîneHeure locale
; - un élément
<time>
de classecurrentTime__time
, vide.
- un élément
- À la suite de cette opération, gardez une référence vers l'élément
<time>
dans la classe, nous en aurons besoin. Trouvez l'élément grâce àthis.querySelector
et stockez le dansthis.$time
. - Insérez la date dans l'élément
<time>
. Elle peut être obtenue grâce à l'expressionnew Date().toLocaleString()
. - La date doit désormais s'afficher de manière statique (sans se mettre à jour).
- Ajoutez quelques styles via un fichier CSS global inclus dans
index.html
.
On souhaite que la date se mette à jour à chaque seconde.
- Trouvez un moyen pour mettre à jour le contenu de
<time>
à chaque seconde (sans rafraîchir l'intégralité du contenu !) - En utilisant la fonction
disconnectedCallback
, assurez-vous que la fonction de mise à jour ne sera plus appelée "dans le vide" après la disparition du composant. Pour le vérifier, vous pouvez supprimer le composant directement dans l'inspecteur et observer les effets (à l'aide deconsole.log
par exemple).
Notre composant va désormais être capable d'accueillir un paramètre format
. Si
ce format vaut utc
, on souhaite afficher l'heure UTC. Pour toute autre valeur,
y compris l'absence de valeur, on reste sur le comportement initial (heure
locale).
- Dans votre HTML, ajoutez ce composant une deuxième fois, en passant cette
fois-ci l'attribut
format="utc"
. Un deuxième composant (pour l'instant identique) apparaît. - Dans votre fonction de rendu, utilisez
this.getAttribute("format")
pour adapter :- le titre du composant (
Heure UTC
ouHeure locale
) - le contenu de l'élément
<time>
(new Date().toUTCString()
ounew Date().toLocaleString()
)
- le titre du composant (
Les deux composants ont désormais un comportement différent !
Que se passe t-il lorsque vous inspectez le DOM et ajoutez / retirez
manuellement l'attribut format="utc"
?
Le format se met à jour (car il est recalculé à chaque seconde), mais pas le titre !
Si l'on souhaite réagir aux changements d'attributs, des adaptations sont nécessaires :
- Déclarez les attributs pris en compte par votre composant grâce à la variable
static observedAttributes
, qui doit être un tableau contenant le nom des attributs concernés. - Grâce à la méthode
attributeChangedCallback(name, oldVal, newVal)
, lorsque vous détectez un changement sur l'attributformat
, faites le nécessaire pour que la mise à jour soit correcte.
Le Shadow DOM permet d'isoler le contenu d'un composant du reste de la page. Cela n'est pas toujours nécessaire, mais peut être pratique dans certains cas.
Le composant <screen-size>
se comporte comme suit :
- il est flottant en haut à droite de l'écran
- il indique en permanence la largeur du viewport
- la largeur est indiquée en
px
ou enrem
- un bouton permet d'alterner entre les deux unités
- le composant accepte un attribut
unit
pour paramétrer son unité initiale (ou utilise la valeurpx
par défaut) - on ne lui demande pas de réagir au changement de valeur de l'attribut
unit
par la suite
En voici un aperçu :
Construisez ce composant en vous inspirant du composant <current-time>
, mais
cette fois-ci en utilisant le Shadow DOM (soit en deux temps, soit
directement).
Note : pour obtenir la largeur de la fenêtre en REM, vous pouvez utiliser l'instruction suivante :
window.innerWidth /
parseInt(getComputedStyle(document.body).getPropertyValue("font-size"));
- Toujours
connectedCallback()
, le Shadow DOM est crée grâce à l'instructionthis.attachShadow({ mode: "open" });
- Les
querySelector
ne s'éxécutent plus surthis
mais surthis.shadowRoot
.
Vous ne pouvez pas appliquer des styles depuis votre fichier global (essayez !). Le shadow DOM encapsule ses propres styles.
Vous devez donc déclarer les styles directement dans le template, avec une
balise <style>
(par exemple injectée via innerHTML
).
En contrepartie, les styles que vous déclarez dans un composant utilisant le shadow DOM ne peuvent pas "fuiter". Il est donc possible de se passer de classes et de cibler les éléments HTML sans s'inquiéter de potentiels conflits.
Vous pouvez cibler l'élément qui contient le web component grâce au sélecteur
:host
.
- Dans la console, cherchez les boutons présents sur la page :
document.querySelector("button")
. Qu'observez-vous ? - Cherchez à présent
document.querySelector("screen-size").shadowRoot.querySelector("button")
et comparez le résultat.
Dans cette partie, nous ferons usage de l'élément HTML <details>
. Sa
compréhension est requise pour la suite (lire la
documentation MDN).
La balise <template>
permet de définir un fragment de HTML, qui n'est pas
affiché de base dans la page mais dont on peut se servir pour créer des
composants.
Ajoutez le code suivant à votre fichier HTML :
<template id="custom-details">
<details>
<summary>Cliquez pour ouvrir</summary>
<div>Je suis le contenu détaillé</div>
</details>
</template>
Comme vous pouvez le constater, le résultat n'est pas affiché dans le navigateur.
Nous allons créer un composant <custom-details>
qui étend le comportement de
base du composant <details>
natif.
Comme précédemment, créez une classe CustomDetails
et associez là au composant
custom-details
.
Comme précédemment, au sein du constructeur, générez un shadow DOM.
Mais cette fois-ci, le contenu du composant n'est pas géré au sein de la classe. Pour le récupérer, utilisez la ligne suivante :
const template = document.getElementById("custom-details").content;
Puis attachez le contenu du template au shadow DOM :
this.shadowRoot.appendChild(template.cloneNode(true));
En ajoutant un élément <custom-details>
à la page, vous devriez désormais voir
le contenu du template.
Complétez le code de la classe pour mettre en place les interactions suivantes :
- ouverture de l'élément au survol
- ouverture de l'élément au focus
- fermeture de l'élément à l'appui sur la touche Échap
Les <slots>
vont permettre de passer du contenu complexe de l'extérieur vers
l'intérieur du composant.
Dans notre cas, il faut en effet passer le contenu du tag <summary>
(qui peut
être une simple string
) et le contenu dévoilé (qui est souvent un contenu plus
complexe).
Dans un template, il est possible d'ajouter un slot de cette façon :
<template>
...
<slot name="summary"></slot>
...
</template>
À cet endroit sera inséré l'élément associé de cette façon :
<custom-details>
<span slot="summary">Les 3 technologies des Web Components</span>
</custom-details>
Ajoutez les slots summary
et content
. Pour l'exemple, passez une liste
<ul>
en contenu.
Vérifiez le bon fonctionnement.
Stylisez le composant final en tenant compte des informations suivantes (effectuez des tests au fur et au mesure) :
- Une balise
<style>
insérée au sein de la balise<template>
permet de styliser le shadow DOM. - Les éléments slottés ne font pas partie du shadow DOM. Ainsi, ils ne sont pas impactés par les styles du composant, mais ils sont impactés par les styles globaux de la page.
- Entre ces deux concepts, il est possible de cibler des éléments slottés dans
les styles du shadow DOM. Par exemple, le sélecteur
::slotted(ul)
cible les listesul
qui sont slottées dans le composant. - Les enfants de composants slottés restent, eux, innaccessible.
L'objectif est de réaliser une Todo List de ce type :
- Vous pouvez utiliser ou non le shadow DOM, selon ce qui vous parle le plus ;
- Découpage en plusieurs composants ;
- Le style est libre (mais il en faut un minimum !) ;
- Mobile first (styles pour écrans élargis optionnels) ;
- Le résultat devra être accessible. Testez pour cela (ordre recommandé) :
- Le résultat du plugin axe DevTools
- La navigation clavier
- Le lecteur d'écran de votre système
C'est une application simple mais il peut y avoir beaucoup de subtilités au niveau des interactions. Prenez le temps d'aller au fond du sujet.
Vous pouvez voir mon implémentation ici. Mais ne copiez pas son code, cela n'aurait pas d'intérêt !
Pour commencer votre implémentation, ne vous pré-occupez pas du stockage : vos todos seront effacées à chaque rechargement.
Il vous faudra quand même dès le départ une méthode pour que tous vos composants puissent partager la même liste de todos.
Pour cela, une méthode courante est l'utilisation d'un Store
, en l'occurence
une simple classe qui sera chargée de stocker et fournir la liste des todos, une
todo spécifique, le nombre de todos restantes, etc... Un peu comme une base de
données qui ne vit que le temps de la page.
Voici un coup de pouce pour le départ. Dans un fichier todo-store.js
, ajoutez le
code suivant :
class TodoStore {
constructor() {
this.todos = [];
}
getTodos() {
return this.todos;
}
}
export const todoStore = new TodoStore();
Vous avez désormais accès à la variable todoStore
, une instance unique de la
classe TodoStore
au travers de l'application.
Dans un composant, vous pouvez l'appeler ainsi :
import { todoStore } from "./todo-store.js";
// Plus loin, dans votre composant...
todoStore.getTodos();
Il vous faudra ajouter toutes les fonctions nécessaires à votre Store
, et
trouver comment faire en sorte que les composants puissent réagir aux changement
dans la liste des todos !
La touche finale, sans quoi votre application n'est pas très utile : stocker la liste en local.
Au moins deux approches sont possible :
localStorage
: il s'agit alors d'utiliserJSON.stringify()
etJSON.parse()
pour convertir les objets à stocker enstring
(seul format supporté parlocalStorage
). Basique, mais ça fait le job !IndexedDB
: une vraie base de données, performante et aynchrone, directement dans le navigateur, avec stockage d'objets complexes. Si vous vous sentez d'essayer, ça vaut le coup !