Custom Elements v1 – Composants Web réutilisables

Les éléments personnalisés permettent aux développeurs Web de définir de nouvelles balises HTML, d'étendre des balises existantes et de créer des composants Web réutilisables.

Grâce aux éléments personnalisés, les développeurs Web peuvent créer des balises HTML, renforcer les balises HTML existantes ou étendre les composants créés par d'autres développeurs. L'API constitue la base des composants Web. Il offre une méthode basée sur des normes Web pour créer des composants réutilisables en utilisant rien de plus que du code JavaScript/HTML/CSS standard. Résultat : moins de code et de code modulaire, et plus de réutilisations dans nos applications.

Introduction

Le navigateur est un excellent outil pour structurer les applications Web. C'est ce qu'on appelle le langage HTML. Vous en avez peut-être entendu parler ! Il est déclaratif, portable, bien pris en charge et facile à utiliser. Quel que soit le langage HTML, son vocabulaire et son extensibilité sont limités. Jusqu'à présent, le HTML Living Standard manquait de moyen pour associer automatiquement le comportement JS à votre balisage.

Les éléments personnalisés sont la solution idéale pour moderniser le code HTML, remplir les pièces manquantes et regrouper la structure par le comportement. Si le langage HTML ne fournit pas de solution à un problème, nous pouvons créer un élément personnalisé qui répond à vos besoins. Les éléments personnalisés enseignent de nouvelles astuces au navigateur tout en préservant les avantages du langage HTML.

Définir un nouvel élément

Pour définir un nouvel élément HTML, nous avons besoin de la puissance de JavaScript.

L'élément global customElements permet de définir un élément personnalisé et d'informer le navigateur d'une nouvelle balise. Appelez customElements.define() avec le nom de la balise que vous souhaitez créer et un class JavaScript qui étend l'HTMLElement de base.

Exemple – Définition d'un panneau de panneau pour mobile, <app-drawer>:

class AppDrawer extends HTMLElement {...}
window.customElements.define('app-drawer', AppDrawer);

// Or use an anonymous class if you don't want a named constructor in current scope.
window.customElements.define('app-drawer', class extends HTMLElement {...});

Exemples d'utilisation :

<app-drawer></app-drawer>

N'oubliez pas que l'utilisation d'un élément personnalisé n'est pas différente de l'utilisation d'un <div> ou de tout autre élément. Les instances peuvent être déclarées sur la page, créées dynamiquement en JavaScript, associés à des écouteurs d'événements, etc. Poursuivez votre lecture pour découvrir d'autres exemples.

Définir l'API JavaScript d'un élément

La fonctionnalité d'un élément personnalisé est définie à l'aide d'un class ES2015 qui étend HTMLElement. L'extension de HTMLElement garantit que l'élément personnalisé hérite de l'ensemble de l'API DOM et signifie que toutes les propriétés/méthodes que vous ajoutez à la classe font partie de l'interface DOM de l'élément. Pour résumer, utilisez la classe pour créer une API JavaScript publique pour votre tag.

Exemple – Définition de l'interface DOM de <app-drawer>:

class AppDrawer extends HTMLElement {

  // A getter/setter for an open property.
  get open() {
    return this.hasAttribute('open');
  }

  set open(val) {
    // Reflect the value of the open property as an HTML attribute.
    if (val) {
      this.setAttribute('open', '');
    } else {
      this.removeAttribute('open');
    }
    this.toggleDrawer();
  }

  // A getter/setter for a disabled property.
  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    // Reflect the value of the disabled property as an HTML attribute.
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Can define constructor arguments if you wish.
  constructor() {
    // If you define a constructor, always call super() first!
    // This is specific to CE and required by the spec.
    super();

    // Setup a click listener on <app-drawer> itself.
    this.addEventListener('click', e => {
      // Don't toggle the drawer if it's disabled.
      if (this.disabled) {
        return;
      }
      this.toggleDrawer();
    });
  }

  toggleDrawer() {
    // ...
  }
}

customElements.define('app-drawer', AppDrawer);

Dans cet exemple, nous créons un panneau avec une propriété open, une propriété disabled et une méthode toggleDrawer(). De plus, les propriétés reflètent les propriétés en tant qu'attributs HTML.

L'avantage des éléments personnalisés est que this dans une définition de classe fait référence à l'élément DOM lui-même, c'est-à-dire à l'instance de la classe. Dans notre exemple, this fait référence à <app-drawer>. C'est (C'est) ainsi que l'élément peut associer un écouteur click à lui-même. Et vous n'êtes pas limité aux écouteurs d'événements. L'ensemble de l'API DOM est disponible dans le code de l'élément. Utilisez this pour accéder aux propriétés de l'élément, inspecter ses enfants (this.children), nœuds de requête (this.querySelectorAll('.items')), etc.

Règles de création d'éléments personnalisés

  1. Le nom d'un élément personnalisé doit contenir un tiret (-). Ainsi, <x-tags>, <my-element> et <my-awesome-app> sont des noms valides, contrairement à <tabs> et <foo_bar>. Cette exigence permet à l'analyseur HTML de distinguer les éléments personnalisés des éléments standards. Il garantit également la compatibilité ascendante lorsque de nouvelles balises sont ajoutées au code HTML.
  2. Vous ne pouvez pas enregistrer la même balise plusieurs fois. Si vous tentez de le faire, une erreur DOMException sera générée. Une fois que vous avez signalé une nouvelle balise au navigateur, c'est tout. Pas de retrait.
  3. Les éléments personnalisés ne peuvent pas se fermer automatiquement, car HTML ne permet la fermeture automatique que de quelques éléments. Écrivez toujours une balise de fermeture (<app-drawer></app-drawer>).

Réactions aux éléments personnalisés

Un élément personnalisé peut définir des hooks de cycle de vie spéciaux pour exécuter du code à des moments intéressants de son existence. C'est ce qu'on appelle des réactions d'éléments personnalisés.

Nom Appelé quand
constructor Une instance de l'élément est créée ou mise à niveau. Utile pour initialiser l'état, configurer des écouteurs d'événements ou créer un bâtiment ombré. Consultez les spécifications pour connaître les restrictions sur ce que vous pouvez faire dans constructor.
connectedCallback Appelée chaque fois que l'élément est inséré dans le DOM. Utile pour exécuter le code de configuration, par exemple pour extraire des ressources ou pour effectuer l'affichage. En règle générale, vous devez essayer de retarder l'exécution jusqu'à cette date.
disconnectedCallback Appelée chaque fois que l'élément est supprimé du DOM. Utile pour exécuter du code de nettoyage.
attributeChangedCallback(attrName, oldVal, newVal) Appelée lorsqu'un attribut observé a été ajouté, supprimé, mis à jour ou remplacé. Également appelé pour les valeurs initiales lorsqu'un élément est créé par l'analyseur ou mis à niveau. Remarque:Seuls les attributs listés dans la propriété observedAttributes recevront ce rappel.
adoptedCallback L'élément personnalisé a été déplacé dans un nouveau document (par exemple, un utilisateur appelé document.adoptNode(el)).

Les rappels de réaction sont synchrones. Si quelqu'un appelle el.setAttribute() sur votre élément, le navigateur appelle immédiatement attributeChangedCallback(). De même, vous recevez un disconnectedCallback() juste après la suppression de votre élément du DOM (par exemple, lorsque l'utilisateur appelle el.remove()).

Exemple:Ajout de réactions d'éléments personnalisés à <app-drawer>:

class AppDrawer extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.
    // ...
  }

  connectedCallback() {
    // ...
  }

  disconnectedCallback() {
    // ...
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    // ...
  }
}

Définissez les réactions si/quand c'est judicieux. Si votre élément est suffisamment complexe et ouvre une connexion à IndexedDB dans connectedCallback(), effectuez le travail de nettoyage nécessaire dans disconnectedCallback(). Mais faites attention ! Vous ne pouvez pas compter sur la suppression de votre élément du DOM en toutes circonstances. Par exemple, disconnectedCallback() ne sera jamais appelé si l'utilisateur ferme l'onglet.

Propriétés et attributs

Réfléchir des propriétés aux attributs

Il est courant que les propriétés HTML reflètent leur valeur dans le DOM en tant qu'attribut HTML. Par exemple, lorsque les valeurs de hidden ou id sont modifiées dans JS:

div.id = 'my-id';
div.hidden = true;

les valeurs sont appliquées au DOM actif en tant qu'attributs:

<div id="my-id" hidden>

C'est ce qu'on appelle la réflexion des propriétés en attributs. Presque toutes les propriétés en HTML le font. Pourquoi ? Les attributs sont également utiles pour configurer un élément de manière déclarative, et certaines API telles que l'accessibilité et les sélecteurs CSS s'appuient sur les attributs pour fonctionner.

La mise en miroir d'une propriété est utile lorsque vous souhaitez synchroniser la représentation DOM de l'élément avec son état JavaScript. Vous pouvez avoir besoin de refléter une propriété pour que le style défini par l'utilisateur s'applique lorsque l'état JavaScript change.

Rappelez-vous notre <app-drawer>. Un utilisateur de ce composant peut vouloir l'appliquer en fondu et/ou empêcher toute interaction de l'utilisateur lorsqu'il est désactivé:

app-drawer[disabled] {
  opacity: 0.5;
  pointer-events: none;
}

Lorsque la propriété disabled est modifiée dans JavaScript, cet attribut doit être ajouté au DOM afin que le sélecteur de l'utilisateur corresponde. L'élément peut offrir ce comportement en reflétant la valeur vers un attribut du même nom:

get disabled() {
  return this.hasAttribute('disabled');
}

set disabled(val) {
  // Reflect the value of `disabled` as an attribute.
  if (val) {
    this.setAttribute('disabled', '');
  } else {
    this.removeAttribute('disabled');
  }
  this.toggleDrawer();
}

Observer les modifications apportées aux attributs

Les attributs HTML permettent aux utilisateurs de déclarer facilement l'état initial:

<app-drawer open disabled></app-drawer>

Les éléments peuvent réagir aux modifications d'attributs en définissant un attributeChangedCallback. Le navigateur appellera cette méthode pour chaque modification apportée aux attributs répertoriés dans le tableau observedAttributes.

class AppDrawer extends HTMLElement {
  // ...

  static get observedAttributes() {
    return ['disabled', 'open'];
  }

  get disabled() {
    return this.hasAttribute('disabled');
  }

  set disabled(val) {
    if (val) {
      this.setAttribute('disabled', '');
    } else {
      this.removeAttribute('disabled');
    }
  }

  // Only called for the disabled and open attributes due to observedAttributes
  attributeChangedCallback(name, oldValue, newValue) {
    // When the drawer is disabled, update keyboard/screen reader behavior.
    if (this.disabled) {
      this.setAttribute('tabindex', '-1');
      this.setAttribute('aria-disabled', 'true');
    } else {
      this.setAttribute('tabindex', '0');
      this.setAttribute('aria-disabled', 'false');
    }
    // TODO: also react to the open attribute changing.
  }
}

Dans l'exemple, nous définissons des attributs supplémentaires sur <app-drawer> lorsqu'un attribut disabled est modifié. Bien que nous ne fassions pas cela ici, vous pouvez également utiliser attributeChangedCallback pour synchroniser une propriété JavaScript avec son attribut.

Mises à niveau des éléments

Code HTML amélioré progressivement

Nous avons déjà appris que les éléments personnalisés sont définis en appelant customElements.define(). Toutefois, cela ne signifie pas que vous devez définir et enregistrer un élément personnalisé en une seule fois.

Les éléments personnalisés peuvent être utilisés avant que leur définition ne soit enregistrée.

L'amélioration progressive est une caractéristique des éléments personnalisés. En d'autres termes, vous pouvez déclarer un ensemble d'éléments <app-drawer> sur la page et n'appeler customElements.define('app-drawer', ...) que bien plus tard. En effet, le navigateur traite les éléments personnalisés potentiels différemment grâce à des balises inconnues. Le processus consistant à appeler define() et à attribuer à un élément existant une définition de classe est appelé "mise à niveau des éléments".

Pour savoir quand un nom de balise est défini, vous pouvez utiliser window.customElements.whenDefined(). Elle renvoie une promesse qui se résout lorsque l'élément est défini.

customElements.whenDefined('app-drawer').then(() => {
  console.log('app-drawer defined');
});

Exemple – Retarder le travail jusqu'à ce qu'un ensemble d'éléments enfants soit mis à niveau

<share-buttons>
  <social-button type="twitter"><a href="...">Twitter</a></social-button>
  <social-button type="fb"><a href="...">Facebook</a></social-button>
  <social-button type="plus"><a href="...">G+</a></social-button>
</share-buttons>
// Fetch all the children of <share-buttons> that are not defined yet.
let undefinedButtons = buttons.querySelectorAll(':not(:defined)');

let promises = [...undefinedButtons].map((socialButton) => {
  return customElements.whenDefined(socialButton.localName);
});

// Wait for all the social-buttons to be upgraded.
Promise.all(promises).then(() => {
  // All social-button children are ready.
});

Contenu défini par les éléments

Les éléments personnalisés peuvent gérer leur propre contenu à l'aide des API DOM dans le code de l'élément. Les réactions sont utiles à cet effet.

Exemple – Créez un élément avec du code HTML par défaut:

customElements.define('x-foo-with-markup', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = "<b>I'm an x-foo-with-markup!</b>";
  }
  // ...
});

La déclaration de cette balise aura les effets suivants:

<x-foo-with-markup>
  <b>I'm an x-foo-with-markup!</b>
</x-foo-with-markup>

// À FAIRE: DevSite – Exemple de code supprimé, car il utilisait des gestionnaires d'événements intégrés

Créer un élément qui utilise Shadow DOM

Le Shadow DOM permet à un élément de posséder, d'afficher et de styliser un fragment de DOM distinct du reste de la page. Vous pouvez même masquer une application entière au sein d'une seule balise:

<!-- chat-app's implementation details are hidden away in Shadow DOM. -->
<chat-app></chat-app>

Pour utiliser Shadow DOM dans un élément personnalisé, appelez this.attachShadow dans votre constructor:

let tmpl = document.createElement('template');
tmpl.innerHTML = `
  <style>:host { ... }</style> <!-- look ma, scoped styles -->
  <b>I'm in shadow dom!</b>
  <slot></slot>
`;

customElements.define('x-foo-shadowdom', class extends HTMLElement {
  constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to the element.
    let shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.appendChild(tmpl.content.cloneNode(true));
  }
  // ...
});

Exemples d'utilisation :

<x-foo-shadowdom>
  <p><b>User's</b> custom text</p>
</x-foo-shadowdom>

<!-- renders as -->
<x-foo-shadowdom>
  #shadow-root
  <b>I'm in shadow dom!</b>
  <slot></slot> <!-- slotted content appears here -->
</x-foo-shadowdom>

Texte personnalisé de l'utilisateur

// À FAIRE: DevSite – Exemple de code supprimé, car il utilisait des gestionnaires d'événements intégrés

Créer des éléments à partir d'un <template>

Pour ceux qui ne sont pas familiers, l'élément <template> vous permet de déclarer des fragments du DOM qui sont analysés, inertes au chargement de la page et peuvent être activés ultérieurement au moment de l'exécution. Il s'agit d'une autre primitive d'API de la famille des composants Web. Les modèles constituent un espace réservé idéal pour déclarer la structure d'un élément personnalisé.

Exemple:enregistrement d'un élément avec du contenu Shadow DOM créé à partir d'un <template>:

<template id="x-foo-from-template">
  <style>
    p { color: green; }
  </style>
  <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p>
</template>

<script>
  let tmpl = document.querySelector('#x-foo-from-template');
  // If your code is inside of an HTML Import you'll need to change the above line to:
  // let tmpl = document.currentScript.ownerDocument.querySelector('#x-foo-from-template');

  customElements.define('x-foo-from-template', class extends HTMLElement {
    constructor() {
      super(); // always call super() first in the constructor.
      let shadowRoot = this.attachShadow({mode: 'open'});
      shadowRoot.appendChild(tmpl.content.cloneNode(true));
    }
    // ...
  });
</script>

Ces quelques lignes de code ont du punch. Récapitulons les principaux éléments:

  1. Nous définissons un nouvel élément en HTML: <x-foo-from-template>
  2. Le Shadow DOM de l'élément est créé à partir d'un <template>.
  3. Le DOM de l'élément est localisé par rapport à celui-ci grâce au Shadow DOM.
  4. La portée du code CSS interne de l'élément est limitée à celui-ci grâce au Shadow DOM.

Je suis dans Shadow DOM. Mon balisage a été imprimé à partir d'un élément <template>.

// À FAIRE: DevSite – Exemple de code supprimé, car il utilisait des gestionnaires d'événements intégrés

Appliquer un style à un élément personnalisé

Même si votre élément définit son propre style à l'aide de Shadow DOM, les utilisateurs peuvent appliquer un style à votre élément personnalisé à partir de leur page. C'est ce qu'on appelle les "styles définis par l'utilisateur".

<!-- user-defined styling -->
<style>
  app-drawer {
    display: flex;
  }
  panel-item {
    transition: opacity 400ms ease-in-out;
    opacity: 0.3;
    flex: 1;
    text-align: center;
    border-radius: 50%;
  }
  panel-item:hover {
    opacity: 1.0;
    background: rgb(255, 0, 255);
    color: white;
  }
  app-panel > panel-item {
    padding: 5px;
    list-style: none;
    margin: 0 7px;
  }
</style>

<app-drawer>
  <panel-item>Do</panel-item>
  <panel-item>Re</panel-item>
  <panel-item>Mi</panel-item>
</app-drawer>

Vous vous demandez peut-être comment fonctionne la spécificité CSS si des styles de l'élément sont définis dans Shadow DOM. En termes de spécificité, les styles d'utilisateur l'emportent. Elles remplaceront toujours le style défini par l'élément. Consultez la section Créer un élément qui utilise Shadow DOM.

Préappliquer un style à des éléments non enregistrés

Avant qu'un élément ne soit mis à niveau, vous pouvez le cibler dans CSS à l'aide de la pseudo-classe :defined. Cela est utile pour prédéfinir le style d'un composant. Par exemple, vous pouvez éviter la mise en page ou d'autres erreurs FOUC visuelles en masquant les composants non définis et en les faisant apparaître en fondu lorsqu'ils sont définis.

Exemple - Masquez <app-drawer> avant qu'il ne soit défini:

app-drawer:not(:defined) {
  /* Pre-style, give layout, replicate app-drawer's eventual styles, etc. */
  display: inline-block;
  height: 100vh;
  opacity: 0;
  transition: opacity 0.3s ease-in-out;
}

Une fois que <app-drawer> est défini, le sélecteur (app-drawer:not(:defined)) ne correspond plus.

Extension d'éléments

L'API Custom Elements est utile pour créer des éléments HTML, mais aussi pour développer d'autres éléments personnalisés ou même le code HTML intégré au navigateur.

Étendre un élément personnalisé

L'extension d'un autre élément personnalisé se fait en étendant sa définition de classe.

Exemple – Créez <fancy-app-drawer> qui étend <app-drawer>:

class FancyDrawer extends AppDrawer {
  constructor() {
    super(); // always call super() first in the constructor. This also calls the extended class' constructor.
    // ...
  }

  toggleDrawer() {
    // Possibly different toggle implementation?
    // Use ES2015 if you need to call the parent method.
    // super.toggleDrawer()
  }

  anotherMethod() {
    // ...
  }
}

customElements.define('fancy-app-drawer', FancyDrawer);

Extension des éléments HTML natifs

Imaginons que vous souhaitiez créer un <button> plus sophistiqué. Au lieu de répliquer le comportement et les fonctionnalités de <button>, il est préférable d'améliorer progressivement l'élément existant à l'aide d'éléments personnalisés.

Un élément intégré personnalisé est un élément personnalisé qui étend l'une des balises HTML intégrées au navigateur. Le principal avantage de l'extension d'un élément existant est d'obtenir toutes ses fonctionnalités (propriétés DOM, méthodes, accessibilité). Le meilleur moyen d'écrire une application Web progressive est d'améliorer progressivement les éléments HTML existants.

Pour étendre un élément, vous devez créer une définition de classe qui hérite de l'interface DOM appropriée. Par exemple, un élément personnalisé qui étend <button> doit hériter de HTMLButtonElement au lieu de HTMLElement. De même, un élément qui étend <img> doit étendre HTMLImageElement.

Exemple – Extension de <button>:

// See https://html.spec.whatwg.org/multipage/indices.html#element-interfaces
// for the list of other DOM interfaces.
class FancyButton extends HTMLButtonElement {
  constructor() {
    super(); // always call super() first in the constructor.
    this.addEventListener('click', e => this.drawRipple(e.offsetX, e.offsetY));
  }

  // Material design ripple animation.
  drawRipple(x, y) {
    let div = document.createElement('div');
    div.classList.add('ripple');
    this.appendChild(div);
    div.style.top = `${y - div.clientHeight/2}px`;
    div.style.left = `${x - div.clientWidth/2}px`;
    div.style.backgroundColor = 'currentColor';
    div.classList.add('run');
    div.addEventListener('transitionend', (e) => div.remove());
  }
}

customElements.define('fancy-button', FancyButton, {extends: 'button'});

Notez que l'appel à define() change légèrement lors de l'extension d'un élément natif. Le troisième paramètre obligatoire indique au navigateur le tag que vous étendez. Cette étape est nécessaire, car de nombreuses balises HTML partagent la même interface DOM. <section>, <address> et <em> (parmi d'autres) partagent HTMLElement ; <q> et <blockquote> partagent HTMLQuoteElement, etc. Spécifier {extends: 'blockquote'} permet au navigateur de savoir que vous créez un <blockquote> secondaire au lieu d'un <q>. Consultez les spécifications HTML pour obtenir la liste complète des interfaces DOM HTML.

Les utilisateurs d'un élément intégré personnalisé peuvent l'utiliser de plusieurs façons. Il peut la déclarer en ajoutant l'attribut is="" à la balise native:

<!-- This <button> is a fancy button. -->
<button is="fancy-button" disabled>Fancy button!</button>

créez une instance en JavaScript:

// Custom elements overload createElement() to support the is="" attribute.
let button = document.createElement('button', {is: 'fancy-button'});
button.textContent = 'Fancy button!';
button.disabled = true;
document.body.appendChild(button);

ou utilisez l'opérateur new:

let button = new FancyButton();
button.textContent = 'Fancy button!';
button.disabled = true;

Voici un autre exemple qui étend <img>.

Exemple – Extension de <img>:

customElements.define('bigger-img', class extends Image {
  // Give img default size if users don't specify.
  constructor(width=50, height=50) {
    super(width * 10, height * 10);
  }
}, {extends: 'img'});

Les utilisateurs déclarent ce composant comme:

<!-- This <img> is a bigger img. -->
<img is="bigger-img" width="15" height="20">

ou créez une instance en JavaScript:

const BiggerImage = customElements.get('bigger-img');
const image = new BiggerImage(15, 20); // pass constructor values like so.
console.assert(image.width === 150);
console.assert(image.height === 200);

Informations diverses

Éléments inconnus et éléments personnalisés non définis

Le langage HTML est indulgent et flexible. Par exemple, si vous déclarez <randomtagthatdoesntexist> sur une page, le navigateur se contente de l'accepter. Pourquoi les tags non standards fonctionnent-ils ? La réponse est simple : la spécification HTML le permet. Les éléments qui ne sont pas définis par la spécification sont analysés en tant que HTMLUnknownElement.

Il en va de même pour les éléments personnalisés. Les éléments personnalisés potentiels sont analysés en tant que HTMLElement s'ils sont créés avec un nom valide (comprend un "-"). Vous pouvez le vérifier dans un navigateur compatible avec les éléments personnalisés. Lancez la console : Ctrl+Maj+J (ou Cmd+Opt+J sur Mac), puis collez les lignes de code suivantes:

// "tabs" is not a valid custom element name
document.createElement('tabs') instanceof HTMLUnknownElement === true

// "x-tabs" is a valid custom element name
document.createElement('x-tabs') instanceof HTMLElement === true

Documentation de référence de l'API

L'élément global customElements définit les méthodes utiles pour utiliser des éléments personnalisés.

define(tagName, constructor, options)

Définit un nouvel élément personnalisé dans le navigateur.

Exemple

customElements.define('my-app', class extends HTMLElement { ... });
customElements.define(
    'fancy-button', class extends HTMLButtonElement { ... }, {extends: 'button'});

get(tagName)

Renvoie le constructeur de l'élément à partir d'un nom de balise d'élément personnalisé valide. Renvoie undefined si aucune définition d'élément n'a été enregistrée.

Exemple

let Drawer = customElements.get('app-drawer');
let drawer = new Drawer();

whenDefined(tagName)

Renvoie une promesse qui se résout lorsque l'élément personnalisé est défini. Si l'élément est déjà défini, résolvez le problème immédiatement. Rejetée si le nom de la balise n'est pas un nom d'élément personnalisé valide.

Exemple

customElements.whenDefined('app-drawer').then(() => {
  console.log('ready!');
});

Historique et navigateurs pris en charge

Si vous suivez les composants Web depuis deux ans, vous savez que Chrome 36 ou version ultérieure a implémenté une version de l'API Custom Elements qui utilise document.registerElement() au lieu de customElements.define(). Elle est désormais considérée comme une version obsolète de la norme, appelée v0. customElements.define() est le nouveau point d'intérêt et ce que les fournisseurs de navigateurs commencent à mettre en œuvre. Il s'agit des éléments personnalisés v1.

Si l'ancienne spécification v0 vous intéresse, consultez l'article html5rocks.

Prise en charge des navigateurs

Chrome 54 (état), Safari 10.1 (état) et Firefox 63 (état) utilisent la version 1 des éléments personnalisés. Edge a commencé le développement.

Pour activer la détection d'éléments personnalisés, vérifiez l'existence de window.customElements:

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

En attendant que la compatibilité avec les navigateurs soit largement disponible, un polyfill autonome est disponible pour Custom Elements v1. Toutefois, nous vous recommandons d'utiliser le chargeur webcomponents.js pour charger de manière optimale les polyfills des composants Web. Le chargeur utilise la détection de fonctionnalités pour ne charger de manière asynchrone que les pollyfills requis par le navigateur.

Installez-la:

npm install --save @webcomponents/webcomponentsjs

Utilisation :

<!-- Use the custom element on the page. -->
<my-element></my-element>

<!-- Load polyfills; note that "loader" will load these async -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js" defer></script>

<!-- Load a custom element definitions in `waitFor` and return a promise -->
<script type="module">
  function loadScript(src) {
    return new Promise(function(resolve, reject) {
      const script = document.createElement('script');
      script.src = src;
      script.onload = resolve;
      script.onerror = reject;
      document.head.appendChild(script);
    });
  }

  WebComponents.waitFor(() => {
    // At this point we are guaranteed that all required polyfills have
    // loaded, and can use web components APIs.
    // Next, load element definitions that call `customElements.define`.
    // Note: returning a promise causes the custom elements
    // polyfill to wait until all definitions are loaded and then upgrade
    // the document in one batch, for better performance.
    return loadScript('my-element.js');
  });
</script>

Conclusion

Les éléments personnalisés nous offrent un nouvel outil pour définir de nouvelles balises HTML dans le navigateur et créer des composants réutilisables. Combinez-les avec les autres nouvelles primitives de la plate-forme, comme Shadow DOM et <template>, pour obtenir une vue d'ensemble des composants Web:

  • Création et extension de composants réutilisables entre navigateurs (norme Web)
  • Ne nécessite aucune bibliothèque ni aucun framework pour commencer. Vanilla JS/HTML FTW!
  • Fournit un modèle de programmation familier. Il s'agit simplement de DOM/CSS/HTML.
  • Fonctionne bien avec d'autres nouvelles fonctionnalités de la plate-forme Web (Shadow DOM, <template>, propriétés CSS personnalisées, etc.)
  • Étroitement intégré aux outils de développement du navigateur.
  • Exploiter les fonctionnalités d'accessibilité existantes