Controles de formulário mais eficientes

Com um novo evento e APIs de elementos personalizados, participar de formulários ficou muito mais fácil.

Arthur Evans

Muitos desenvolvedores criam controles de formulário personalizados para fornecer controles que não estã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 de formulários HTML integrados. 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 definir o estilo da 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 normalmente têm poucos desses recursos. Os desenvolvedores podem contornar algumas das limitações em JavaScript, como adicionar um <input> oculto a um formulário para participar do envio. Mas outros recursos não podem ser replicados apenas 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 de formulário. Assim, você pode adicionar dados de 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 assim:

  1. Adicione um listener de eventos formdata ao formulário com que você quer interagir.
  2. Quando um usuário clica no botão "Enviar", o formulário dispara um evento formdata, que inclui um objeto FormData que contém 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.

Confira 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-o no Chrome 77 ou posterior para ver a API em ação.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • 5
  • 12
  • 4
  • 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 que você interaja com o processo de envio.

Além do envio, os controles padronizados de formulários participam de muitas partes do ciclo de vida do formulário. O objetivo dos elementos personalizados associados a formulários é preencher a lacuna entre widgets personalizados e controles integrados. Os elementos personalizados associados a formulários 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 um flag indicando se ele tem ou não uma entrada válida. Se um dos controles do 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, por exemplo, quando ele é desativado ou redefinido para o estado padrão.
  • O elemento é compatível com as pseudoclasses padrão CSS para controles de formulário, como :disabled e :invalid.

São muitos recursos! Este artigo não abordará todos eles, mas descreverá as noções básicas necessárias para integrar seu elemento personalizado a um formulário.

Definir um elemento personalizado associado a um formulário

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

  • Adicione uma propriedade formAssociated estática à classe do elemento personalizado. Isso informa ao navegador para tratar o elemento como um controle de formulário.
  • Chame o método attachInternals() no elemento para ter acesso a 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ários, 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);

Depois de registrado, você pode usar esse elemento sempre que usaria 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 às APIs de controle de formulário. O mais básico é 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 um número de cartão, uma data de validade e um código de verificação.

Para definir um valor simples, faça o seguinte:

this.internals_.setFormValue(this.value_);

Para definir diversos valores, faça 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 de formulários chamando o método setValidity() no objeto interno.

// 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 as pseudoclasses :valid e :invalid, assim como um controle de formulário integrado.

Callbacks do ciclo de vida

Uma API de elemento personalizado associado a um formulário inclui um conjunto de callbacks do ciclo de vida extras para vincular ao ciclo de vida do formulário. Os callbacks são opcionais: só implemente 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 após a mudança do estado disabled do elemento, seja porque o atributo disabled desse elemento foi adicionado ou removido ou porque o estado disabled mudou em um <fieldset> que é ancestral desse elemento. O parâmetro disabled representa o novo estado desativado do elemento. O elemento poderá, por exemplo, desativar elementos no shadow DOM quando estiver desativado.

void formResetCallback()

Chamado depois que o formulário é redefinido. O elemento precisa ser redefinido para algum tipo de estado padrão. Para elementos <input>, isso geralmente envolve a configuração da propriedade value para corresponder ao atributo value definido na marcação. No caso de uma caixa de seleção, definir a propriedade checked para corresponder ao atributo checked.

void formStateRestoreCallback(state, mode)

É chamada em uma das 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 assistência 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.

Como restaurar o estado do formulário

Em algumas circunstâncias, como ao navegar de volta 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.

Para 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 de envio do controle. O parâmetro state opcional é 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 que o parâmetro value: pode ser uma string, um File ou um objeto FormData.

O parâmetro state é útil quando não é possível restaurar o estado de um controle apenas com base 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 enviável seria a cor selecionada em uma forma canônica, como "#7fff00". No entanto, para restaurar o controle a um estado específico, você também precisa saber em qual modo ele estava. Portanto, o state pode ser semelhante a "palette/#7fff00".

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

Seu 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-o no Chrome 77 ou posterior para ver a API em ação.

Detecção de recursos

Use a detecção de recursos para determinar se o evento formdata e os elementos personalizados associados ao formulário estão disponíveis. No momento, não há polyfills liberados para nenhum dos recursos. Em ambos os casos, é possível voltar a adicionar um elemento de formulário oculto para propagar o valor do controle para o formulário. Muitos dos recursos mais avançados dos elementos personalizados associados a formulários provavelmente serão difíceis ou impossíveis de usar no 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 a criação de controles de formulário personalizados.

O evento formdata não oferece novos recursos, mas fornece 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 ao formulário fornece um novo conjunto de recursos para criar controles de formulário personalizados que funcionam como controles de formulário integrados.

Imagem principal de Oudom Pravat no Unsplash (link em inglês).