Les Custom Elements : Créer vos premiers composants
Bienvenue dans cette leçon dédiée aux Custom Elements, une brique essentielle de la spécification des Web Components. Après avoir abordé les fondations des Web Components, nous allons maintenant plonger au cœur de la création de vos propres balises HTML. Les Custom Elements vous permettent d'étendre le langage HTML en définissant de nouvelles balises avec des fonctionnalités et un comportement spécifiques, transformant ainsi votre code en composants modulaires, réutilisables et interopérables.
1. Introduction aux Custom Elements
Imaginez pouvoir créer votre propre balise <mon-bouton-interactif> ou <carte-produit> qui encapsule toute la logique, le style et la structure nécessaires. C'est exactement ce que les Custom Elements vous permettent de faire ! Ils constituent l'un des trois piliers des Web Components, les deux autres étant le Shadow DOM (pour l'encapsulation du style et de la structure) et les HTML Templates (pour la réutilisabilité des structures de code).
L'objectif principal des Custom Elements est de standardiser la création de composants réutilisables. Plutôt que de dépendre de frameworks JavaScript pour cette modularité, les Custom Elements offrent une solution native et universellement supportée par les navigateurs.
Pourquoi utiliser les Custom Elements ?
- Réutilisabilité : Créez des composants une seule fois et utilisez-les partout dans votre application ou même dans d'autres projets.
- Encapsulation : Bien qu'il nécessite souvent le Shadow DOM pour une encapsulation complète, le Custom Element lui-même encapsule le comportement (JavaScript).
- Interopérabilité : Vos composants fonctionnent avec n'importe quel framework JavaScript (React, Vue, Angular, Svelte, etc.) ou même sans aucun framework, car ils sont basés sur des standards web.
- Lisibilité : Un code HTML plus sémantique et plus facile à comprendre grâce à des balises personnalisées significatives.
- Maintenance : Moins de code dupliqué et une gestion plus simple des mises à jour des composants.
2. Qu'est-ce qu'un Custom Element ?
Un Custom Element est une nouvelle balise HTML que vous définissez et dont vous contrôlez le comportement. C'est une instance d'une classe JavaScript que vous créez et qui hérite de HTMLElement.
Exemple : Au lieu d'écrire :
<div class="card">
<h2>Titre</h2>
<p>Description</p>
<button>Action</button>
</div>
Vous pourriez simplement écrire :
<ma-carte titre="Titre" description="Description" action-label="Action"></ma-carte>
où <ma-carte> est votre Custom Element.
Naming Conventions (Conventions de nommage)
Il est obligatoire que le nom d'un Custom Element contienne au moins un tiret (-). Cette règle permet de s'assurer qu'il n'y aura jamais de conflit avec les futures balises HTML natives ajoutées par les spécifications du W3C.
Exemples valides : <mon-composant>, <bouton-action>, <user-profile-card>.
Exemples invalides : <moncomposant>, <bouton>.
3. Anatomie d'un Custom Element
La création d'un Custom Element implique généralement deux étapes :
- Définir la classe JavaScript qui décrit le comportement de votre élément. Cette classe doit étendre
HTMLElement. - Enregistrer l'élément auprès du navigateur en associant votre classe JavaScript à un nom de balise HTML spécifique.
3.1 Définition de la classe JavaScript
Votre classe JavaScript contiendra la logique de votre composant. Elle peut inclure des méthodes, des propriétés et des "callbacks" du cycle de vie que le navigateur appellera à des moments précis.
class MonPremierElement extends HTMLElement {
constructor() {
// Toujours appeler super() en premier dans le constructeur des classes Web Components.
super();
// Initialisation des propriétés internes, etc.
console.log("MonPremierElement a été construit !");
}
// Des méthodes du cycle de vie (nous y reviendrons)
connectedCallback() {
console.log("MonPremierElement a été ajouté au DOM.");
this.innerHTML = `<p>Bonjour du composant <strong>MonPremierElement</strong> !</p>`;
}
disconnectedCallback() {
console.log("MonPremierElement a été retiré du DOM.");
}
}
3.2 Enregistrement de l'élément
Une fois votre classe définie, vous devez l'enregistrer auprès du registre des Custom Elements du navigateur en utilisant la méthode customElements.define().
customElements.define('mon-premier-element', MonPremierElement);
'mon-premier-element'est le nom de la balise HTML que vous utiliserez.MonPremierElementest la classe JavaScript que vous avez définie.
4. Les Types de Custom Elements
Il existe deux types de Custom Elements, bien que les Autonomous Custom Elements soient de loin les plus couramment utilisés et la pierre angulaire de la plupart des composants.
4.1 Autonomous Custom Elements (Éléments personnalisés autonomes)
Ce sont des éléments HTML entièrement nouveaux, qui héritent directement de HTMLElement. Ils ont leur propre balise HTML (par exemple, <mon-composant>) et sont complètement indépendants. C'est le type que nous allons principalement étudier et utiliser dans ce cours.
4.2 Customized Built-in Elements (Éléments natifs personnalisés)
Ce sont des Custom Elements qui étendent un élément HTML natif existant (par exemple, un HTMLButtonElement pour un bouton ou HTMLParagraphElement pour un paragraphe). Ils sont utilisés en ajoutant l'attribut is à une balise native.
Exemple : <button is="mon-bouton-special"></button>.
Ce type est moins courant et a une portée légèrement différente. Nous nous concentrerons sur les Autonomous Custom Elements.
5. Le Cycle de Vie des Custom Elements
Les Custom Elements disposent de méthodes de "callbacks" de cycle de vie qui sont appelées par le navigateur à des moments précis de l'existence de l'élément. C'est là que vous placez votre logique d'initialisation, de mise à jour ou de nettoyage.
-
constructor(): Le constructeur de la classe. Appelé lorsque l'instance de l'élément est créée. C'est le bon endroit pour initialiser l'état interne et les gestionnaires d'événements, mais pas pour interagir avec le DOM de l'élément ou ses enfants, car l'élément n'est pas encore attaché au document. Toujours appelersuper()en premier. -
connectedCallback(): Appelé lorsque l'élément est inséré dans le document (le DOM). C'est le moment idéal pour :- Mettre en place la structure initiale du DOM de votre composant.
- Attacher des gestionnaires d'événements.
- Récupérer des données (par exemple, via
fetch). - Mettre en place des observateurs.
-
disconnectedCallback(): Appelé lorsque l'élément est retiré du document (le DOM). C'est l'endroit pour effectuer le nettoyage :- Supprimer les gestionnaires d'événements attachés.
- Annuler les timers (
setTimeout,setInterval). - Arrêter les observateurs (
IntersectionObserver,MutationObserver). - Libérer les ressources pour éviter les fuites de mémoire.
-
attributeChangedCallback(name, oldValue, newValue): Appelé lorsque l'un des attributs de l'élément spécifié dansstatic get observedAttributes()est ajouté, supprimé ou modifié.name: Le nom de l'attribut modifié.oldValue: L'ancienne valeur de l'attribut.newValue: La nouvelle valeur de l'attribut. (Nous verrons plus de détails sur celui-ci dans la section sur les attributs).
-
adoptedCallback(oldDoc, newDoc): Appelé lorsque l'élément est déplacé vers un nouveau document (par exemple, en utilisantdocument.adoptNode()). Rarement utilisé dans la plupart des applications.
6. Créer votre premier Custom Element : Un compteur simple
Nous allons créer un Custom Element simple qui affiche un nombre et permet de l'incrémenter ou de le décrémenter avec des boutons.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mon Premier Custom Element : Compteur</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: #f4f4f4;
}
my-counter {
display: block; /* Important pour les règles CSS */
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
background-color: white;
text-align: center;
}
my-counter h2 {
color: #333;
margin-bottom: 15px;
}
my-counter p {
font-size: 2em;
margin: 20px 0;
color: #007bff;
}
my-counter button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
margin: 0 5px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
transition: background-color 0.3s ease;
}
my-counter button:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<my-counter></my-counter>
<script>
// 1. Définition de la classe du Custom Element
class MyCounter extends HTMLElement {
constructor() {
super(); // Appelle le constructeur de HTMLElement
console.log('Compteur construit !');
this._count = 0; // Initialisation de la propriété interne
}
// 2. Méthode du cycle de vie : connectedCallback
// Appelé lorsque l'élément est ajouté au DOM
connectedCallback() {
console.log('Compteur ajouté au DOM !');
this.render(); // Appelle la méthode pour créer le contenu de l'élément
}
// 3. Méthode pour rendre le contenu HTML de l'élément
render() {
this.innerHTML = `
<h2>Mon Compteur</h2>
<p>${this._count}</p>
<button id="decrement">-</button>
<button id="increment">+</button>
`;
// Ajout des écouteurs d'événements aux boutons
this.querySelector('#decrement').addEventListener('click', this.decrement.bind(this));
this.querySelector('#increment').addEventListener('click', this.increment.bind(this));
}
// 4. Méthodes pour incrémenter et décrémenter le compteur
increment() {
this._count++;
this.updateCountDisplay();
}
decrement() {
this._count--;
this.updateCountDisplay();
}
// 5. Méthode pour mettre à jour l'affichage du nombre
updateCountDisplay() {
this.querySelector('p').textContent = this._count;
}
// 6. Méthode du cycle de vie : disconnectedCallback
// Appelé lorsque l'élément est retiré du DOM
disconnectedCallback() {
console.log('Compteur retiré du DOM !');
// Nettoyage des écouteurs d'événements si nécessaire
// (dans cet exemple, les écouteurs sont attachés aux éléments enfants créés dans render(),
// donc ils sont automatiquement supprimés avec l'élément parent.
// Ce serait plus critique si les écouteurs étaient sur le document ou la fenêtre.)
}
}
// 7. Enregistrement du Custom Element
// Associe le nom de balise 'my-counter' à la classe MyCounter
customElements.define('my-counter', MyCounter);
</script>
</body>
</html>
Explication du code :
class MyCounter extends HTMLElement: Nous définissons une nouvelle classe JavaScriptMyCounterqui hérite deHTMLElement. C'est la base de tout Custom Element.constructor(): C'est la première méthode appelée lors de la création d'une instance de votre élément.super(): Indispensable ! Il appelle le constructeur de la classe parente (HTMLElement), assurant que l'initialisation interne nécessaire à un élément HTML est effectuée.this._count = 0;: Nous initialisons une propriété interne_countpour stocker la valeur du compteur. L'utilisation du_est une convention pour les propriétés "privées" (non accessibles directement de l'extérieur).
connectedCallback(): Cette méthode est appelée par le navigateur lorsque votre élément<my-counter>est ajouté au DOM de la page. C'est le moment idéal pour :- Générer la structure HTML interne de votre composant.
- Attacher des écouteurs d'événements.
this.render(): J'ai encapsulé la logique de création de l'interface dans une méthoderender()pour une meilleure organisation.
render(): Cette méthode crée l'interface utilisateur de notre compteur.this.innerHTML = \...`;`: Définit le contenu HTML interne de notre Custom Element.this.querySelector(...): Recherche des éléments à l'intérieur de notre Custom Element pour attacher des écouteurs d'événements.addEventListener('click', this.decrement.bind(this)): Attache les gestionnaires d'événements aux boutons. L'utilisation de.bind(this)est cruciale pour s'assurer quethisà l'intérieur des fonctionsincrementetdecrementfait référence à l'instance deMyCounter, et non au bouton lui-même.
increment()/decrement(): Ces méthodes mettent à jour la propriété_countet appellentupdateCountDisplay()pour rafraîchir l'interface.updateCountDisplay(): Met à jour le texte du paragraphe qui affiche le compte.disconnectedCallback(): Appelé lorsque l'élément est retiré du DOM. C'est l'endroit pour le nettoyage. Dans cet exemple simple, les écouteurs d'événements sont nettoyés automatiquement avec l'élément, mais pour des cas plus complexes (observateurs, écouteurs surwindow), ce callback serait essentiel.customElements.define('my-counter', MyCounter);: C'est la ligne qui dit au navigateur : "Désormais, lorsque tu vois la balise<my-counter>, utilise la logique définie dans la classeMyCounter".
7. Passer des données aux Custom Elements : Attributs et Propriétés
Les Custom Elements seraient limités s'ils ne pouvaient pas interagir avec le monde extérieur. Il existe deux façons principales de leur passer des données : les attributs HTML et les propriétés JavaScript.
7.1 Les Attributs HTML (attributeChangedCallback)
Les attributs HTML sont parfaits pour passer des valeurs initiales ou des configurations simples à votre Custom Element directement depuis le balisage. Pour qu'un Custom Element réagisse aux changements d'attributs, vous devez :
- Définir une propriété statique
observedAttributesdans votre classe, qui est un tableau des noms des attributs que vous souhaitez observer. - Implémenter la méthode
attributeChangedCallback.
Exemple : Un Custom Element avec un attribut data-message
class MyMessageElement extends HTMLElement {
static get observedAttributes() {
// Liste des attributs que nous voulons observer
return ['data-message', 'type'];
}
constructor() {
super();
console.log('MyMessageElement construit !');
}
connectedCallback() {
console.log('MyMessageElement connecté au DOM !');
this.render();
}
render() {
const message = this.getAttribute('data-message') || 'Pas de message';
const type = this.getAttribute('type') || 'info';
this.innerHTML = `<div class="message ${type}">${message}</div>`;
// Pour une vraie mise en forme, on utiliserait le Shadow DOM et des styles encapsulés ici.
// Pour l'exemple, nous nous contentons de la structure HTML simple.
}
attributeChangedCallback(name, oldValue, newValue) {
// Appelé lorsque l'un des attributs listés dans observedAttributes est modifié
console.log(`L'attribut '${name}' a changé de '${oldValue}' à '${newValue}'.`);
// Si la valeur a réellement changé et que ce n'est pas la première fois qu'il est défini
if (oldValue !== newValue) {
this.render(); // Re-rendu de l'élément pour refléter le changement
}
}
// Propriétés JavaScript (voir section suivante)
get message() {
return this.getAttribute('data-message');
}
set message(val) {
if (val) {
this.setAttribute('data-message', val);
} else {
this.removeAttribute('data-message');
}
}
}
customElements.define('my-message', MyMessageElement);
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Element avec Attributs</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
my-message {
display: block;
margin-bottom: 10px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
}
.message.info { background-color: #e0f7fa; border-color: #00bcd4; color: #00796b; }
.message.warning { background-color: #fffde7; border-color: #ffc107; color: #ff8f00; }
.message.error { background-color: #ffebee; border-color: #f44336; color: #d32f2f; }
button { margin-top: 15px; padding: 8px 15px; }
</style>
</head>
<body>
<my-message data-message="Ceci est un message d'information initial." type="info"></my-message>
<my-message data-message="Attention : quelque chose pourrait se passer !" type="warning"></my-message>
<button id="changeMessageBtn">Changer le message du premier élément</button>
<script src="script.js"></script> <!-- Suppose que le JS est dans script.js -->
<script>
const myMessageElement = document.querySelector('my-message[type="info"]');
document.getElementById('changeMessageBtn').addEventListener('click', () => {
myMessageElement.setAttribute('data-message', 'Le message a été mis à jour via JavaScript !');
myMessageElement.setAttribute('type', 'error');
});
</script>
</body>
</html>
Dans cet exemple, lorsque vous cliquez sur le bouton, l'attribut data-message (et type) du premier <my-message> est modifié. Cela déclenche attributeChangedCallback, qui appelle render() à nouveau, mettant à jour le contenu affiché.
7.2 Les Propriétés JavaScript
Pour des données plus complexes (objets, tableaux) ou pour des interactions programmatiques, il est préférable d'utiliser des propriétés JavaScript sur l'instance de votre Custom Element. Vous pouvez définir des accesseurs (get et set) pour les propriétés afin de synchroniser les attributs et les propriétés, ce qui est une bonne pratique.
class MyProductCard extends HTMLElement {
constructor() {
super();
this._product = {}; // Propriété privée pour stocker les données du produit
}
connectedCallback() {
this.render();
}
render() {
const { name, price, description } = this._product;
this.innerHTML = `
<h3>${name || 'Produit inconnu'}</h3>
<p>Prix : ${price ? `${price} €` : 'N/A'}</p>
<p>${description || 'Pas de description.'}</p>
`;
}
// Définition d'un accesseur (setter) pour la propriété 'product'
set product(data) {
// Effectuer une validation ou une transformation si nécessaire
this._product = data;
this.render(); // Re-render l'élément lorsque la propriété est mise à jour
}
// Définition d'un accesseur (getter) pour la propriété 'product'
get product() {
return this._product;
}
}
customElements.define('product-card', MyProductCard);
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Element avec Propriétés</title>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
product-card {
display: block;
border: 1px solid #eee;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
max-width: 300px;
background-color: white;
}
product-card h3 { color: #333; margin-top: 0; }
product-card p { color: #666; font-size: 0.9em; line-height: 1.4; }
product-card p:first-of-type { font-weight: bold; color: #007bff; }
</style>
</head>
<body>
<product-card id="myProductCard"></product-card>
<script>
const productCard = document.getElementById('myProductCard');
// Passer un objet JavaScript comme propriété
productCard.product = {
name: 'Ordinateur Portable Alpha',
price: 1200,
description: 'Un ordinateur portable puissant pour les professionnels et les gamers.'
};
// Vous pouvez modifier la propriété plus tard
setTimeout(() => {
productCard.product = {
name: 'Clavier Mécanique RGB',
price: 85,
description: 'Clavier avec des switches mécaniques et rétroéclairage RGB personnalisable.'
};
}, 3000);
</script>
</body>
</html>
Dans ce cas, nous ne nous appuyons pas sur attributeChangedCallback car nous passons un objet complexe directement à la propriété product via JavaScript. Le set product(data) est appelé, met à jour la propriété interne _product, puis appelle render() pour actualiser l'affichage.
8. Styliser un Custom Element
Par défaut, les Custom Elements héritent des styles du document principal. Vous pouvez styliser un Custom Element comme n'importe quelle autre balise HTML :
my-counter {
border: 2px solid blue;
padding: 10px;
}
Cependant, pour une véritable encapsulation des styles (où les styles définis à l'intérieur de votre composant n'affectent pas le reste de la page, et les styles externes n'affectent pas l'intérieur du composant), vous devrez utiliser le Shadow DOM. C'est le sujet de la prochaine leçon !
9. Bonnes pratiques et considérations
- Encapsulation avec Shadow DOM : Pour des composants réellement autonomes et réutilisables, combinez toujours les Custom Elements avec le Shadow DOM. Cela isole le markup et les styles de votre composant du reste de la page.
- Performance des attributs : Utilisez
observedAttributesavec parcimonie. Observer trop d'attributs ou effectuer des opérations coûteuses dansattributeChangedCallbackpeut impacter les performances. - Synchronisation Attributs/Propriétés : Pour les données simples, il est souvent utile de synchroniser les attributs et les propriétés (comme montré avec
messagedansMyMessageElement), permettant à l'élément d'être configuré via HTML ou JavaScript. - Gestion des événements : Attachez les écouteurs d'événements dans
connectedCallbacket nettoyez-les (si nécessaire) dansdisconnectedCallbackpour éviter les fuites de mémoire. - Accessibilité : Pensez à l'accessibilité dès la conception de vos Custom Elements (rôles ARIA, tabindex, etc.).
10. Conclusion
Félicitations ! Vous avez maintenant une solide compréhension des Custom Elements, la première et essentielle brique des Web Components. Vous avez appris à :
- Définir et enregistrer vos propres balises HTML.
- Comprendre les méthodes du cycle de vie (
constructor,connectedCallback,disconnectedCallback,attributeChangedCallback). - Passer des données à vos composants via les attributs HTML et les propriétés JavaScript.
Les Custom Elements sont un outil puissant qui vous permet de créer des composants modulaires, réutilisables et interopérables, indépendamment de tout framework JavaScript. Ils sont la fondation sur laquelle nous construirons des composants plus complexes et mieux encapsulés dans les prochaines leçons, en explorant le Shadow DOM pour une encapsulation complète du style et de la structure.