Éléments personnalisés associés à un formulaire

N'utilisez pas un <input> masqué pour afficher la valeur d'un contrôle personnalisé dans un formulaire. Utilisez des éléments personnalisés associés à un formulaire (FACE) : avec formAssociated et ElementInternals, l'élément envoie sa propre valeur, participe à la validation et reflète l'état du formulaire de manière native.

class RatingInput extends HTMLElement {
  // MANDATORY: opt in. Without this, attachInternals() form APIs are unavailable.
  static formAssociated = true;

  #internals;
  #value = '';

  constructor() {
    super();
    this.#internals = this.attachInternals();
    this.attachShadow({ mode: 'open', delegatesFocus: true });
  }

  connectedCallback() {
    // Validate initial state once attributes (such as `required`) are present.
    this.#validate();
  }

  get value() { return this.#value; }
  set value(v) {
    this.#value = v;
    // Push the value into the owning <form> for submission.
    this.#internals.setFormValue(v);
    this.#validate();
  }

  #validate() {
    if (this.hasAttribute('required') && !this.#value) {
      // Mark invalid and supply the native validation message + anchor.
      this.#internals.setValidity(
        { valueMissing: true },
        'Please choose a rating.',
        this.shadowRoot.querySelector('[tabindex]'),
      );
    } else {
      this.#internals.setValidity({}); // clear
    }
  }

  // Optional FACE lifecycle hooks:
  formResetCallback() { this.value = ''; }
  formDisabledCallback(disabled) {
    // Disable internal controls so they leave the tab order and ignore clicks.
    // Do NOT toggle `disabled` on `this`, or a `<fieldset disabled>` toggle will
    // leave the host permanently stuck with its own `disabled` attribute.
    for (const el of this.shadowRoot.querySelectorAll('button')) {
      el.disabled = disabled;
    }
  }
  formStateRestoreCallback(state) { this.value = state; }
}
customElements.define('rating-input', RatingInput);

Points essentiels :

  • static formAssociated = true est OBLIGATOIRE. Il débloque les méthodes ElementInternals liées au formulaire et le cycle de vie formXxxCallback.
  • setFormValue(value) est l'élément qui envoie le formulaire, sans qu'une entrée masquée soit nécessaire. Transmettez un FormData pour les commandes à valeurs multiples.
  • setValidity(flags, message, anchor) s'intègre à la validation des contraintes natives : le formulaire ne sera pas envoyé s'il n'est pas valide, :invalid/:valid s'appliquent et l'élément d'ancrage reçoit le focus sur le rapport. Appelez setValidity({}) pour effacer. Les pseudo-classes :user-valid/:user-invalid sensibles aux interactions ne sont pas systématiquement associées aux éléments associés à un formulaire dans tous les navigateurs. Vérifiez la compatibilité avant de vous y fier, plutôt que de supposer qu'elles suivent l'interaction de l'utilisateur comme elles le font pour les commandes natives.
  • Implémentez formResetCallback, formDisabledCallback et formStateRestoreCallback pour que l'élément se comporte comme un contrôle natif lors de la réinitialisation, de fieldset[disabled] et de la restauration de la saisie automatique précédente/suivante. Dans formDisabledCallback(disabled), désactivez les commandes internes (ou supprimez-les de l'ordre de tabulation et ignorez l'entrée) plutôt que de définir disabled sur l'hôte. Définir l'attribut de l'hôte dans le rappel empêche la réactivation lorsqu'un ancêtre <fieldset> est réactivé, et l'hôte correspond déjà automatiquement à :disabled.
  • Suivez les conventions de la plate-forme pour les événements distribués et privilégiez les événements input et change intégrés aux événements personnalisés. input se déclenche lorsque la valeur change à la suite d'une action de l'utilisateur (saisie, déplacement d'un curseur). change se déclenche une fois par flux de modifications, lorsque l'utilisateur valide (relâchement du curseur, floutage du champ). Créez-les respectivement avec InputEvent et Event.

Stratégies de remplacement

Détectez les fonctionnalités avec la compatibilité static formAssociated avant de vous y fier : 'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype. Lorsqu'il n'est pas disponible, utilisez un <input> natif masqué visuellement et synchronisé avec la valeur de la commande pour que le formulaire soit toujours envoyé. C'est le seul cas où le modèle d'entrée masquée est justifié.