Eventos de formulário e foco
TL;DR
Formulários têm três camadas de eventos: entrada do usuário (
input/change), foco (focus/blur/focusin/focusout), e submissão (submit/reset). A distinção crítica:inputdispara a cada caractere digitado;changedispara ao perder o foco com valor diferente (ou ao selecionar em checkboxes/selects).submité o lugar certo para validar e enviar — nãoclickno botão. A Constraint Validation API permite validação nativa sem JS.
Eventos de entrada — input vs change
const input = document.querySelector('input[type="text"]');
// input: dispara a cada mudança de valor (cada tecla, cada colagem)
input.addEventListener('input', (event) => {
console.log('Valor atual:', event.target.value);
// Ideal para: busca em tempo real, contagem de caracteres, validação on-the-fly
});
// change: dispara quando o valor muda E o elemento perde o foco
input.addEventListener('change', (event) => {
console.log('Valor final:', event.target.value);
// Ideal para: validação ao sair do campo, persistência
});Comportamento por tipo de input
| Elemento | input | change |
|---|---|---|
<input type="text"> | A cada caractere | Ao perder foco com valor diferente |
<input type="checkbox"> | Ao clicar | Ao clicar (sincrônico) |
<input type="radio"> | Ao selecionar | Ao selecionar (sincrônico) |
<input type="range"> | Ao arrastar | Ao soltar |
<select> | Ao selecionar opção | Ao selecionar opção (sincrônico) |
<input type="file"> | Ao selecionar arquivo | Ao selecionar arquivo |
// Checkbox — change é o evento correto
checkbox.addEventListener('change', (event) => {
console.log('Marcado:', event.target.checked);
});
// Select — change é o evento correto
select.addEventListener('change', (event) => {
console.log('Selecionado:', event.target.value);
});
// Range — use input para feedback em tempo real
range.addEventListener('input', (event) => {
output.textContent = event.target.value;
});beforeinput — interceptar antes da mudança
// beforeinput: dispara ANTES de `input`, permite cancelar a entrada
input.addEventListener('beforeinput', (event) => {
// Só permite dígitos
if (event.inputType === 'insertText' && !/^\d$/.test(event.data)) {
event.preventDefault(); // cancela a entrada
}
// event.inputType: "insertText", "deleteContentBackward", "insertFromPaste", etc.
// event.data: o texto que seria inserido
});Eventos de foco
const input = document.querySelector('input');
// focus / blur — NÃO borbulham
input.addEventListener('focus', () => {
console.log('Focado');
// Não propaga — handlers em ancestrais não recebem
});
input.addEventListener('blur', () => {
console.log('Desfocado');
});
// focusin / focusout — BORBULHAM
form.addEventListener('focusin', (event) => {
console.log('Algum filho ganhou foco:', event.target);
});
form.addEventListener('focusout', (event) => {
console.log('Algum filho perdeu foco:', event.target);
});Rastrear se o foco está dentro de um container
const container = document.querySelector('.search-container');
container.addEventListener('focusin', () => {
container.classList.add('focused');
});
container.addEventListener('focusout', (event) => {
// focusout dispara antes de focusin do próximo elemento
// Checar se o foco foi para dentro do mesmo container
if (!container.contains(event.relatedTarget)) {
container.classList.remove('focused');
}
});event.relatedTarget: o elemento que recebe o foco (em blur/focusout) ou o elemento que perde o foco (em focus/focusin).
Foco programático
// Focar um elemento
input.focus();
input.focus({ preventScroll: true }); // sem scroll automático
// Desfoca o elemento que tem foco atualmente
document.activeElement.blur();
// Verificar qual elemento tem foco
document.activeElement; // o elemento com foco atual
document.hasFocus(); // se o documento tem foco (em foreground)
// Tornar qualquer elemento focável
el.setAttribute('tabindex', '0'); // focável pelo Tab, sem alterar a ordem natural
el.setAttribute('tabindex', '-1'); // focável via .focus(), mas não pelo Tab
el.setAttribute('tabindex', '1'); // ❌ evite: altera a ordem natural do TabSubmissão de formulário — submit
const form = document.querySelector('form');
// ✅ Sempre ouvir 'submit' no form — não 'click' no botão
// submit dispara ao clicar em <button type="submit">, pressionar Enter em input, etc.
form.addEventListener('submit', (event) => {
event.preventDefault(); // impede a submissão nativa (reload da página)
// Coletar dados
const data = new FormData(form);
const payload = Object.fromEntries(data.entries());
// Ou acessar individualmente:
data.get('email');
data.getAll('skills'); // para múltiplos values (checkboxes, multi-select)
// Enviar
fetch('/api/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
});FormData — coletar dados do formulário
const form = document.querySelector('form');
const data = new FormData(form);
// Ler
data.get('name'); // string (primeiro valor)
data.getAll('hobbies'); // array de strings
data.has('name'); // boolean
// Modificar antes de enviar
data.set('name', 'João');
data.append('tag', 'novo');
data.delete('campo_interno');
// Iterar
for (const [key, value] of data) {
console.log(key, value);
}
// Enviar arquivos: FormData encode corretamente como multipart/form-data
const fileInput = document.querySelector('input[type="file"]');
const formData = new FormData();
formData.append('file', fileInput.files[0]);
fetch('/upload', { method: 'POST', body: formData });
// Não colocar Content-Type manualmente — o browser define com o boundary corretoreset
form.addEventListener('reset', (event) => {
// Dispara antes de os campos serem resetados
// event.preventDefault() cancela o reset
console.log('Form sendo resetado');
});
// Programático
form.reset(); // reseta todos os campos para os valores iniciais do HTMLConstraint Validation API — validação nativa
HTML5 tem validação nativa sem JavaScript:
<form>
<input type="email" required placeholder="email@exemplo.com">
<input type="text" minlength="3" maxlength="50" pattern="[A-Za-z]+">
<input type="number" min="0" max="100" step="5">
<input type="url" required>
</form>// Verificar validade programaticamente
const input = document.querySelector('input[type="email"]');
input.checkValidity(); // boolean — true se válido
input.validity.valueMissing; // required mas vazio
input.validity.typeMismatch; // tipo errado (email com @, etc.)
input.validity.patternMismatch; // não bate com pattern
input.validity.tooShort; // menor que minlength
input.validity.tooLong; // maior que maxlength
input.validity.rangeUnderflow; // menor que min
input.validity.rangeOverflow; // maior que max
// Validação customizada
input.setCustomValidity('Email já cadastrado'); // marca como inválido
input.setCustomValidity(''); // limpa (marca como válido)
// Validar o form inteiro
form.checkValidity(); // false se algum campo inválido
form.reportValidity(); // false + mostra tooltip de erro nativo// Pattern de validação customizada + native
form.addEventListener('submit', (event) => {
// Validar primeiro com a API nativa
if (!form.checkValidity()) {
form.reportValidity();
event.preventDefault();
return;
}
// Validações extras que HTML não cobre
const password = form.querySelector('[name="password"]');
const confirm = form.querySelector('[name="confirm-password"]');
if (password.value !== confirm.value) {
confirm.setCustomValidity('Senhas não coincidem');
confirm.reportValidity();
event.preventDefault();
return;
}
confirm.setCustomValidity(''); // limpar antes de submeter
event.preventDefault();
submitForm();
});Estilizar com pseudo-classes de validação
/* Estados de validação sem JavaScript */
input:valid { border-color: green; }
input:invalid { border-color: red; }
/* Só após interação do usuário */
input:user-valid { border-color: green; }
input:user-invalid { border-color: red; }
/* required + vazio */
input:placeholder-shown { /* campo com placeholder visível (provavelmente vazio) */ }Para fixar
- Qual a diferença entre
inputechangepara<input type="text">? E para<input type="checkbox">?- Por que
focusnão borbulha? Como você detectaria quando qualquer input dentro de um form ganhou foco?- Por que ouvir
submitno form é melhor que ouvirclickno botão de submit?- O que
new FormData(form)faz? Como você enviaria um arquivo usando FormData?- O que
input.setCustomValidity('Mensagem')faz? Como você limparia o erro depois?
Veja também
- 02 — Teclado e ponteiro — anterior
- 04 — Event delegation — próxima
- HTML — formulários semânticos e atributos de validação