Bonnes pratiques concernant les éléments personnalisés

Les éléments personnalisés vous permettent de créer vos propres balises HTML. Cette liste de contrôle couvre les meilleures pratiques pour vous aider à créer des éléments de haute qualité.

Les éléments personnalisés vous permettent d'étendre le code HTML et de définir vos propres balises. Ces fonctionnalités sont incroyablement puissantes, mais elles sont également de bas niveau, ce qui signifie qu'il n'est pas toujours évident de savoir comment implémenter au mieux votre propre élément.

Pour vous aider à créer les meilleures expériences possibles, nous avons élaboré cette checklist. Il décompose tout ce qu'il faut pour être un élément personnalisé qui se comporte correctement.

Checklist

Shadow DOM

Créez une racine fantôme pour encapsuler les styles.

Why? L'encapsulation de styles dans la racine fantôme de votre élément garantit son bon fonctionnement, quel que soit son emplacement d'utilisation. Cela est particulièrement important si un développeur souhaite placer votre élément à l'intérieur de la racine fantôme d'un autre élément. Cela s'applique même aux éléments simples tels qu'une case à cocher ou une case d'option. Il se peut que le seul contenu à l'intérieur de la racine fantôme soit les styles eux-mêmes.
Exemple L'élément <howto-checkbox>.

Créez la racine fantôme dans le constructeur.

Why? Le constructeur intervient lorsque vous avez la connaissance exclusive de votre élément. C'est le moment idéal pour configurer les détails d'implémentation dont vous ne voulez pas que les autres éléments perturbent. Si vous effectuez cette tâche dans un rappel ultérieur, tel que connectedCallback, vous devrez vous prémunir des cas où votre élément est dissocié, puis réassocié au document.
Exemple L'élément <howto-checkbox>.

Placez tous les enfants créés par l'élément dans sa racine fantôme.

Why? Les enfants créés par votre élément font partie de son implémentation et doivent être privés. Sans la protection d'une racine fantôme, l'extérieur de JavaScript peut interférer par inadvertance avec ces éléments enfants.
Exemple L'élément <howto-tabs>.

Utilisez <slot> pour projeter les éléments enfants Light DOM dans votre Shadow DOM.

Why? Autorisez les utilisateurs de votre composant à spécifier du contenu, car les enfants HTML rendent votre composant plus composable. Lorsqu'un navigateur n'accepte pas les éléments personnalisés, le contenu imbriqué reste disponible, visible et accessible.
Exemple L'élément <howto-tabs>.

Définissez un style d'affichage :host (par exemple, block, inline-block, flex), sauf si vous préférez la valeur par défaut inline.

Why? Les éléments personnalisés sont display: inline par défaut. Par conséquent, définir leurs width ou height n'aura aucun effet. Cela est souvent surprenant pour les développeurs et peut entraîner des problèmes liés à la mise en page. Sauf si vous préférez un affichage inline, vous devez toujours définir une valeur display par défaut.
Exemple L'élément <howto-checkbox>.

Ajoutez un style d'affichage :host qui respecte l'attribut masqué.

Why? Un élément personnalisé avec un style display par défaut, comme :host { display: block }, remplacera l' attribut hidden intégré de spécificité inférieure. Cela peut vous surprendre si vous vous attendez à définir l'attribut hidden sur votre élément pour l'afficher display: none. En plus du style display par défaut, ajoutez la prise en charge de hidden avec :host([hidden]) { display: none }.
Exemple L'élément <howto-checkbox>.

Attributs et propriétés

Ne remplacez pas les attributs globaux définis par l'auteur.

Why? Les attributs globaux sont ceux qui sont présents dans tous les éléments HTML. Voici quelques exemples : tabindex et role. Un élément personnalisé peut définir sa tabindex initiale sur 0 pour pouvoir être sélectionné au clavier. Toutefois, vous devez toujours vérifier d'abord si le développeur qui utilise votre élément a défini cette valeur sur une autre valeur. Par exemple, si elle a défini tabindex sur -1, cela indique qu'elle ne souhaite pas que l'élément soit interactif.
Exemple L'élément <howto-checkbox>. Ce point est expliqué plus en détail dans la section Ne pas remplacer l'auteur de la page.

Acceptez toujours les données primitives (chaînes, nombres, valeurs booléennes) en tant qu'attributs ou propriétés.

Why? Les éléments personnalisés, tout comme leurs équivalents intégrés, doivent être configurables. La configuration peut être transmise de manière déclarative, via des attributs ou de manière impérative via des propriétés JavaScript. Idéalement, chaque attribut doit également être associé à une propriété correspondante.
Exemple L'élément <howto-checkbox>.

Essayez de synchroniser les attributs et les propriétés des données primitifs, en réfléchissant de propriété à attribut, et inversement.

Why? Vous ne savez jamais comment un utilisateur interagira avec votre élément. Ils peuvent définir une propriété en JavaScript, puis s'attendre à lire cette valeur à l'aide d'une API telle que getAttribute(). Si chaque attribut a une propriété correspondante et que les deux se reflètent, les utilisateurs pourront plus facilement travailler avec votre élément. En d'autres termes, l'appel de setAttribute('foo', value) doit également définir une propriété foo correspondante, et inversement. Il existe bien sûr des exceptions à cette règle. Vous ne devez pas refléter les propriétés de haute fréquence (par exemple, currentTime) dans un lecteur vidéo. Faites appel à votre bon sens. Si vous avez l'impression qu'un utilisateur va interagir avec une propriété ou un attribut et qu'il n'est pas contraignant de le refléter, faites-le.
Exemple L'élément <howto-checkbox>. Cela est expliqué plus en détail dans la section Éviter les problèmes de réentrée.

Essayez de n'accepter que des données enrichies (objets, tableaux) comme propriétés.

Why? En règle générale, il n'existe aucun exemple d'éléments HTML intégrés qui acceptent les données enrichies (objets et tableaux JavaScript simples) via leurs attributs. Les données enrichies sont acceptées via des appels de méthode ou des propriétés. L'acceptation de données enrichies en tant qu'attributs présente quelques inconvénients évidents: la sérialisation d'un objet volumineux en chaîne peut s'avérer coûteuse, et toutes les références d'objet seront perdues dans ce processus de stringification. Par exemple, si vous définissez une chaîne avec un objet qui fait référence à un autre objet, voire à un nœud DOM, ces références seront perdues.

Ne pas refléter les propriétés de données enrichies sur les attributs.

Why? La réflexion de propriétés de données enrichies en attributs s'avère inutilement coûteuse, car elle nécessite de sérialiser et désérialiser les mêmes objets JavaScript. Il est probablement préférable de l'éviter, sauf si vous avez un cas d'utilisation qui ne peut être résolu qu'avec cette fonctionnalité.

Envisagez de vérifier les propriétés qui ont peut-être été définies avant la mise à niveau de l'élément.

Why? Un développeur utilisant votre élément peut tenter de définir une propriété sur celui-ci avant que sa définition n'ait été chargée. Cela est particulièrement vrai si le développeur utilise un framework qui gère le chargement des composants, leur intégration à la page et la liaison de leurs propriétés à un modèle.
Exemple L'élément <howto-checkbox>. Pour en savoir plus, consultez la section Rendre les propriétés différées.

N'appliquez pas vous-même les cours.

Why? Les éléments qui doivent exprimer leur état doivent le faire à l'aide d'attributs. L'attribut class est généralement considéré comme appartenant au développeur qui utilise votre élément. Si vous écrivez sur celui-ci vous-même, vous risquez d'écraser par inadvertance des classes de développeur.

Événements

Envoyez les événements en réponse à l'activité des composants internes.

Why? Les propriétés de votre composant peuvent changer en réponse à une activité dont seul le composant a connaissance, par exemple si un minuteur ou une animation se termine, ou si le chargement d'une ressource est terminé. En réponse à ces modifications, il est utile d'envoyer des événements pour avertir l'hôte que l'état du composant est différent.

N'envoyez pas d'événements en réponse à la définition d'une propriété par l'hôte (flux de données vers le bas).

Why? Lors de l'envoi d'un événement en réponse à la définition d'une propriété par l'hôte, une propriété est superflue (l'hôte connaît l'état actuel puisqu'il vient de la définir). La distribution d'événements en réponse à la définition d'une propriété par l'hôte peut provoquer des boucles infinies avec les systèmes de liaison de données.
Exemple L'élément <howto-checkbox>.

Vidéo explicative

Ne pas écraser l'auteur de la page

Il est possible qu'un développeur utilisant votre élément souhaite remplacer une partie de son état initial. (par exemple, en modifiant son role ARIA ou sa sélection avec tabindex). Vérifiez si ces attributs et tous les autres attributs globaux ont été définis avant d'appliquer vos propres valeurs.

connectedCallback() {
  if (!this.hasAttribute('role'))
    this.setAttribute('role', 'checkbox');
  if (!this.hasAttribute('tabindex'))
    this.setAttribute('tabindex', 0);

Rendre les propriétés différées

Un développeur peut tenter de définir une propriété sur votre élément avant que sa définition n'ait été chargée. Cela est particulièrement vrai si le développeur utilise un framework qui gère le chargement des composants, leur insertion dans la page et la liaison de leurs propriétés à un modèle.

Dans l'exemple suivant, Angular lie de manière déclarative la propriété isChecked de son modèle à la propriété checked de la case à cocher. Si la définition de la case à cocher a été chargée de manière différée, Angular peut tenter de définir la propriété cochée avant la mise à niveau de l'élément.

<howto-checkbox [checked]="defaults.isChecked"></howto-checkbox>

Un élément personnalisé doit gérer ce scénario en vérifiant si des propriétés ont déjà été définies sur son instance. L'élément <howto-checkbox> illustre ce schéma à l'aide d'une méthode appelée _upgradeProperty().

connectedCallback() {
  ...
  this._upgradeProperty('checked');
}

_upgradeProperty(prop) {
  if (this.hasOwnProperty(prop)) {
    let value = this[prop];
    delete this[prop];
    this[prop] = value;
  }
}

_upgradeProperty() capture la valeur de l'instance non mise à niveau et supprime la propriété afin de ne pas omettre le setter de la propriété de l'élément personnalisé. Ainsi, lorsque la définition de l'élément se charge finalement, elle peut immédiatement refléter l'état correct.

Éviter les problèmes de rentrée

Il est tentant d'utiliser attributeChangedCallback() pour refléter l'état d'une propriété sous-jacente, par exemple:

// When the [checked] attribute changes, set the checked property to match.
attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'checked')
    this.checked = newValue;
}

Toutefois, cela peut créer une boucle infinie si le setter de la propriété reflète également l'attribut.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    // OOPS! This will cause an infinite loop because it triggers the
    // attributeChangedCallback() which then sets this property again.
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

Une alternative consiste à permettre au setter de la propriété de refléter l'attribut et à demander au getter de déterminer sa valeur en fonction de l'attribut.

set checked(value) {
  const isChecked = Boolean(value);
  if (isChecked)
    this.setAttribute('checked', '');
  else
    this.removeAttribute('checked');
}

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

Dans cet exemple, l'ajout ou la suppression de l'attribut définit également la propriété.

Enfin, attributeChangedCallback() peut être utilisé pour gérer les effets secondaires tels que l'application des états ARIA.

attributeChangedCallback(name, oldValue, newValue) {
  const hasValue = newValue !== null;
  switch (name) {
    case 'checked':
      // Note the attributeChangedCallback is only handling the *side effects*
      // of setting the attribute.
      this.setAttribute('aria-checked', hasValue);
      break;
    ...
  }
}