Skip to content

Formularios y validación ​

Registro de usuarios, formularios de contacto, checkout de una tienda, cotizadores: los formularios son la forma en que tus clientes reciben datos. Un formulario bien validado evita datos basura, reduce la frustración del usuario y aumenta las conversiones. En esta lección aprenderás a leer, validar y enviar formularios con JavaScript.

Concepto ​

Validar es como el guardia de la entrada de un evento: revisa que cada persona tenga boleto, que sea mayor de edad y que su nombre esté en la lista antes de dejarla pasar. Hay dos guardias:

  1. En el navegador (frontend): da retroalimentación inmediata y mejora la experiencia. Lo veremos aquí.
  2. En el servidor (backend): es obligatorio, porque cualquiera puede saltarse el JavaScript del navegador (desactivándolo o enviando peticiones directas).

Importante

La validación en JavaScript no es seguridad, es experiencia de usuario. Siempre valida también en el servidor (PHP, Node, Laravel...).

Leer los valores de un formulario ​

html
<form id="registro" novalidate>
  <input name="nombre" id="nombre">
  <input name="email" type="email" id="email">
  <select name="plan">
    <option value="basico">Básico</option>
    <option value="pro">Pro</option>
  </select>
  <input name="terminos" type="checkbox">
  <label><input type="radio" name="pago" value="tarjeta" checked> Tarjeta</label>
  <label><input type="radio" name="pago" value="oxxo"> OXXO</label>
  <button>Registrarme</button>
</form>
js
const form = document.querySelector('#registro');

form.addEventListener('submit', (e) => {
  e.preventDefault();

  // Opción 1: campo por campo
  const nombre = form.elements.nombre.value.trim();
  const acepta = form.elements.terminos.checked;
  const pago = form.elements.pago.value;  // valor del radio seleccionado

  // Opción 2: FormData (lee todos los campos con atributo name)
  const datos = new FormData(form);
  datos.get('email');
  datos.get('plan');

  // Convertir a objeto plano
  const objeto = Object.fromEntries(datos);
  // { nombre: 'Ana', email: 'ana@...', plan: 'pro', pago: 'tarjeta', terminos: 'on' }
  console.log(objeto);
});

Error común

FormData sólo incluye los campos que tienen atributo name. Los checkbox desmarcados no aparecen en el objeto. Y todos los valores son strings: convierte números con Number().

Para varios checkbox con el mismo nombre usa getAll:

js
// <input type="checkbox" name="intereses" value="html">
// <input type="checkbox" name="intereses" value="css">
const intereses = new FormData(form).getAll('intereses'); // ['html', 'css']

Validación nativa de HTML5 ​

Antes de escribir JavaScript, aprovecha lo que el navegador ya sabe hacer:

html
<input name="nombre" required minlength="3" maxlength="50">
<input name="email" type="email" required>
<input name="edad" type="number" min="18" max="99">
<input name="telefono" type="tel" pattern="[0-9]{10}" title="10 dígitos">
<input name="web" type="url">

Con estos atributos el navegador bloquea el envío y muestra mensajes. Las pseudoclases CSS :valid, :invalid y :user-invalid (esta última sólo después de que el usuario interactúa) te permiten estilizar:

css
input:user-invalid { border-color: #dc2626; }
input:valid { border-color: #16a34a; }

Constraint Validation API ​

Los mensajes nativos son feos y no se pueden personalizar bien. La solución profesional: agregar novalidate al formulario (desactiva las burbujas nativas) y usar la API de validación desde JavaScript, que sigue leyendo los atributos required, type, minlength...

js
const email = document.querySelector('#email');

email.checkValidity();    // true / false
email.validity;           // objeto con el detalle:
// {
//   valueMissing: true,   ← required y vacío
//   typeMismatch: false,  ← no es un email/url válido
//   tooShort: false,      ← menos que minlength
//   tooLong: false,
//   patternMismatch: false,
//   rangeUnderflow: false, ← menor que min
//   rangeOverflow: false,
//   customError: false,
//   valid: false
// }
email.validationMessage;  // mensaje del navegador

// Errores personalizados
email.setCustomValidity('Este correo ya está registrado');
email.setCustomValidity(''); // limpiar (vuelve a ser válido)

form.checkValidity(); // ¿todo el formulario es válido?

Mensajes en español y a tu medida ​

js
function mensajeDeError(campo) {
  const v = campo.validity;
  if (v.valueMissing) return 'Este campo es obligatorio.';
  if (v.typeMismatch && campo.type === 'email') return 'Escribe un correo válido, por ejemplo ana@correo.com.';
  if (v.tooShort) return `Usa al menos ${campo.minLength} caracteres (llevas ${campo.value.length}).`;
  if (v.patternMismatch) return campo.title || 'El formato no es válido.';
  if (v.rangeUnderflow) return `El valor mínimo es ${campo.min}.`;
  if (v.rangeOverflow) return `El valor máximo es ${campo.max}.`;
  if (v.customError) return campo.validationMessage;
  return '';
}

Validación personalizada en vivo ​

Mostrar los errores mientras el usuario escribe (o al salir del campo) es mucho mejor que esperar al envío. Patrón recomendado:

  • Valida al salir del campo (blur) la primera vez, para no regañar mientras escribe.
  • Una vez que el campo fue tocado, valida en cada input.
  • Al enviar, valida todo y enfoca el primer campo con error.
html
<div class="campo">
  <label for="email">Correo</label>
  <input id="email" name="email" type="email" required aria-describedby="email-error">
  <small class="error" id="email-error" aria-live="polite"></small>
</div>
js
const form = document.querySelector('#registro');
const campos = form.querySelectorAll('input, select, textarea');

function validarCampo(campo) {
  const mensaje = mensajeDeError(campo);
  const error = document.getElementById(`${campo.id}-error`);
  if (error) error.textContent = mensaje;
  campo.setAttribute('aria-invalid', mensaje ? 'true' : 'false');
  campo.classList.toggle('invalido', Boolean(mensaje));
  return !mensaje;
}

campos.forEach((campo) => {
  campo.addEventListener('blur', () => {
    campo.dataset.tocado = 'true';
    validarCampo(campo);
  });
  campo.addEventListener('input', () => {
    if (campo.dataset.tocado) validarCampo(campo);
  });
});

form.addEventListener('submit', (e) => {
  e.preventDefault();
  const resultados = [...campos].map(validarCampo);
  if (resultados.includes(false)) {
    form.querySelector('[aria-invalid="true"]').focus();
    return;
  }
  enviar(new FormData(form));
});

Reglas comunes con expresiones regulares ​

Una expresión regular (regex) describe un patrón de texto. No necesitas memorizarlas, pero sí entender las más usadas:

js
const reglas = {
  // algo@algo.algo (validación práctica, no perfecta)
  email: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/,

  // teléfono de México: 10 dígitos
  telefono: /^\d{10}$/,

  // código postal de México: 5 dígitos
  cp: /^\d{5}$/,

  // RFC persona física (simplificado)
  rfc: /^[A-ZÑ&]{4}\d{6}[A-Z0-9]{3}$/i,

  // contraseña: mínimo 8, una mayúscula, una minúscula y un número
  password: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/
};

reglas.email.test('ana@correo.com'); // true
reglas.telefono.test('33 1234 5678'.replace(/\s/g, '')); // true

Buena práctica

Limpia antes de validar: quita espacios y guiones de teléfonos y tarjetas (valor.replace(/[\s-]/g, '')). Acepta el formato que el usuario escriba y normalízalo tú.

Reglas que dependen de otros campos ​

js
const pass = form.elements.password;
const confirmar = form.elements.confirmar;

confirmar.addEventListener('input', () => {
  confirmar.setCustomValidity(
    confirmar.value !== pass.value ? 'Las contraseñas no coinciden.' : ''
  );
  validarCampo(confirmar);
});

Indicador de fortaleza de contraseña ​

js
function fortaleza(password) {
  let puntos = 0;
  if (password.length >= 8) puntos++;
  if (password.length >= 12) puntos++;
  if (/[A-Z]/.test(password) && /[a-z]/.test(password)) puntos++;
  if (/\d/.test(password)) puntos++;
  if (/[^A-Za-z0-9]/.test(password)) puntos++;
  return ['Muy débil', 'Débil', 'Regular', 'Buena', 'Fuerte', 'Excelente'][puntos];
}

Demo en vivo ​

Formulario de registro con validación en vivo, mensajes en español, confirmación de contraseña, medidor de fortaleza y envío simulado:

Validación de formulario

Enviar el formulario con fetch ​

Una vez validado, se envía al servidor sin recargar la página. Verás fetch a fondo en Fetch y APIs; este es el patrón:

js
async function enviar(datos) {
  const boton = form.querySelector('button[type="submit"]');
  boton.disabled = true;               // evita doble envío
  boton.textContent = 'Enviando...';

  try {
    const respuesta = await fetch('https://jsonplaceholder.typicode.com/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(Object.fromEntries(datos))
    });
    if (!respuesta.ok) throw new Error(`Error ${respuesta.status}`);

    const resultado = await respuesta.json();
    mostrarMensaje(`¡Registro exitoso! ID: ${resultado.id}`, 'exito');
    form.reset();
  } catch (error) {
    mostrarMensaje('No pudimos enviar el formulario. Intenta de nuevo.', 'error');
    console.error(error);
  } finally {
    boton.disabled = false;
    boton.textContent = 'Registrarme';
  }
}

Simular sin servidor

Para practicar sin backend puedes simular la espera con una promesa:

js
const esperar = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
await esperar(1200); // "el servidor está procesando"

Accesibilidad en formularios ​

  • Cada campo con su <label for="..."> (hacer clic en la etiqueta enfoca el campo).
  • Errores conectados con aria-describedby y anunciados con aria-live="polite".
  • Marca los campos con error con aria-invalid="true".
  • No dependas sólo del color: acompaña el borde rojo con un texto.
  • Enfoca el primer campo con error al enviar.

Ejercicios ​

  1. Escribe validarCP(valor) que acepte códigos postales mexicanos de 5 dígitos, ignorando espacios.
  2. Crea un formulario de contacto (nombre, email, mensaje de 20 a 500 caracteres) que muestre los errores debajo de cada campo al enviar.
  3. Deshabilita el botón "Enviar" mientras haya algún campo inválido (escuchando input en el formulario completo).
  4. Haz que el campo "Confirmar email" muestre error si no coincide con "Email".
  5. Convierte un FormData con campos nombre, edad y intereses (checkbox múltiple) en este objeto: { nombre: 'Ana', edad: 28, intereses: ['html', 'css'] }.
Ver solución
js
// 1
const validarCP = (valor) => /^\d{5}$/.test(valor.replace(/\s/g, ''));

// 2 (lógica)
const form = document.querySelector('#contacto');
form.addEventListener('submit', (e) => {
  e.preventDefault();
  const { nombre, email, mensaje } = form.elements;
  const errores = {
    nombre: nombre.value.trim().length < 2 ? 'Escribe tu nombre.' : '',
    email: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email.value) ? '' : 'Correo no válido.',
    mensaje: mensaje.value.trim().length < 20 || mensaje.value.length > 500
      ? 'El mensaje debe tener entre 20 y 500 caracteres.' : ''
  };
  for (const [campo, msg] of Object.entries(errores)) {
    document.getElementById(`${campo}-error`).textContent = msg;
  }
  if (Object.values(errores).every((m) => m === '')) {
    alert('¡Mensaje enviado!');
    form.reset();
  }
});

// 3
const boton = form.querySelector('button');
form.addEventListener('input', () => {
  boton.disabled = !form.checkValidity();
});

// 4
const { email, confirmarEmail } = form.elements;
confirmarEmail.addEventListener('input', () => {
  confirmarEmail.setCustomValidity(
    confirmarEmail.value.trim().toLowerCase() !== email.value.trim().toLowerCase()
      ? 'Los correos no coinciden.' : ''
  );
});

// 5
const datos = new FormData(form);
const objeto = {
  nombre: datos.get('nombre'),
  edad: Number(datos.get('edad')),
  intereses: datos.getAll('intereses')
};

Resumen ​

  • Valida en el navegador para la experiencia y siempre en el servidor para la seguridad.
  • Lee formularios con form.elements o new FormData(form) + Object.fromEntries.
  • Aprovecha los atributos HTML5 (required, type, minlength, pattern) y léelos con la Constraint Validation API (validity, setCustomValidity).
  • Usa novalidate para mostrar tus propios mensajes en español.
  • Valida al salir del campo y luego en vivo; al enviar, enfoca el primer error.
  • Evita dobles envíos deshabilitando el botón y maneja errores con try/catch.

Siguiente: Storage y JSON

Academia Web — aprende, copia y construye.