Elementi personalizzati associati ai moduli

Non utilizzare un elemento <input> nascosto per mostrare il valore di un controllo personalizzato in un modulo. Utilizza gli elementi personalizzati associati ai moduli (FACE): con formAssociated e ElementInternals, l'elemento invia il proprio valore, partecipa alla convalida e riflette lo stato del modulo in modo nativo.

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

Punti chiave:

  • static formAssociated = true è OBBLIGATORIO; sblocca i metodi ElementInternals relativi ai moduli e il ciclo di vita formXxxCallback.
  • setFormValue(value) è ciò che invia, senza bisogno di input nascosti. Passa un FormData per i controlli con più valori.
  • setValidity(flags, message, anchor) si integra con la convalida dei vincoli nativi: il modulo non verrà inviato se non è valido, si applicano :invalid/:valid e l'elemento di ancoraggio riceve lo stato attivo nel report. Chiama il numero setValidity({}) per cancellare. Le pseudo-classi :user-valid/:user-invalid sensibili all'interazione non sono collegate in modo coerente agli elementi associati ai moduli nei vari browser. Verifica il supporto prima di fare affidamento su di esse, anziché presumere che monitorino l'interazione dell'utente come fanno per i controlli nativi.
  • Implementa formResetCallback, formDisabledCallback e formStateRestoreCallback in modo che l'elemento si comporti come un controllo nativo in caso di reset, fieldset[disabled] e ripristino della compilazione automatica indietro/avanti. In formDisabledCallback(disabled), disattiva i controlli interni (o rimuovili dall'ordine di tabulazione e ignora l'input) anziché impostare disabled sull'host. L'impostazione dell'attributo host all'interno del callback impedisce la riattivazione quando un elemento <fieldset> predecessore viene riattivato e l'host corrisponde già automaticamente a :disabled.
  • Segui le convenzioni della piattaforma per gli eventi inviati e preferisci gli eventi integrati input e change a quelli personalizzati. input si attiva quando il valore cambia come risultato diretto dell'azione dell'utente (digitazione, trascinamento di un cursore); change si attiva una volta per flusso di modifiche, quando l'utente esegue il commit (rilascio del cursore, sfocatura del campo). Costruiscili rispettivamente con InputEvent e Event.

Strategie di riserva

Rileva le funzionalità con il supporto di static formAssociated prima di fare affidamento su di esse: 'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype. Se non è disponibile, utilizza un elemento <input> nativo nascosto visivamente e sincronizzato con il valore del controllo, in modo che il modulo venga comunque inviato. Questo è l'unico caso in cui il pattern di input nascosto è giustificato.