Com um novo evento e APIs de elementos personalizados, participar de formulários ficou muito mais fácil.
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:
- Adicione um listener de eventos
formdata
ao formulário com que você 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.
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
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, comosetFormValue()
esetValidity()
. - Adição de métodos e propriedades comuns aceitos por controles de formulário, como
name
,value
evalidity
.
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 objetoFormData
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.