Skip to main content

Code that compiles on the first try! CodeWithMpia wishes you very happy holidays.✨

Aller au contenu principal

Les Web Components : créer des composants réutilisables en JavaScript pur

Découvrez comment créer des composants web réutilisables et encapsulés sans framework grâce aux Web Components natifs (Custom Elements, Shadow DOM, templates, slots, intégration avec Vue, Django, etc.).

M
Mpia
12/17/2025 21 min
79 vues
0 comme.
#JS#Web Components#Frontend
Les Web Components : créer des composants réutilisables en JavaScript pur

Salut ! Aujourd'hui, on va plonger dans un sujet fascinant qui change la donne en développement frontend : les Web Components. Si tu utilises Vue, React ou Svelte, tu connais déjà le concept de composants. Mais savais-tu que tu peux créer des composants réutilisables directement avec JavaScript, sans aucun framework ?

Pourquoi s'intéresser aux Web Components ?

Imagine pouvoir créer un composant une seule fois et l'utiliser partout : dans une application Vue, un site Django, Flask avec du HTML classique, ou même dans un projet React. C'est exactement ce que permettent les Web Components.

Les avantages sont nombreux :

  • Indépendance des frameworks : tes composants fonctionnent partout

  • Encapsulation native : le CSS et le JavaScript sont isolés

  • Standard web : supporté nativement par tous les navigateurs modernes

  • Réutilisabilité : écris une fois, utilise partout

Les trois piliers des Web Components

Les Web Components reposent sur trois technologies principales :

1. Custom Elements

Les Custom Elements te permettent de créer tes propres balises HTML. Au lieu d'utiliser <div> ou <span>, tu peux créer <mon-composant>.

class MonBouton extends HTMLElement {
  constructor() {
    super();
    // Initialisation du composant
  }

  connectedCallback() {
    // Appelé quand l'élément est ajouté au DOM
    this.innerHTML = `<button>Clique-moi !</button>`;
  }
}

// Enregistrement du composant
customElements.define('mon-bouton', MonBouton);

Tu peux maintenant utiliser <mon-bouton></mon-bouton> dans ton HTML !

2. Shadow DOM

Le Shadow DOM crée une encapsulation complète. Ton CSS ne fuite pas vers l'extérieur, et le CSS externe n'affecte pas ton composant.

class CarteUtilisateur extends HTMLElement {
  constructor() {
    super();
    // Création du Shadow DOM
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .carte {
          border: 2px solid #333;
          padding: 1rem;
          border-radius: 8px;
          background: white;
        }
        h2 {
          color: #2563eb;
          margin: 0 0 0.5rem 0;
        }
      </style>
      <div class="carte">
        <h2>${this.getAttribute('nom')}</h2>
        <p>${this.getAttribute('email')}</p>
      </div>
    `;
  }
}

customElements.define('carte-utilisateur', CarteUtilisateur);

Utilisation :

<carte-utilisateur 
  nom="Mpia M." 
  email="contact@codewithmpia.com">
</carte-utilisateur>

3. HTML Templates

Les templates te permettent de définir du HTML réutilisable qui n'est pas rendu immédiatement.

<template id="template-carte">
  <style>
    .carte {
      border: 1px solid #ddd;
      padding: 1rem;
      margin: 1rem 0;
    }
  </style>
  <div class="carte">
    <slot name="titre"></slot>
    <slot name="contenu"></slot>
  </div>
</template>

Créons un composant complet : Un compteur interactif

Passons à la pratique avec un exemple concret. On va créer un composant de compteur avec des boutons pour incrémenter et décrémenter.

class CompteurApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.count = 0;
  }

  connectedCallback() {
    this.render();
    this.setupEventListeners();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: system-ui, -apple-system, sans-serif;
        }

        .compteur {
          display: flex;
          flex-direction: column;
          align-items: center;
          gap: 1rem;
          padding: 2rem;
          border: 2px solid #e5e7eb;
          border-radius: 12px;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: white;
          max-width: 300px;
        }

        .valeur {
          font-size: 4rem;
          font-weight: bold;
          margin: 1rem 0;
        }

        .boutons {
          display: flex;
          gap: 1rem;
        }

        button {
          padding: 0.75rem 1.5rem;
          font-size: 1.125rem;
          font-weight: 600;
          border: none;
          border-radius: 8px;
          background: white;
          color: #667eea;
          cursor: pointer;
          transition: all 0.2s;
        }

        button:hover {
          transform: translateY(-2px);
          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        }

        button:active {
          transform: translateY(0);
        }

        .reset {
          margin-top: 1rem;
          background: transparent;
          color: white;
          border: 2px solid white;
        }

        .reset:hover {
          background: white;
          color: #667eea;
        }
      </style>

      <div class="compteur">
        <h2>Compteur</h2>
        <div class="valeur">${this.count}</div>
        <div class="boutons">
          <button id="decrement">−</button>
          <button id="increment">+</button>
        </div>
        <button id="reset" class="reset">Réinitialiser</button>
      </div>
    `;
  }

  setupEventListeners() {
    const increment = this.shadowRoot.getElementById('increment');
    const decrement = this.shadowRoot.getElementById('decrement');
    const reset = this.shadowRoot.getElementById('reset');

    increment.addEventListener('click', () => {
      this.count++;
      this.updateDisplay();
      this.dispatchEvent(new CustomEvent('change', { 
        detail: { value: this.count } 
      }));
    });

    decrement.addEventListener('click', () => {
      this.count--;
      this.updateDisplay();
      this.dispatchEvent(new CustomEvent('change', { 
        detail: { value: this.count } 
      }));
    });

    reset.addEventListener('click', () => {
      this.count = 0;
      this.updateDisplay();
      this.dispatchEvent(new CustomEvent('reset'));
    });
  }

  updateDisplay() {
    const valeurElement = this.shadowRoot.querySelector('.valeur');
    valeurElement.textContent = this.count;
  }

  // Méthode pour définir la valeur depuis l'extérieur
  setValue(newValue) {
    this.count = newValue;
    this.updateDisplay();
  }

  // Méthode pour obtenir la valeur
  getValue() {
    return this.count;
  }
}

customElements.define('compteur-app', CompteurApp);

Utilisation du composant

C'est aussi simple que :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Component - Compteur</title>
</head>
<body>
  <h1>Mon Application</h1>

  <!-- Utilisation du composant -->
  <compteur-app></compteur-app>

  <script src="compteur.js"></script>

  <script>
    // Écouter les événements du composant
    const compteur = document.querySelector('compteur-app');

    compteur.addEventListener('change', (e) => {
      console.log('Nouvelle valeur:', e.detail.value);
    });

    compteur.addEventListener('reset', () => {
      console.log('Compteur réinitialisé');
    });
  </script>
</body>
</html>

Gérer les attributs et propriétés

Pour rendre ton composant encore plus flexible, tu peux observer les changements d'attributs :

class CompteurAvance extends HTMLElement {
  static get observedAttributes() {
    return ['min', 'max', 'step', 'value'];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.min = parseInt(this.getAttribute('min')) || 0;
    this.max = parseInt(this.getAttribute('max')) || Infinity;
    this.step = parseInt(this.getAttribute('step')) || 1;
    this.count = parseInt(this.getAttribute('value')) || 0;
    this.render();
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      switch(name) {
        case 'min':
          this.min = parseInt(newValue);
          break;
        case 'max':
          this.max = parseInt(newValue);
          break;
        case 'step':
          this.step = parseInt(newValue);
          break;
        case 'value':
          this.count = parseInt(newValue);
          this.updateDisplay();
          break;
      }
    }
  }

  increment() {
    if (this.count + this.step <= this.max) {
      this.count += this.step;
      this.updateDisplay();
    }
  }

  decrement() {
    if (this.count - this.step >= this.min) {
      this.count -= this.step;
      this.updateDisplay();
    }
  }

  // ... reste du code
}

customElements.define('compteur-avance', CompteurAvance);

Utilisation :

<compteur-avance min="0" max="100" step="5" value="50"></compteur-avance>

Utiliser les Slots pour la composition

Les slots permettent d'insérer du contenu depuis l'extérieur :

class CarteModale extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .modale {
          background: white;
          border-radius: 8px;
          padding: 2rem;
          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .entete {
          margin-bottom: 1rem;
          padding-bottom: 1rem;
          border-bottom: 1px solid #e5e7eb;
        }

        .pied {
          margin-top: 1rem;
          padding-top: 1rem;
          border-top: 1px solid #e5e7eb;
          text-align: right;
        }
      </style>

      <div class="modale">
        <div class="entete">
          <slot name="titre">Titre par défaut</slot>
        </div>
        <div class="contenu">
          <slot>Contenu par défaut</slot>
        </div>
        <div class="pied">
          <slot name="actions"></slot>
        </div>
      </div>
    `;
  }
}

customElements.define('carte-modale', CarteModale);

Utilisation avec slots :

<carte-modale>
  <h2 slot="titre">Confirmation</h2>
  <p>Êtes-vous sûr de vouloir continuer ?</p>
  <div slot="actions">
    <button>Annuler</button>
    <button>Confirmer</button>
  </div>
</carte-modale>

Intégration avec des frameworks

Avec Vue.js

<template>
  <div>
    <h1>Mon App Vue</h1>
    <compteur-app @change="handleChange"></compteur-app>
  </div>
</template>

<script>
export default {
  mounted() {
    // Importer le composant web
    import('./composants/compteur.js');
  },
  methods: {
    handleChange(event) {
      console.log('Valeur:', event.detail.value);
    }
  }
}
</script>

Avec Django ou Flask

{% extends "base.html" %}

{% block content %}
<div class="container">
  <h1>Tableau de bord</h1>
  <compteur-app></compteur-app>
</div>

<script src="{% static 'js/compteur.js' %}"></script>
{% endblock %}

Bonnes pratiques

Voici quelques conseils pour créer des Web Components de qualité :

1. Nommage

Utilise toujours un tiret dans le nom de ton composant pour éviter les conflits avec les éléments HTML natifs :

  • mon-composant

  • app-bouton

  • composant (pas de tiret)

2. Gestion de la mémoire

N'oublie pas de nettoyer les écouteurs d'événements :

class MonComposant extends HTMLElement {
  connectedCallback() {
    this.handleClick = this.handleClick.bind(this);
    this.addEventListener('click', this.handleClick);
  }

  disconnectedCallback() {
    // Nettoyage quand le composant est retiré du DOM
    this.removeEventListener('click', this.handleClick);
  }

  handleClick() {
    console.log('Cliqué !');
  }
}

3. Accessibilité

Pense toujours à l'accessibilité :

class BoutonAccessible extends HTMLElement {
  connectedCallback() {
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = `
      <button 
        role="button" 
        aria-label="${this.getAttribute('label')}"
        tabindex="0">
        <slot></slot>
      </button>
    `;
  }
}

4. Attributs vs Propriétés

Utilise les attributs pour les données simples et les propriétés pour les objets complexes :

// Attribut pour une chaîne simple
<mon-composant titre="Bonjour"></mon-composant>

// Propriété pour un objet
const composant = document.querySelector('mon-composant');
composant.data = { nom: 'Mpia', age: 25 };

Performance et optimisations

Lazy Loading

Charge tes composants seulement quand nécessaire :

// Définition simple
customElements.define('composant-lourd', class extends HTMLElement {
  connectedCallback() {
    // Charge le contenu de façon asynchrone
    this.loadContent();
  }

  async loadContent() {
    const module = await import('./composant-lourd-content.js');
    this.attachShadow({ mode: 'open' });
    this.shadowRoot.innerHTML = module.template;
  }
});

Mise en cache des templates

Pour éviter de recréer le template à chaque instance :

const template = document.createElement('template');
template.innerHTML = `
  <style>
    /* styles ici */
  </style>
  <div class="composant">
    <!-- contenu ici -->
  </div>
`;

class ComposantOptimise extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // Clone le template au lieu de recréer
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}

Cas d'usage réels

Les Web Components brillent dans plusieurs scénarios :

1. Design Systems

Crée une bibliothèque de composants réutilisable dans toute ton organisation :

  • Boutons

  • Cartes

  • Modales

  • Formulaires

2. Widgets intégrables

Parfait pour créer des widgets que d'autres sites peuvent intégrer facilement :

<!-- Widget de chat -->
<chat-widget api-key="ABC123" theme="dark"></chat-widget>

3. Micro-frontends

Utilise des Web Components pour créer des applications modulaires où différentes équipes peuvent travailler sur différents composants.

Compatibilité et Polyfills

Les Web Components sont supportés par tous les navigateurs modernes. Pour les anciens navigateurs, utilise les polyfills :

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2/webcomponents-loader.js"></script>

Aller plus loin

Pour approfondir tes connaissances :

  1. Lit : Une bibliothèque légère pour simplifier la création de Web Components

  2. Stencil : Un compilateur pour créer des composants web performants

  3. Open Web Components : Des outils et bonnes pratiques pour le développement

Conclusion

Les Web Components représentent l'avenir du développement web modulaire. Ils offrent une solution native, performante et indépendante des frameworks pour créer des composants réutilisables. Que tu travailles avec Vue, React, Django ou du HTML pur, les Web Components peuvent s'intégrer partout.

Le plus beau dans tout ça ? C'est du JavaScript standard. Pas de build, pas de dépendances, juste du code qui fonctionne dans tous les navigateurs modernes.

Alors, prêt à créer tes premiers Web Components ? Lance-toi et partage tes créations !


Pour aller plus loin, je te recommande de consulter :

Tu as des questions sur les Web Components ? N'hésite pas à laisser un commentaire ou à me contacter !

Commentaires (0)

Laisser un commentaire

Aucun commentaire pour le moment. Soyez le premier à commenter !