Formulários e validação no frontend

1. Fundamentos de Formulários HTML e JavaScript

Todo formulário web começa com uma estrutura HTML básica. Elementos como <form>, <input>, <select> e <textarea> formam a espinha dorsal da coleta de dados do usuário.

// Estrutura básica de um formulário HTML
const formHTML = `
<form id="cadastro">
  <label for="nome">Nome:</label>
  <input type="text" id="nome" name="nome" />

  <label for="email">E-mail:</label>
  <input type="email" id="email" name="email" />

  <label for="pais">País:</label>
  <select id="pais" name="pais">
    <option value="">Selecione</option>
    <option value="br">Brasil</option>
    <option value="us">Estados Unidos</option>
  </select>

  <label for="bio">Biografia:</label>
  <textarea id="bio" name="bio"></textarea>

  <button type="submit">Enviar</button>
</form>
`;

Para capturar dados com JavaScript puro, utilizamos document.forms e as propriedades dos inputs:

// Capturando dados do formulário
const form = document.forms['cadastro'];

form.addEventListener('submit', function(event) {
  event.preventDefault(); // Evita recarregamento da página

  const dados = {
    nome: form.elements['nome'].value,
    email: form.elements['email'].value,
    pais: form.elements['pais'].value,
    bio: form.elements['bio'].value
  };

  console.log('Dados do formulário:', dados);
});

Os principais eventos de formulário são:
- submit: disparado ao enviar o formulário
- input: disparado a cada alteração no valor do campo
- change: disparado quando o valor é confirmado (perda de foco)
- focus/blur: quando o campo ganha ou perde foco

2. Validação Nativa do HTML5 vs. Validação Customizada

O HTML5 oferece atributos de validação nativos que são fáceis de implementar:

// Exemplo de formulário com validação HTML5
const formComValidacao = `
<form id="validacao-nativa">
  <input type="text" required minlength="3" maxlength="50" />
  <input type="email" required />
  <input type="password" required pattern=".{8,}" />
  <input type="number" min="18" max="120" />
  <button type="submit">Enviar</button>
</form>
`;

No entanto, a validação nativa tem limitações:
- Mensagens de erro padronizadas e difíceis de personalizar
- Estilo visual limitado
- Comportamento inconsistente entre navegadores

Para superar essas limitações, implementamos validação customizada:

// Validação customizada com JavaScript puro
function validarCampo(input) {
  const valor = input.value.trim();
  let mensagemErro = '';

  if (input.hasAttribute('required') && !valor) {
    mensagemErro = 'Este campo é obrigatório';
  } else if (input.type === 'email' && valor) {
    const regexEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!regexEmail.test(valor)) {
      mensagemErro = 'E-mail inválido';
    }
  }

  // Exibe ou remove mensagem de erro
  const erroEl = input.nextElementSibling;
  if (mensagemErro) {
    input.classList.add('invalido');
    erroEl.textContent = mensagemErro;
    erroEl.style.display = 'block';
  } else {
    input.classList.remove('invalido');
    erroEl.style.display = 'none';
  }

  return !mensagemErro;
}

3. Técnicas de Validação com JavaScript Puro

Vamos implementar validações específicas para campos comuns:

// Validação de e-mail
function validarEmail(email) {
  const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
  return regex.test(email);
}

// Validação de CPF (apenas dígitos verificadores)
function validarCPF(cpf) {
  cpf = cpf.replace(/\D/g, '');
  if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false;

  let soma = 0;
  for (let i = 0; i < 9; i++) soma += parseInt(cpf[i]) * (10 - i);
  let resto = (soma * 10) % 11;
  if (resto === 10) resto = 0;
  if (resto !== parseInt(cpf[9])) return false;

  soma = 0;
  for (let i = 0; i < 10; i++) soma += parseInt(cpf[i]) * (11 - i);
  resto = (soma * 10) % 11;
  if (resto === 10) resto = 0;
  return resto === parseInt(cpf[10]);
}

// Validação de senha forte
function validarSenha(senha) {
  const regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return regex.test(senha);
}

Para validação em tempo real, usamos os eventos input e blur:

// Validação em tempo real
const campoEmail = document.getElementById('email');
const mensagemErro = document.getElementById('erro-email');

campoEmail.addEventListener('input', function() {
  if (this.value && !validarEmail(this.value)) {
    mensagemErro.textContent = 'Formato de e-mail inválido';
    this.classList.add('invalido');
  } else {
    mensagemErro.textContent = '';
    this.classList.remove('invalido');
  }
});

campoEmail.addEventListener('blur', function() {
  if (!this.value) {
    mensagemErro.textContent = 'E-mail é obrigatório';
    this.classList.add('invalido');
  }
});

4. Validação no Frontend com React – Estado e Eventos

No React, gerenciamos formulários com estado usando useState:

import React, { useState } from 'react';

function FormularioContato() {
  const [formData, setFormData] = useState({
    nome: '',
    email: '',
    mensagem: ''
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Dados enviados:', formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="nome"
        value={formData.nome}
        onChange={handleChange}
        placeholder="Nome"
      />
      <input
        type="email"
        name="email"
        value={formData.email}
        onChange={handleChange}
        placeholder="E-mail"
      />
      <textarea
        name="mensagem"
        value={formData.mensagem}
        onChange={handleChange}
        placeholder="Mensagem"
      />
      <button type="submit">Enviar</button>
    </form>
  );
}

5. Validação com React – Abordagens e Bibliotecas

Para validação manual, podemos criar um estado de erros:

function FormularioValidado() {
  const [formData, setFormData] = useState({ email: '', senha: '' });
  const [erros, setErros] = useState({});

  const validar = () => {
    const novosErros = {};

    if (!formData.email) {
      novosErros.email = 'E-mail é obrigatório';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      novosErros.email = 'E-mail inválido';
    }

    if (!formData.senha) {
      novosErros.senha = 'Senha é obrigatória';
    } else if (formData.senha.length < 8) {
      novosErros.senha = 'Mínimo de 8 caracteres';
    }

    setErros(novosErros);
    return Object.keys(novosErros).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (validar()) {
      // Enviar dados
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        value={formData.email}
        onChange={handleChange}
        aria-invalid={!!erros.email}
        aria-describedby={erros.email ? 'erro-email' : undefined}
      />
      {erros.email && <span id="erro-email">{erros.email}</span>}
      {/* ... */}
    </form>
  );
}

Com React Hook Form, a validação fica mais concisa:

import { useForm } from 'react-hook-form';

function FormularioComHook() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const onSubmit = (data) => {
    console.log('Dados válidos:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'E-mail é obrigatório',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: 'E-mail inválido'
          }
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        {...register('senha', {
          required: 'Senha é obrigatória',
          minLength: {
            value: 8,
            message: 'Mínimo de 8 caracteres'
          }
        })}
        type="password"
      />
      {errors.senha && <span>{errors.senha.message}</span>}

      <button type="submit">Enviar</button>
    </form>
  );
}

6. Validação Assíncrona e Integração com APIs

Para validar contra o backend (ex: e-mail já cadastrado):

import { useForm } from 'react-hook-form';

function FormularioAssincrono() {
  const { register, handleSubmit, formState: { errors } } = useForm();

  const validarEmailUnico = async (email) => {
    try {
      const response = await fetch(`/api/verificar-email?email=${email}`);
      const data = await response.json();
      return data.disponivel || 'E-mail já cadastrado';
    } catch (error) {
      return 'Erro ao verificar e-mail';
    }
  };

  const onSubmit = async (data) => {
    try {
      const response = await fetch('/api/cadastro', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data)
      });

      if (!response.ok) {
        throw new Error('Erro no servidor');
      }

      alert('Cadastro realizado com sucesso!');
    } catch (error) {
      alert('Erro de rede. Tente novamente.');
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        {...register('email', {
          required: 'E-mail é obrigatório',
          validate: validarEmailUnico
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit" disabled={Object.keys(errors).length > 0}>
        Cadastrar
      </button>
    </form>
  );
}

7. Boas Práticas, Acessibilidade e UX

Para uma experiência de usuário de qualidade:

// Componente de input acessível
function InputAcessivel({ label, erro, ...props }) {
  const id = `campo-${props.name}`;
  const erroId = `erro-${props.name}`;

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input
        id={id}
        aria-invalid={!!erro}
        aria-describedby={erro ? erroId : undefined}
        {...props}
      />
      {erro && (
        <span id={erroId} role="alert" style={{ color: 'red' }}>
          {erro}
        </span>
      )}
    </div>
  );
}

Boas práticas essenciais:
- Mensagens claras: "E-mail inválido" em vez de "Erro no campo"
- Posicionamento: mensagens próximas ao campo correspondente
- Feedback visual: bordas vermelhas para erro, verdes para sucesso
- Foco automático: levar o foco ao primeiro campo com erro
- Desabilitar submit: impedir envio enquanto houver erros
- Acessibilidade: usar aria-invalid, aria-describedby e role="alert"

// Foco automático no primeiro campo com erro
useEffect(() => {
  const primeiroErro = document.querySelector('[aria-invalid="true"]');
  if (primeiroErro) {
    primeiroErro.focus();
  }
}, [erros]);

A validação no frontend não é apenas sobre impedir dados inválidos — é sobre guiar o usuário com clareza, eficiência e respeito ao seu tempo. Combinando HTML semântico, JavaScript estratégico e React com boas bibliotecas, construímos formulários que funcionam bem para todos.

Referências