Controles de formulário mais eficientes

Com um novo evento e APIs de elementos personalizados, a participação em formulários ficou muito mais fácil.

Arthur Evans

Muitos desenvolvedores criam controles de formulário personalizados, seja para fornecer controles que não são integrados ao navegador ou para personalizar a aparência além do que é possível com os controles de formulário integrados.

No entanto, pode ser difícil replicar os recursos dos controles incorporados de formulário HTML. Considere alguns dos recursos que um elemento <input> recebe automaticamente quando você o adiciona a um formulário:

  • A entrada é adicionada automaticamente à lista de controles do formulário.
  • O valor da entrada é enviado automaticamente com o formulário.
  • A entrada participa da validação do formulário. É possível estilizar a entrada usando as pseudoclasses :valid e :invalid.
  • A entrada é notificada quando o formulário é redefinido, quando o formulário é recarregado ou quando o navegador tenta preencher automaticamente as entradas do formulário.

Os controles de formulário personalizados geralmente têm alguns desses recursos. Os desenvolvedores podem contornar algumas das limitações do JavaScript, como adicionar um <input> oculto a um formulário para participar do envio. No entanto, outros recursos não podem ser replicados sozinho em JavaScript.

Dois novos recursos da Web facilitam a criação de controles de formulário personalizados e removem as limitações dos controles personalizados atuais:

  • O evento formdata permite que um objeto JavaScript arbitrário participe do envio do formulário. Assim, você pode adicionar dados do formulário sem usar um <input> oculto.
  • A API de elementos personalizados associados a formulários permite que esses elementos funcionem como controles de formulários integrados.

Esses dois recursos podem ser usados para criar novos tipos de controles que funcionam melhor.

API baseada em eventos

O evento formdata é uma API de nível inferior que permite que qualquer código JavaScript participe do envio do formulário. O mecanismo funciona da seguinte forma:

  1. Adicione um listener de eventos formdata ao formulário com que quer interagir.
  2. Quando um usuário clica no botão "Enviar", o formulário dispara um evento formdata, que inclui um objeto FormData com todos os dados que estão sendo enviados.
  3. Cada listener formdata tem a chance de adicionar ou modificar os dados antes do envio do formulário.

Veja um exemplo de envio de um único valor em um listener de eventos formdata:

const form = document.querySelector('form');
// FormData event is sent on <form> submission, before transmission.
// The event has a formData property
form.addEventListener('formdata', ({formData}) => {
  // https://developer.mozilla.org/docs/Web/API/FormData
  formData.append('my-input', myInputValue);
});

Teste isso usando nosso exemplo no Glitch. Execute-a no Chrome 77 ou posterior para ver a API em ação.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • Chrome: 5.
  • Borda: 12.
  • Firefox: 4.
  • Safari: 5.

Origem

Elementos personalizados associados ao formulário

Você pode usar a API baseada em eventos com qualquer tipo de componente, mas ela só permite interagir com o processo de envio.

Além do envio, os controles padronizados de formulários fazem parte do ciclo de vida deles. O objetivo dos elementos personalizados associados a formulários é preencher a lacuna entre widgets personalizados e controles integrados. Os elementos personalizados associados ao formulário correspondem a muitos dos recursos dos elementos de formulário padronizados:

  • Quando você coloca um elemento personalizado associado a um formulário em um <form>, ele é automaticamente associado ao formulário, como um controle fornecido pelo navegador.
  • O elemento pode ser rotulado usando um elemento <label>.
  • O elemento pode definir um valor que é enviado automaticamente com o formulário.
  • O elemento pode definir uma sinalização que indica se ele tem ou não uma entrada válida. Se um dos controles de formulário tiver entradas inválidas, o formulário não poderá ser enviado.
  • O elemento pode fornecer callbacks para várias partes do ciclo de vida do formulário, como quando o formulário é desativado ou redefinido para o estado padrão.
  • O elemento é compatível com as pseudoclasses CSS padrão para controles de formulários, como :disabled e :invalid.

São muitos recursos! Este artigo não abordará todos eles, mas descreverá os fundamentos necessários para integrar seu elemento personalizado a um formulário.

Definição de um elemento personalizado associado a um formulário

Para transformar um elemento personalizado em um elemento personalizado associado a formulários, são necessárias algumas etapas extras:

  • Adicione uma propriedade formAssociated estática à classe de elemento personalizado. Isso informa ao navegador para tratar o elemento como um controle de formulário.
  • Chame o método attachInternals() no elemento para acessar outros métodos e propriedades para controles de formulário, como setFormValue() e setValidity().
  • Adicione as propriedades e os métodos comuns compatíveis com os controles de formulário, como name, value e validity.

Veja como esses itens se encaixam em uma definição básica de elemento personalizado:

// Form-associated custom elements must be autonomous custom elements--
// meaning they must extend HTMLElement, not one of its subclasses.
class MyCounter extends HTMLElement {

  // Identify the element as a form-associated custom element
  static formAssociated = true;

  constructor() {
    super();
    // Get access to the internal form control APIs
    this.internals_ = this.attachInternals();
    // internal value for this control
    this.value_ = 0;
  }

  // Form controls usually expose a "value" property
  get value() { return this.value_; }
  set value(v) { this.value_ = v; }

  // The following properties and methods aren't strictly required,
  // but browser-level form controls provide them. Providing them helps
  // ensure consistency with browser-provided controls.
  get form() { return this.internals_.form; }
  get name() { return this.getAttribute('name'); }
  get type() { return this.localName; }
  get validity() {return this.internals_.validity; }
  get validationMessage() {return this.internals_.validationMessage; }
  get willValidate() {return this.internals_.willValidate; }

  checkValidity() { return this.internals_.checkValidity(); }
  reportValidity() {return this.internals_.reportValidity(); }

  
}
customElements.define('my-counter', MyCounter);

Após o registro, será possível usar esse elemento sempre que você usar um controle de formulário fornecido pelo navegador:

<form>
  <label>Number of bunnies: <my-counter></my-counter></label>
  <button type="submit">Submit</button>
</form>

Como definir um valor

O método attachInternals() retorna um objeto ElementInternals que fornece acesso a APIs de controle de formulários. O mais básico deles é o método setFormValue(), que define o valor atual do controle.

O método setFormValue() pode usar um destes três tipos de valores:

  • Um valor de string.
  • Um objeto File.
  • Um objeto FormData. É possível usar um objeto FormData para transmitir vários valores. Por exemplo, um controle de entrada de cartão de crédito pode passar o número do cartão, a data de validade e o código de verificação.

Para definir um valor simples:

this.internals_.setFormValue(this.value_);

Para definir diversos valores, você pode fazer algo assim:

// Use the control's name as the base name for submitted data
const n = this.getAttribute('name');
const entries = new FormData();
entries.append(n + '-first-name', this.firstName_);
entries.append(n + '-last-name', this.lastName_);
this.internals_.setFormValue(entries);

Validação de entrada

Seu controle também pode participar da validação do formulário chamando o método setValidity() no objeto internals.

// Assume this is called whenever the internal value is updated
onUpdateValue() {
  if (!this.matches(':disabled') && this.hasAttribute('required') &&
      this.value_ < 0) {
    this.internals_.setValidity({customError: true}, 'Value cannot be negative.');
  }
  else {
    this.internals_.setValidity({});
  }
  this.internals.setFormValue(this.value_);
}

É possível estilizar um elemento personalizado associado a um formulário com :valid e :invalid. pseudoclasses, assim como um controle de formulário integrado.

Callbacks do ciclo de vida

Uma API de elemento personalizado associada a um formulário inclui um conjunto de callbacks de ciclo de vida extras para vincular ao ciclo de vida do formulário. Os callbacks são opcionais. Eles só podem ser implementados se o elemento precisar fazer algo nesse ponto do ciclo de vida.

void formAssociatedCallback(form)

Chamado quando o navegador associa o elemento a um elemento de formulário ou desassocia o elemento de um elemento de formulário.

void formDisabledCallback(disabled)

Chamado depois que o estado disabled do elemento muda, seja porque o atributo disabled desse elemento foi adicionado ou removido. ou porque o estado disabled mudou em um <fieldset> que é um ancestral desse elemento. O parâmetro disabled representa o novo estado desativado do elemento. O elemento pode, por exemplo, desativar elementos em seu shadow DOM quando estiver desativado.

void formResetCallback()

Chamado após a redefinição do formulário. O elemento precisa redefinir-se para algum tipo de estado padrão. Para elementos <input>, isso geralmente envolve definir a propriedade value para corresponder ao atributo value definido na marcação ou, no caso de uma caixa de seleção, definir a propriedade checked para corresponder ao atributo checked.

void formStateRestoreCallback(state, mode)

Chamado em uma destas duas circunstâncias:

  • Quando o navegador restaura o estado do elemento (por exemplo, após uma navegação ou quando o navegador é reiniciado). Nesse caso, o argumento mode é "restore".
  • Quando os recursos de auxílio de entrada do navegador, como o preenchimento automático de formulários, definem um valor. Nesse caso, o argumento mode é "autocomplete".

O tipo do primeiro argumento depende de como o método setFormValue() foi chamado. Para mais detalhes, consulte Como restaurar o estado do formulário.

Restaurando o estado do formulário

Em algumas circunstâncias, como ao voltar para uma página ou reiniciar o navegador, o navegador pode tentar restaurar o formulário para o estado em que o usuário o deixou.

No caso de um elemento personalizado associado a um formulário, o estado restaurado vem dos valores transmitidos ao método setFormValue(). É possível chamar o método com um único parâmetro de valor, como mostrado nos exemplos anteriores, ou com dois parâmetros:

this.internals_.setFormValue(value, state);

O value representa o valor que pode ser enviado do controle. O parâmetro opcional state é uma representação interna do estado do controle, que pode incluir dados que não são enviados ao servidor. O parâmetro state usa os mesmos tipos do parâmetro value. Ele pode ser uma string, um objeto File ou FormData.

O parâmetro state é útil quando não é possível restaurar o estado de um controle com base apenas no valor. Por exemplo, suponha que você crie um seletor de cores com vários modos: uma paleta ou uma roda de cores RGB. O valor da tabela de envio seria a cor selecionada em um formato canônico, como "#7fff00". No entanto, para restaurar o controle para um estado específico, você também precisa saber em qual modo ele estava. Portanto, o state pode ser "palette/#7fff00".

this.internals_.setFormValue(this.value_,
    this.mode_ + '/' + this.value_);

O código precisaria restaurar o estado com base no valor de estado armazenado.

formStateRestoreCallback(state, mode) {
  if (mode == 'restore') {
    // expects a state parameter in the form 'controlMode/value'
    [controlMode, value] = state.split('/');
    this.mode_ = controlMode;
    this.value_ = value;
  }
  // Chrome currently doesn't handle autofill for form-associated
  // custom elements. In the autofill case, you might need to handle
  // a raw value.
}

No caso de um controle mais simples (por exemplo, uma entrada numérica), o valor provavelmente é suficiente para restaurar o controle ao estado anterior. Se você omitir state ao chamar setFormValue(), o valor será transmitido para formStateRestoreCallback().

formStateRestoreCallback(state, mode) {
  // Simple case, restore the saved value
  this.value_ = state;
}

Um exemplo funcional

O exemplo a seguir reúne muitos dos recursos de elementos personalizados associados a formulários. Execute-a no Chrome 77 ou posterior para ver a API em ação.

Detecção de recursos

É possível usar a detecção de recursos para determinar se o evento formdata e os elementos personalizados associados a formulários estão disponíveis. No momento, não há polyfills liberados para nenhum dos recursos. Em ambos os casos, você pode retornar à adição de um elemento de formulário oculto para propagar o valor do controle para o formulário. Muitos dos recursos mais avançados de elementos personalizados associados a formulários provavelmente serão difíceis ou impossíveis de aplicar o polyfill.

if ('FormDataEvent' in window) {
  // formdata event is supported
}

if ('ElementInternals' in window &&
    'setFormValue' in window.ElementInternals.prototype) {
  // Form-associated custom elements are supported
}

Conclusão

O evento formdata e os elementos personalizados associados a formulários oferecem novas ferramentas para criar controles de formulários personalizados.

O evento formdata não oferece novos recursos, mas oferece uma interface para adicionar os dados do formulário ao processo de envio, sem precisar criar um elemento <input> oculto.

A API de elementos personalizados associados a formulários oferece um novo conjunto de recursos para criar controles de formulário personalizados que funcionam como controles de formulários integrados.

Imagem principal de Oudom Pravat no Unsplash.