Com um novo evento e APIs de elementos personalizados, a participação em formulários ficou muito mais fácil.
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:
- Adicione um listener de eventos
formdata
ao formulário com que quer interagir. - Quando um usuário clica no botão "Enviar", o formulário dispara um evento
formdata
, que inclui um objetoFormData
com todos os dados que estão sendo enviados. - 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
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, comosetFormValue()
esetValidity()
. - Adicione as propriedades e os métodos comuns compatíveis com os controles de formulário, como
name
,value
evalidity
.
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 objetoFormData
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.