Elementos personalizados associados a formulários

Não use um <input> oculto para mostrar o valor de um controle personalizado em um formulário. Use elementos personalizados associados a formulários (FACE, na sigla em inglês): com formAssociated e ElementInternals, o elemento envia o próprio valor, participa da validação e reflete o estado do formulário de forma nativa.

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

Pontos principais:

  • static formAssociated = true é OBRIGATÓRIO. Ele desbloqueia os métodos ElementInternals relacionados ao formulário e o ciclo de vida formXxxCallback.
  • setFormValue(value) é o que envia, sem necessidade de entrada oculta. Transmita um FormData para controles de vários valores.
  • O setValidity(flags, message, anchor) se integra à validação de restrição nativa: o formulário não é enviado enquanto for inválido, :invalid/:valid se aplicam e o elemento de âncora recebe foco no relatório. Chame setValidity({}) para limpar. As pseudoclasses :user-valid/:user-invalid que reconhecem interações não são conectadas de forma consistente para elementos associados a formulários em todos os navegadores. Verifique se há suporte antes de confiar nelas, em vez de presumir que elas rastreiam a interação do usuário da mesma forma que fazem para controles nativos.
  • Implemente formResetCallback, formDisabledCallback e formStateRestoreCallback para que o elemento se comporte como um controle nativo na redefinição, no fieldset[disabled] e na restauração do preenchimento automático para voltar/avançar. Em formDisabledCallback(disabled), desative os controles internos (ou remova-os da ordem de tabulação e ignore a entrada) em vez de definir disabled no host. Definir o atributo do host dentro do callback impede a reativação quando um <fieldset> ancestral é reativado, e o host já corresponde a :disabled automaticamente.
  • Siga as convenções da plataforma para eventos enviados e prefira os eventos input e change integrados aos personalizados. input é disparado quando o valor muda como resultado direto da ação do usuário (digitar, arrastar um controle deslizante); change é disparado uma vez por fluxo de mudanças, quando o usuário confirma (soltar o controle deslizante, desfocar o campo). Crie-os com InputEvent e Event, respectivamente.

Estratégias de fallback

Faça a detecção de recursos com suporte a static formAssociated antes de usar: 'attachInternals' in HTMLElement.prototype && 'setFormValue' in ElementInternals.prototype. Quando ele não estiver disponível, use um <input> nativo visualmente oculto sincronizado com o valor do controle para que o formulário ainda seja enviado. Esse é o único caso em que o padrão de entrada oculta é justificado.