Skip to content
This page has been auto-translated and may contain errors.View in English

Validación de formularios ​

docs.scrimba.com

Un formulario recopila datos que las personas ingresan, y las personas dejan campos vacíos, escriben mal un correo o meten letras donde debería ir un número. No quieres que esos datos lleguen a tu código sin ningún filtro. Antes de que se envíe cualquier cosa, el navegador puede revisar buena parte de eso por ti, pero solo si le describes qué espera cada campo. Esa descripción se escribe directamente en tu HTML, y este capítulo trata de cómo escribirla.

Campos obligatorios y tipos de entrada ​

Las dos comprobaciones más baratas que puedes agregar son indicar que un campo debe llenarse sí o sí, y decir qué tipo de valor le corresponde.

Agrega la palabra required a un input y el navegador se niega a enviar el formulario hasta que ese campo tenga algo escrito. No necesitas escribir ningún código para que esto funcione. El atributo mismo es la instrucción.

html
<input type="text" name="full_name" required>

La otra mitad es el atributo type. Le indica al navegador qué forma debería tener el valor, y el navegador lo revisa por ti. Usa type="email" y el navegador se asegura de que el valor parezca un correo electrónico. Usa type="number" y solo acepta números. Hay un tipo para fechas, uno para direcciones web, y más.

Piénsalo como un formulario en el consultorio de un médico donde algunas casillas están marcadas como "obligatorio llenar" y la casilla de fecha ya tiene espacios separados para día, mes y año. El papel te dice qué va en cada lugar antes de que escribas algo.

required es un atributo booleano: su sola presencia activa la regla, y no necesita ningún valor. Un control marcado con required bloquea el envío del formulario mientras esté vacío.

El atributo type cumple una doble función. Elige el control que renderiza el navegador (un selector de fecha, un contador numérico) y establece una restricción incorporada sobre el valor:

  • type="email" requiere una @ y una parte final con forma de dominio.
  • type="url" requiere algo que se pueda interpretar como una dirección web absoluta.
  • type="number" solo acepta entradas numéricas y habilita min, max y step.
  • type="tel" no restringe el valor, pero activa un teclado numérico de teléfono en dispositivos móviles.

Elige el tipo más específico antes de recurrir a cualquier otra cosa. Es la menor cantidad de código para la mayor cantidad de validación, y además mejora el teclado en pantalla que reciben las personas en dispositivos táctiles.

required y type son las dos primeras capas de lo que la plataforma llama validación de restricciones (constraint validation): el sistema incorporado del navegador para revisar el valor de un control contra reglas que declaras en el markup. required establece la regla de "valor faltante". type establece una regla de formato asociada a ese tipo.

Dos notas importantes sobre type para producción. Primero, type="email" no valida contra la especificación completa de correo electrónico. Los navegadores usan un patrón deliberadamente permisivo (una serie de caracteres, una @, y luego un dominio separado por puntos) porque una revisión más estricta rechazaría direcciones que en realidad sí son entregables. Trátalo como una revisión de forma, no como prueba de que la dirección existe. Segundo, un type desconocido o no soportado cae de vuelta a type="text", así que un campo nunca queda inutilizable en un navegador antiguo, solo pierde la validación adicional. Esa degradación controlada es la razón por la que puedes adoptar tipos nuevos sin necesitar una tabla de compatibilidad a la mano.

El tipo también determina el teclado en pantalla y, a través del atributo relacionado inputmode (una pista sobre qué diseño de teclado mostrar sin afectar la validación), puedes ajustar ese teclado de forma independiente. Elegir bien el tipo es tanto una decisión de accesibilidad y ergonomía como de validación.

Aquí hay un pequeño formulario de registro que usa ambos atributos:

html
<form>
  <label>
    Correo electrónico
    <input type="email" name="email" required>
  </label>
  <label>
    Sitio web
    <input type="url" name="website">
  </label>
  <button>Registrarse</button>
</form>

El campo de correo debe llenarse y debe parecer un correo electrónico. El campo del sitio web es opcional, pero si alguien escribe algo, tiene que parecer una dirección web. El navegador revisa ambos en el momento en que se presiona el botón.

JunoCampos obligatorios y tipos de entrada Dos palabras pequeñas hacen mucho trabajo aquí. Pon required en un campo y el navegador no dejará pasar uno vacío. Define el type, como email o number, y revisa que el valor tenga la forma correcta. Elige el tipo que corresponda al campo y obtienes validación gratis.
JunoCampos obligatorios y tipos de entradarequired es un atributo booleano: o está o no está, no necesita ningún valor. El type tanto elige el control como fija una regla de formato, así que type="email" requiere una @ y un dominio. Empieza siempre con el tipo más específico; es la menor cantidad de markup para la mayor validación, y de paso los teclados móviles mejoran también.
JunoCampos obligatorios y tipos de entrada Estas son las dos primeras capas de la validación de restricciones, el verificador de reglas incorporado del navegador. Vale la pena recordar: type="email" es una revisión de forma laxa y no es prueba de que la dirección sea real, y un tipo no soportado cae discretamente a text en lugar de romperse. Elegir el tipo también es una decisión de accesibilidad, ya que define el teclado en pantalla que reciben las personas.

Atributos de restricción ​

Más allá de "lleno" y "tipo correcto", muchas veces quieres poner límites: un valor de al menos cierta longitud, un número que no supere cierto tope, un código con un formato específico. Un puñado de atributos cubren esto.

Puedes poner límites a lo que acepta un campo. Dos que son útiles para empezar:

html
<input type="text" name="username" maxlength="20">
<input type="number" name="quantity" min="1" max="10">

maxlength="20" detiene el campo una vez que tiene veinte caracteres. min y max fijan el número más pequeño y el más grande permitidos. El navegador mantiene a las personas dentro de esos límites, así que tú no tienes que revisarlo por tu cuenta.

Hay un conjunto pequeño de atributos de restricción, y cada uno corresponde a una regla:

  • minlength / maxlength fijan el número mínimo y máximo de caracteres que puede tener un valor de texto.
  • min / max fijan el valor más bajo y más alto para números y fechas.
  • step fija el incremento permitido para un número, así que step="5" acepta 0, 5, 10, y rechaza 3.
  • pattern contiene una expresión regular (una notación compacta para describir la forma exacta del texto permitido) que el valor debe cumplir por completo.
html
<input type="text" name="username" minlength="3" maxlength="20" required>
<input type="number" name="quantity" min="1" max="10" step="1">
<input type="text" name="pin" pattern="[0-9]{4}" title="Cuatro dígitos">

Combina pattern con un title: algunos navegadores muestran ese texto en el mensaje de error, y le indica a los usuarios que ven la pantalla qué espera el campo. En CSS, :valid y :invalid te permiten aplicar estilos a un control según si actualmente cumple sus restricciones.

Cada atributo de restricción corresponde a una marca que el navegador puede levantar sobre un control, algo que importa cuando lees el estado de validación desde JavaScript. Violar min levanta "range underflow", superar maxlength levanta "too long", no cumplir pattern levanta "pattern mismatch", y así sucesivamente. El atributo es la declaración; la marca es cómo se reporta el resultado.

Dos detalles con los que la gente suele tropezar. pattern está anclado implícitamente: la expresión debe coincidir con el valor completo, como si estuviera envuelta para cubrir desde el inicio hasta el final, así que pattern="[0-9]{4}" significa exactamente cuatro dígitos, no "contiene cuatro dígitos". Y step se mide desde una base (el min, o cero si no hay ninguno) usando aritmética de punto flotante, así que step="0.1" puede rechazar un valor que esperabas que pasara, por cómo el binario representa los decimales; cuando la precisión importa, fija un min explícito como base.

Las pseudoclases :valid y :invalid (selectores de CSS que hacen coincidir un elemento según su estado en lugar de su posición) son útiles pero poco sutiles: un campo required vacío coincide con :invalid desde el primer renderizado, así que pintarlo de rojo recibe al usuario con errores antes de que haya escrito nada. La solución es :user-invalid, que solo coincide después de que la persona haya interactuado con el campo, así que la retroalimentación llega cuando es útil y no de entrada.

La regla que ahorra más confusiones: un pattern debe coincidir con el valor completo, no solo con una parte de él.

html
<input
  type="text"
  name="product_code"
  pattern="[A-Z]{2}-[0-9]{3}"
  title="Dos letras, un guion, tres dígitos, por ejemplo AB-123"
  required
>
JunoAtributos de restricción Una vez que un campo es obligatorio y tiene un tipo definido, puedes agregar límites. maxlength limita cuántos caracteres entran, y min y max encierran un número entre dos valores. El navegador mantiene a las personas dentro de esos límites automáticamente, y eso es mucha validación que nunca tienes que escribir.
JunoAtributos de restricción El conjunto es pequeño: minlength/maxlength para texto, min/max para números y fechas, step para incrementos, y pattern para una forma exacta. Dale a pattern un title para que el mensaje signifique algo. Y pattern coincide con el valor completo, así que [0-9]{4} son cuatro dígitos en total, no cuatro dígitos en algún lugar dentro del valor.
JunoAtributos de restricción Cada atributo corresponde a una marca de validez que puedes leer después, así que el markup y el reporte quedan alineados. Dos trampas: pattern está anclado de extremo a extremo, y step usa matemática de punto flotante desde una base, así que fija un min explícito cuando la precisión importa. Aplica estilos con :user-invalid, no con :invalid, o pintarás de rojo un campo que nadie ha tocado todavía antes de que alguien escriba algo.

Retroalimentación nativa de validación ​

Declarar las reglas es solo la mitad de la historia. La otra mitad es lo que ve la persona cuando un valor rompe alguna de ellas.

Cuando alguien presiona el botón de enviar y un campo está mal, el navegador detiene el envío y muestra un pequeño mensaje junto al primer campo con problemas, y luego mueve el cursor hacia él. Tú no construyes ese mensaje. El navegador lo escribe, en el idioma de la persona que visita la página, y lo muestra automáticamente.

Así que un campo de correo required que quedó vacío produce algo como "Por favor completa este campo", y el formulario no se envía hasta que se corrija.

La validación se activa al enviar. El navegador recorre los controles en orden, encuentra el primero que no es válido, le da el foco, y muestra una pequeña burbuja de mensaje describiendo el problema. Si todo pasa, el formulario se envía con normalidad.

Tienes algo de control de estilos a través de CSS. :required, :valid, :invalid y :in-range te permiten marcar campos visualmente. Lo que no puedes rediseñar fácilmente es la burbuja del mensaje en sí; su apariencia le pertenece al navegador. También puedes desactivar todo el sistema para un formulario con el atributo novalidate, lo cual es útil cuando piensas validar todo con JavaScript.

css
input:user-invalid {
  border-color: #c0392b;
}
input:user-valid {
  border-color: #2d7a3f;
}

La retroalimentación nativa es conveniente y prácticamente gratuita, y viene con límites que conviene tener en cuenta antes de depender de ella en un producto en producción.

La burbuja del mensaje es un elemento transitorio: aparece al enviar, desaparece con la siguiente interacción, y no es parte del documento, así que no la puedes seleccionar ni aplicarle estilos. El soporte de lectores de pantalla para ella es irregular entre navegadores, así que una burbuja sola no es una forma confiable de anunciar un error a alguien que no puede verla. Su texto además se localiza según el idioma del navegador, no según el lang de tu página, así que un formulario escrito en inglés puede mostrar un mensaje de error en francés a una persona cuyo navegador está configurado en francés. Eso es correcto para el usuario y sorprendente para quien desarrolla.

Puedes disparar el mismo flujo tú mismo sin enviar el formulario: reportValidity() ejecuta las revisiones y muestra las burbujas a demanda. Pero para cualquier cosa más allá de un prototipo rápido, el patrón accesible es suprimir la burbuja nativa (con novalidate) y renderizar tu propio texto de error en la página, asociado al campo, para que sea visible, se le puedan aplicar estilos, y se anuncie de forma confiable. La siguiente sección explica cómo hacerlo.

JunoRetroalimentación nativa de validación Aquí está la recompensa de todos esos atributos: presiona enviar con un campo mal llenado y el navegador muestra un pequeño mensaje, te señala el campo, y se niega a enviarlo. Tú no escribiste nada de ese texto; lo escribió el navegador, en el idioma de quien visita la página. Para muchos formularios, esto es toda la retroalimentación que necesitas.
JunoRetroalimentación nativa de validación La revisión se ejecuta al enviar: el primer campo con problemas recibe el foco y una burbuja de mensaje, y el formulario queda detenido hasta que pase la validación. Puedes aplicar estilos a los campos con :user-valid y :user-invalid, pero la burbuja en sí es cosa del navegador. Desactiva todo con novalidate cuando planees manejarlo con JavaScript.
JunoRetroalimentación nativa de validación La burbuja es transitoria y no se le pueden aplicar estilos, su soporte en lectores de pantalla es desigual, y su texto sigue el idioma del navegador, no el de tu página. Suficiente para un prototipo, insuficiente para producción. Cuando la accesibilidad importa, agrega novalidate y renderiza tu propio texto de error en la página para que se pueda ver, estilizar y anunciar correctamente.

Cuándo todavía necesitas JavaScript ​

La validación nativa revisa un campo contra sus propias reglas. Muchas revisiones reales no caben en ese molde, y ahí es donde entra JavaScript.

Hay cosas que el navegador no puede revisar por su cuenta. Si dos campos de contraseña coinciden entre sí. Si un nombre de usuario ya lo tiene otra persona. Si un código de descuento es real. Ninguna de estas tiene que ver con la forma de un solo campo, así que no existe un atributo para ellas, y ahí recurres a JavaScript para hacer la validación.

Eso no es una falla de HTML. Las validaciones incorporadas cubren los casos comunes sin necesidad de código, y JavaScript se encarga del resto.

El vacío lo llena cualquier cosa que dependa de más de un campo, o de información que la página todavía no tiene. Un ejemplo común es confirmar que dos contraseñas coincidan. Las comparas en JavaScript y le devuelves el resultado al sistema nativo con setCustomValidity, que fija un mensaje de error personalizado en un control (una cadena vacía significa "esto es válido"):

html
<input type="password" id="password" name="password" required>
<input type="password" id="confirm" name="confirm" required>

La comparación en sí se ejecuta en un <script>:

js
const password = document.querySelector("#password")
const confirm = document.querySelector("#confirm")

confirm.addEventListener("input", () => {
  // una cadena vacía limpia el error y marca el campo como válido
  const message = confirm.value === password.value ? "" : "Las contraseñas no coinciden"
  confirm.setCustomValidity(message)
})

El campo ahora participa en la validación nativa como cualquier otro, bloqueando el envío y mostrando tu mensaje cuando las contraseñas difieren. Las revisiones que necesitan un servidor, como la disponibilidad de un nombre de usuario, siguen la misma lógica, pero fijan el mensaje después de que llega la respuesta de la petición de red.

JavaScript llega a la validación nativa a través de la Constraint Validation API (el conjunto de propiedades y métodos que el navegador expone en los controles de formulario para leer y establecer su validez). Las piezas que más vas a usar:

  • setCustomValidity(message) fija una cadena de error personalizada en un control. Una cadena no vacía lo marca como inválido y se convierte en su mensaje; una cadena vacía limpia el error personalizado.
  • validity es un objeto de solo lectura ValidityState: un booleano por cada posible falla (valueMissing, typeMismatch, patternMismatch, rangeOverflow, tooLong, customError, y demás), más valid para el resultado general. Lo lees para saber por qué falló un campo, no solo que falló.
  • checkValidity() devuelve verdadero o falso sin mostrar nada; reportValidity() hace lo mismo y además muestra las burbujas nativas.

Para errores accesibles, el color nunca es suficiente por sí solo: un borde rojo no le dice nada a un lector de pantalla ni a alguien que no puede distinguir el rojo del verde. Asocia el texto del error con su campo para que la tecnología de asistencia los lea juntos. Fija aria-describedby en el input con el id del elemento de error (esto le indica a un lector de pantalla "lee este texto como la descripción del campo"), y fija aria-invalid="true" mientras el campo esté fallando (esto anuncia que el campo está en estado de error):

html
<input
  type="text"
  id="username"
  name="username"
  aria-describedby="username-error"
  aria-invalid="true"
  required
>
<p id="username-error" role="alert">Ese nombre de usuario ya está en uso.</p>

El role="alert" hace que un lector de pantalla anuncie el mensaje en cuanto aparece. El capítulo de Accesibilidad profundiza en cómo asociar controles con sus descripciones.

Eso deja la decisión de compromiso. La validación nativa requiere menos código, es consistente con la plataforma, y funciona antes de que tu script cargue, pero su retroalimentación es difícil de estilizar y anunciar. La validación personalizada requiere más código y más responsabilidad, pero te da control total sobre la redacción, el momento en que aparece, y la accesibilidad. La mayoría de los formularios en producción usan ambas: los atributos nativos como base, y JavaScript encima para las revisiones y los mensajes que los atributos no pueden cubrir.

Cualquiera que elijas, hay una regla que no se negocia. La validación del cliente es una comodidad, nunca una garantía. Todo lo descrito en este capítulo se ejecuta en el navegador de quien visita la página, donde cualquiera puede desactivarlo, editar la página, o enviar una petición directamente a tu servidor sin tocar el formulario en absoluto. El servidor debe revalidar cada valor que recibe como si nunca hubiera ocurrido ninguna revisión en el navegador. Trata las revisiones del navegador como un primer filtro rápido y amigable que le ahorra a tus usuarios un viaje de ida y vuelta, y trata al servidor como la revisión que en verdad protege tus datos.

JunoCuándo todavía necesitas JavaScript Las validaciones incorporadas cubren un campo a la vez, así que cualquier cosa que compare campos, como "estas dos contraseñas coinciden", o que revise con un servidor, como "este nombre de usuario está disponible", necesita JavaScript. Eso es normal. HTML se encarga de los casos cotidianos sin costo, y JavaScript se encarga de los que no puede ver por su cuenta.
JunoCuándo todavía necesitas JavaScript Cuando una revisión abarca dos campos o necesita al servidor, calcúlala en JavaScript y devuelve el resultado con setCustomValidity: una cadena de mensaje para invalidar el campo, una cadena vacía para limpiarlo. El campo se une entonces a la validación nativa como cualquier otro. Que las contraseñas coincidan y que un nombre de usuario esté disponible son los dos casos clásicos.
JunoCuándo todavía necesitas JavaScript La Constraint Validation API es tu punto de entrada: setCustomValidity para fijar un mensaje, validity para leer por qué falló un campo, checkValidity y reportValidity para ejecutar las revisiones. Para errores accesibles, asocia el texto con el campo usando aria-describedby y aria-invalid, nunca solo con color. Y lo que no es opcional: cada revisión aquí se ejecuta en el navegador, así que el servidor tiene que revalidar todo de nuevo.