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 oferecer 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 de formulário 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 estilizar a entrada usando as pseudoclasses :valid e :invalid.
  • A entrada é notificada quando o formulário é redefinido, recarregado ou quando o navegador tenta preencher automaticamente as entradas do formulário.

Os controles de formulário personalizados geralmente têm poucos 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 atuais:

  • O evento formdata permite que um objeto JavaScript arbitrário participe do envio de formulários. Assim, você pode adicionar dados de formulários sem usar um <input> oculto.
  • A API de elementos personalizados associados a formulários permite que os elementos personalizados se comportem mais como controles de formulário 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 baixo nível que permite que qualquer código JavaScript participe do envio de formulários. 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 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.

Confira um exemplo de envio de um único valor em um listener de evento 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 mais recente para conferir a API em ação.

Compatibilidade com navegadores

Compatibilidade com navegadores

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

Origem

Elementos personalizados associados a formulários

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

Os controles de formulário padronizados participam de muitas partes do ciclo de vida do formulário, além do envio. 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 será 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 do formulário tiver uma entrada inválida, ele 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 oferece suporte às pseudoclasses CSS padrão para controles de formulário, 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.

Como definir um elemento personalizado associado a um formulário

Para transformar um elemento personalizado em um elemento associado a um formulário, é necessário seguir algumas etapas extras:

  • Adicione uma propriedade formAssociated estática à classe de elemento personalizada. 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 métodos e propriedades extras para controles de formulário, como setFormValue() e setValidity().
  • Adição de métodos e propriedades comuns aceitos por controles de formulário, como name, value e validity.

Confira 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 transmitir um número de cartão, uma data de validade e um 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

O controle também pode participar da validação de formulário 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 formulários com as pseudoclasses :valid e :invalid, 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 se 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)

É chamada 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. Por exemplo, o elemento pode desativar elementos no shadow DOM quando ele está 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)

Chamada 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.

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 ao estado em que o usuário 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 parâmetro de valor único, conforme 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 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. 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 precisa restaurar o estado com base no valor 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 mais recente para conferir 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, é possível adicionar 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 polifill.

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 ao formulário oferecem novas ferramentas para criar controles de formulário 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ários personalizados que funcionam como controles de formulários integrados.

Imagem principal de Oudom Pravat no Unsplash.