Publicado em: 8 de agosto de 2019
Muitos desenvolvedores criam controles de formulário personalizados, seja para fornecer controles que não estão integrados ao navegador, seja 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 as funcionalidades 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:
- O campo de entrada é adicionado 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 . Você pode estilizar a entrada usando as pseudoclasses
:valide:invalid. - A entrada é notificada quando o formulário é redefinido ou recarregado ou quando o navegador tenta preencher automaticamente as entradas do formulário.
Os controles de formulário personalizados normalmente possuem 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 do formulário. Mas outras funcionalidades simplesmente não podem ser replicadas apenas com JavaScript.
Dois recursos da Web facilitam a criação de controles de formulário personalizados e removem as limitações dos controles personalizados:
- O evento
formdatapermite que um objeto JavaScript arbitrário participe do envio de formulários. Assim, você pode adicionar dados de formulário sem usar um<input>oculto. - A API de elementos personalizados associados a formulários permite que elementos personalizados se comportem de maneira mais semelhante aos 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.
- Adicione um listener de eventos
formdataao formulário com que você quer interagir. - Quando um usuário clica em enviar, o formulário dispara um evento.
formdataevento, que inclui umFormDataObjeto que contém todos os dados que estão sendo enviados. - Cada listener
formdatatem a chance de adicionar ou modificar os dados antes do envio do formulário.
Aqui está um exemplo de envio de um único valor em um ouvinte 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);
});
Elementos personalizados associados a formulários
Você pode usar a API baseada em eventos com qualquer tipo de componente, mas ela permite interagir apenas com o processo de submissão.
Os controles de formulário padronizados participam de muitas partes do ciclo de vida do formulário. Os elementos personalizados associados ao formulário visam preencher a lacuna entre widgets personalizados e controles integrados. Os elementos personalizados associados ao formulário correspondem a muitas das características dos elementos de formulário padronizados:
- Quando você coloca um elemento personalizado associado a um formulário dentro de 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 flag indicando se ele tem ou não uma entrada válida. Se um dos campos do formulário contiver dados inválidos, o formulário não poderá ser enviado.
- O elemento pode fornecer funções de retorno de chamada para várias partes do ciclo de vida do formulário, como quando o formulário é desativado ou redefinido para seu estado padrão.
- O elemento suporta as pseudoclasses CSS padrão para controles de formulário, como
:disablede:invalid.
Este documento não aborda tudo, mas descreve os conceitos básicos necessários 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, são necessárias algumas etapas extras:
- Adicione uma propriedade estática
formAssociatedà sua classe de elemento personalizado. Isso diz 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 de controles de formulário, comosetFormValue()esetValidity(). - Adicione as propriedades e os métodos comuns compatíveis com controles de formulário, como
name,valueevalidity.
Veja como esses itens se encaixam em uma definição básica de elemento personalizado:
// Form-associated custom elements must be autonomous custom elements.
// 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, você poderá usar esse elemento em qualquer lugar onde 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>
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 deles é o método
setFormValue(), que define o valor atual do controle.
O método setFormValue() pode receber um de três tipos de valores:
- Um valor de texto.
- UM
Fileobjeto. - Um objeto
FormData. Você pode usar um objetoFormDatapara passar múltiplos valores. Por exemplo, um controle de entrada de cartão de crédito pode receber o número do cartão, a data de validade e o código de verificação.
Para definir um valor:
this.internals_.setFormValue(this.value_);
Para definir vários 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 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_);
}
Você pode 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 elementos personalizados associada a formulários inclui um conjunto de funções de retorno de chamada adicionais para o ciclo de vida do formulário. Os callbacks são opcionais. Implemente um callback apenas se o elemento precisar fazer algo naquele 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 o estado disabled do elemento mudar, seja porque o atributo disabled deste elemento foi adicionado ou removido; ou porque o estado disabled mudou em um <fieldset> que é um ancestral deste elemento.
Por exemplo, o elemento pode desativar elementos no shadow DOM quando ele é desativado.
void formResetCallback()
Chamado após o formulário ser redefinido. O elemento deve se redefinir 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. Com uma caixa de seleção, isso está relacionado a definir a propriedade checked para corresponder ao atributo checked.
void formStateRestoreCallback(state, mode)
Chamado em uma das duas circunstâncias:
- Quando o navegador restaura o estado do elemento, como após a navegação ou quando o navegador é reiniciado. 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. O argumento
modeé"autocomplete".
O tipo do primeiro argumento depende de como o método setFormValue() foi
chamado.
Restaurar o estado do formulário
Em algumas circunstâncias, como ao voltar para uma página ou reiniciar o navegador, ele 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 do(s) valor(es) que você passa para o método setFormValue(). Você pode chamar o método com um único parâmetro de valor, como mostrado na figura.exemplos anteriores ou com dois parâmetros:
this.internals_.setFormValue(value, state);
O value representa o valor submetível 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 aceita os mesmos tipos que o parâmetro value: uma string, um objeto File ou um objeto FormData.
O parâmetro state é útil quando não é possível restaurar o estado de um controle com base apenas no valor. Por exemplo, imagine que você crie um seletor de cores com vários modos: uma paleta ou um círculo cromático RGB. O valor { da tabela de submissão é a cor selecionada em um formato canônico, como "#7fff00". Para restaurar o controle a um estado específico, você também precisaria saber em qual modo ele estava, então o state poderia parecer "palette/#7fff00".
this.internals_.setFormValue(this.value_,
this.mode_ + '/' + this.value_);
Seu código precisaria 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 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 de número), 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;
}
Detecção de características
Você pode usar a detecção de recursos para determinar se o evento formdata e os elementos personalizados associados ao formulário estão disponíveis. Não foram lançados polyfills para nenhuma das funcionalidades. Em ambos os casos, você pode recorrer à adição de um elemento de formulário oculto para propagar o valor do controle para o formulário.
É provável que muitos dos recursos mais avançados de elementos personalizados associados a formulários sejam difíceis ou impossíveis de usar com polyfill.
if ('FormDataEvent' in window) {
// formdata event is supported
}
if ('ElementInternals' in window &&
'setFormValue' in window.ElementInternals.prototype) {
// Form-associated custom elements are supported
}
O evento formdata oferece uma interface para adicionar os dados do formulário ao processo de envio
sem precisar criar um elemento <input> oculto. Com a
API de elementos personalizados associados a formulários, é possível oferecer um novo conjunto de recursos
para controles de formulário personalizados que funcionam como controles de formulário integrados.