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 les balises existantes et de créer des composants Web réutilisables.

Avec les éléments personnalisés, les développeurs Web peuvent créer des balises HTML, renforcer des balises HTML existantes ou étendre les composants créés par d'autres développeurs. L'API est la base des composants Web. Il offre un moyen basé sur des normes Web de créer des composants réutilisables en utilisant rien de plus que des fichiers JS/HTML/CSS vanilla. Cela permet de réduire le code, de le rendre modulaire et de le réutiliser davantage dans nos applications.

Introduction

Le navigateur est un excellent outil pour structurer les applications Web. Il s'agit du langage HTML. Vous en avez peut-être déjà entendu parler. Il est déclaratif, portable, bien pris en charge et facile à utiliser. Bien que le langage HTML soit excellent, son vocabulaire et son extensibilité sont limités. Jusqu'à présent, la norme HTML Living n'a toujours pas pu associer automatiquement le comportement JS à votre balisage.

Les éléments personnalisés sont la réponse à la modernisation du code HTML, au remplissage des éléments manquants et au regroupement de la structure avec le comportement. Si le code HTML ne fournit pas de solution à un problème, nous pouvons créer un élément personnalisé qui le fait. Les éléments personnalisés apprennent de nouvelles astuces au navigateur tout en préservant les avantages du code HTML.

Définir un nouvel élément

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

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

Exemple : définition d'un panneau de panneau de tiroir 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 {...});

Exemple d'utilisation :

<app-drawer></app-drawer>

N'oubliez pas que l'utilisation d'un élément personnalisé ne diffère pas de l'utilisation d'un <div> ou de tout autre élément. Les instances peuvent être déclarées sur la page, créées de manière dynamique en JavaScript, des écouteurs d'événements peuvent être associés, etc. Poursuivez votre lecture pour en savoir plus.

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. Cela signifie que toutes les propriétés/méthodes que vous ajoutez à la classe font partie de l'interface DOM de l'élément. Utilisez essentiellement la classe pour créer une API JavaScript publique pour votre balise.

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 comportant une propriété open, une propriété disabled et une méthode toggleDrawer(). Il reflète également les propriétés en tant qu'attributs HTML.

Une fonctionnalité intéressante 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 ainsi (😉) que l'élément peut associer un écouteur click à lui-même. Vous n'êtes pas limité aux écouteurs d'événements. L'intégralité 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), interroger les nœuds (this.querySelectorAll('.items')), etc.

Règles relatives à la 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 tous 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 assure é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 essayez de le faire, une exception DOMException sera générée. Une fois que vous avez informé le navigateur d'une nouvelle balise, c'est terminé. Aucune rétractation.
  3. Les éléments personnalisés ne peuvent pas être autofermés, car le langage HTML n'autorise que quelques éléments à être autofermés. Écrivez toujours une balise fermante (<app-drawer></app-drawer>).

Réactions d'é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. Ces éléments sont appelés réactions d'éléments personnalisés.

Nom Appelé lorsque
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 Shadow DOM. Consultez la spécification pour connaître les restrictions applicables dans constructor.
connectedCallback Appelé chaque fois que l'élément est inséré dans le DOM. Utile pour exécuter du code de configuration, tel que la récupération de ressources ou le rendu. En règle générale, vous devez essayer de retarder le travail jusqu'à ce moment-là.
disconnectedCallback Appelé chaque fois que l'élément est supprimé du DOM. Utile pour exécuter du code de nettoyage.
attributeChangedCallback(attrName, oldVal, newVal) Appelé lorsqu'un attribut observé a été ajouté, supprimé, mis à jour ou remplacé. Appelé également 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é vers un nouvel élément document (par exemple, un élément 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 recevrez un disconnectedCallback() juste après la suppression de votre élément du DOM (par exemple, l'utilisateur appelle el.remove()).

Exemple : ajouter des 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 des réactions si/lorsque cela a du sens. Si votre élément est suffisamment complexe et ouvre une connexion à IndexedDB dans connectedCallback(), effectuez le nettoyage nécessaire dans disconnectedCallback(). Mais attention ! Vous ne pouvez pas vous attendre à ce que votre élément soit supprimé du DOM dans toutes les circonstances. Par exemple, disconnectedCallback() n'est jamais appelé si l'utilisateur ferme l'onglet.

Propriétés et attributs

Refléter les propriétés dans les 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 en 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 vers les attributs. Presque toutes les propriétés du code HTML fonctionnent de cette manière. 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 des attributs pour fonctionner.

La réflexion d'une propriété est utile partout où vous souhaitez synchroniser la représentation DOM de l'élément avec son état JavaScript. Vous pouvez refléter une propriété pour que le style défini par l'utilisateur s'applique lorsque l'état JS change.

Consultez notre <app-drawer>. Un consommateur de ce composant peut vouloir l'estomper et/ou empêcher l'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 en JS, nous souhaitons que cet attribut soit ajouté au DOM afin que le sélecteur de l'utilisateur corresponde. L'élément peut fournir ce comportement en reflétant la valeur dans 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 appelle cette méthode pour chaque modification apportée aux attributs listé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 le fassions pas ici, vous pouvez également utiliser attributeChangedCallback pour synchroniser une propriété JavaScript avec son attribut.

Mises à niveau des éléments

HTML amélioré progressivement

Nous avons déjà appris que les éléments personnalisés sont définis en appelant customElements.define(). 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 l'enregistrement de leur définition.

L'amélioration progressive est une fonctionnalité d'éléments personnalisés. En d'autres termes, vous pouvez déclarer un ensemble d'éléments <app-drawer> sur la page et n'invoquer jamais customElements.define('app-drawer', ...) avant bien plus tard. En effet, le navigateur traite les éléments personnalisés potentiels différemment grâce aux balises inconnues. Le processus d'appel de define() et d'attribution d'une définition de classe à un élément existant s'appelle "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 l'élément

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 très utiles pour cela.

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>";
  }
  // ...
});

Déclarer cette balise génère les résultats 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 dans 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));
  }
  // ...
});

Exemple 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

// TODO : 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>

Si vous ne la connaissez pas, 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:Enregistrer 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 sont très efficaces. Voyons les principaux éléments en jeu :

  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 local à l'élément grâce au Shadow DOM.
  4. Le CSS interne de l'élément est limité à l'élément grâce au Shadow DOM.

Je suis dans Shadow DOM. Mon balisage est basé sur 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 styliser votre élément personnalisé à partir de leur page. Ces styles sont appelés "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 les styles de l'élément sont définis dans Shadow DOM. En termes de spécificité, les styles utilisateur l'emportent. Ils remplacent toujours le style défini par l'élément. Consultez la section Créer un élément qui utilise Shadow DOM.

Pré-stylisation des éléments non enregistrés

Avant de mettre à niveau un élément, vous pouvez le cibler en CSS à l'aide de la pseudo-classe :defined. Cela est utile pour préstyliser un composant. Par exemple, vous pouvez souhaiter éviter les erreurs de parcours utilisateur visuelles en masquant les composants non définis et en les faisant apparaître progressivement lorsqu'ils sont définis.

Exemple – Masquer la valeur <app-drawer> avant qu'elle soit définie:

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 <app-drawer> défini, le sélecteur (app-drawer:not(:defined)) ne correspond plus.

Éléments extensibles

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

Étendre un élément personnalisé

Pour étendre un autre élément personnalisé, vous devez étendre sa définition de classe.

Exemple : créer un <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);

Étendre les éléments HTML natifs

Supposons que vous souhaitiez créer un <button> plus élaboré. Au lieu de reproduire 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 du navigateur. L'avantage principal d'étendre un élément existant est de bénéficier de toutes ses fonctionnalités (propriétés DOM, méthodes, accessibilité). Il n'existe pas de meilleure façon d'écrire une application Web progressive que 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 de define() change légèrement lorsque vous étendez un élément natif. Le troisième paramètre obligatoire indique au navigateur la balise que vous étendez. Cela est nécessaire, car de nombreuses balises HTML partagent la même interface DOM. <section>, <address> et <em> (entre autres) partagent tous HTMLElement. <q> et <blockquote> partagent HTMLQuoteElement, etc. Spécifier {extends: 'blockquote'} permet au navigateur de savoir que vous créez un <blockquote> amélioré au lieu d'un <q>. Consultez la spécification HTML pour obtenir la liste complète des interfaces DOM du HTML.

Les utilisateurs d'un élément intégré personnalisé peuvent l'utiliser de plusieurs façons. Ils peuvent le 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 utiliser l'opérateur new :

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

Voici un autre exemple qui étend <img>.

Exemple : étendre <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 suit :

<!-- 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);

Divers détails

Éléments inconnus par rapport aux éléments personnalisés non définis

Le langage HTML est indulgent et flexible. Par exemple, déclarez <randomtagthatdoesntexist> sur une page. Le navigateur l'acceptera avec plaisir. Pourquoi les balises non standards fonctionnent-elles ? La réponse est que la spécification HTML l'autorise. Les éléments qui ne sont pas définis par la spécification sont analysés en tant que HTMLUnknownElement.

Il n'en va pas 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 (qui inclut 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

Le global customElements définit des méthodes utiles pour travailler avec 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)

À partir d'un nom de balise d'élément personnalisé valide, renvoie le constructeur de l'élément. 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 immédiatement. Refusé 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 compatibles

Si vous suivez les composants Web depuis quelques années, vous savez que Chrome 36 et versions ultérieures ont implémenté une version de l'API Custom Elements qui utilise document.registerElement() au lieu de customElements.define(). Cette version est désormais considérée comme obsolète et est appelée v0. customElements.define() est la nouvelle tendance et ce que les fournisseurs de navigateurs commencent à implémenter. Il s'agit de Custom Elements 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) sont compatibles avec la version 1 des éléments personnalisés. Le développement d'Edge a commencé.

Pour détecter des éléments personnalisés, vérifiez l'existence de window.customElements :

const supportsCustomElementsV1 = 'customElements' in window;

Polyfill

En attendant que les navigateurs soient compatibles, un polyfill autonome est disponible pour les éléments personnalisés 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 remplissages de polymères nécessaires requis par le navigateur.

Installez-le :

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. En les combinant aux autres nouvelles primitives de plate-forme telles que Shadow DOM et <template>, nous commençons à comprendre l'ensemble des Web Components :

  • Inter-navigateur (norme Web) pour créer et étendre des composants réutilisables.
  • Aucune bibliothèque ni aucun framework n'est nécessaire pour commencer. Vanilla JS/HTML FTW!
  • Fournit un modèle de programmation familier. Il s'agit simplement de DOM/CSS/HTML.
  • Fonctionne bien avec les autres nouvelles fonctionnalités de la plate-forme Web (Shadow DOM, <template>, propriétés personnalisées CSS, etc.)
  • Intégration étroite avec les outils de développement du navigateur.
  • Exploitez les fonctionnalités d'accessibilité existantes.