El uso insuficiente de la validación de formularios HTML
(expressionstatement.com)- Los formularios HTML ya cuentan con funciones nativas de validación como
required,type="email",pattern,maxlengthysetCustomValidity, pero en la práctica se usan menos de lo que su alcance permitiría setCustomValidity, la más potente de todas, puede manejar lógica de validación arbitraria y casos complejos, pero como solo se ofrece como método del DOM, no encaja bien con componentes declarativos- En entornos como React, para hacer coincidir la validación del valor inicial terminan mezclándose
useRef,useLayoutEffectyonChange, y la misma lógica de validación suele duplicarse entre el render inicial y el manejador de cambios - Si existiera una abstracción basada en atributos como
custom-validity, sería posible expresar en un solo lugar validaciones con dependencia entre estado e input, como una comprobación asíncrona de nombre de usuario duplicado o la confirmación de contraseña - La baja adopción de la validación nativa de formularios parece deberse menos a la falta de funciones y más a un problema de usabilidad del API; si un API declarativa llegara a la especificación de HTML, su uso podría crecer bastante
Lo que ya ofrece la validación de formularios HTML
- La forma más simple de impedir una entrada vacía es agregar el atributo
required - Las restricciones de entrada pueden aplicarse en tres grandes formas
- usar tipos de entrada como
type="email",type="number"ytype="url" - usar atributos de restricción como
patternymaxlength - usar el método del DOM
setCustomValiditydel input
- usar tipos de entrada como
setCustomValidityes la herramienta más potente porque puede manejar lógica de validación arbitraria y casos complejos- Las dos primeras formas pueden declararse como atributos HTML, pero
setCustomValidityse diferencia en que requiere una llamada a método - Como material de referencia sobre la diferencia entre atributos y propiedades del DOM, se enlaza Attributes vs Properties
Dónde setCustomValidity empieza a volverse incómodo
setCustomValidityse expone solo como método, sin un atributo HTML equivalente- Si se le pasa una cadena, el input queda en estado invalid y el navegador muestra esa cadena como motivo del fallo de validación
- Si se le pasa una cadena vacía, el input queda en estado valid salvo que exista alguna otra restricción
- Para implementar manualmente algo como
required, hay que llamar asetCustomValiditycada vez que cambia el valor - Sin embargo, el input comienza inicialmente en estado valid, así que si se presiona el botón de submit justo después de resetear el componente, el envío del formulario podría pasar
- Esto ocurre porque, aunque el valor esté vacío, el código de validación no se ejecuta antes de que se dispare un evento de cambio
- Para que también coincida con el valor inicial, la misma validación debe ejecutarse en el momento de montar el componente
Más boilerplate en componentes declarativos
- Si también hay que cubrir la validación del valor inicial, el código se vuelve repetitivo e incómodo muy rápido
- En particular aparecen tres problemas
- La lógica de validación se duplica entre el manejador
onChangey la etapa de render inicial - El código de validación inicial queda separado del elemento de entrada, baja la cohesión y existe el riesgo de modificar solo una de las dos partes
- La combinación de
useRef,useLayoutEffectyonChangese vuelve excesiva a medida que aumentan los inputs, y resulta todavía más confusa cuando solo algunos usancustomValidity
- La lógica de validación se duplica entre el manejador
- Esta complejidad se vuelve especialmente evidente al manejar una API imperativa pura dentro de componentes declarativos
CustomValidityno tiene un atributo de entrada que permita establecer su valor de forma declarativa, como sí ocurre con los atributos nativos de validación- Si una API es engorrosa, su adopción puede ser baja aunque la funcionalidad sea potente, y ese es el motivo central del bajo aprovechamiento de la validación nativa de formularios
La pieza faltante: el atributo custom-validity
- Lo que hace falta es un atributo declarativo como
custom-validityque pueda definirse directamente en el input - En frameworks declarativos, este atributo permitiría mantener el estado de validación cerca del propio elemento de entrada
- Actualmente no existe un
custom-validityreal en laHTML Spec - Es posible imitar este comportamiento con una implementación en espacio de usuario para fines de demostración
- También se ofrece una implementación más completa como ejemplo de componente para producción
Validación asíncrona y dependencias entre inputs
- En aplicaciones reales, la validación puede ser más compleja que una simple verificación local
- En un campo de nombre de usuario, hay que consultar al servidor si ese nombre ya está en uso, y mientras la solicitud está en curso el formulario no debería considerarse valid
- El ejemplo usa
requiredpara impedir entradas vacías y, según el estado de carga y el resultado de la respuesta, pone el input en estado invalid mediantecustomValidity - La implementación se compone de dos partes
- el estado de la solicitud para verificar la unicidad del nombre de usuario se gestiona con
useQuerydereact-query - se utiliza un componente personalizado
<Input />que puede recibir la propcustomValidity
- el estado de la solicitud para verificar la unicidad del nombre de usuario se gestiona con
- Este enfoque permite expresar en un solo atributo todo el flujo de validación asíncrona, incluyendo estados de carga, error y éxito
- Otro ejemplo adicional es un formulario que pide volver a escribir la contraseña ingresada, tratando la validación de campos de entrada que dependen entre sí
Conclusión
setCustomValidityes una herramienta potente capaz de cubrir distintos requisitos de validación- Lo que realmente determina su uso no es solo la existencia de la funcionalidad, sino un API que haga fácil utilizarla
- Una abstracción declarativa como
custom-validityharía más natural el uso de la validación nativa de formularios - Se puede esperar que una función así llegue algún día de forma nativa a la especificación de HTML
1 comentarios
Opiniones de Hacker News
La última vez que revisé, los navegadores web actuales todavía no permitían dar estilo a la apariencia de los mensajes de validación HTML integrados https://stackoverflow.com/questions/5328883/how-do-i-style-t...
Sería menos grave si Chrome y Firefox al menos siguieran las guías de UI de la plataforma del sistema operativo, de modo que parecieran mostrados por el sistema, como los tooltips de
title=""; pero Chrome usa un ícono amarillo/naranja, texto negro sobre fondo blanco y un globo con esquinas redondeadas fijas, lo que choca bastante con la estética del proyecto actual.En versiones antiguas, Chrome permitía estilizar los mensajes de validación con selectores de pseudoelementos con prefijo de proveedor, pero después quitó esa función y nunca la volvió a traer. Termina en mi lista arbitraria de molestias, junto con cosas como “denme un combobox HTML nativo” y “¿por qué esto sigue siendo una caja difícil de usar con Ctrl+clic en lugar de una lista de checkboxes?”
validitynativo del input y renderizarlo como uno quiera.El verdadero problema es que suscribirse con precisión a los cambios de ese estado de validez es complicado. Existen eventos de validez, pero no siempre se disparan. Si se cambia programáticamente el estado del formulario, por ejemplo con una llamada a
form.reset()oinput.value = '...', esos eventos no se activan.Creo que este sería un buen tema para investigar por separado y proponer a la plataforma web.
Lo más grande, fácil de implementar y aun así poco usado es emplear valores específicos del atributo
type, comoemail,numberourl.En móviles, puede mostrar el teclado óptimo y mejorar mucho la experiencia de usuario.
type=numbery uso en su lugartype=text inputmode=numeric. No agrega los botones de flecha que la mayoría de los usuarios no necesita para ingresar números, y en iOS el teclado también es mejor.typecorrecto.date. Cada biblioteca frontend tiene su propio widget de fecha, y muchos se ven pésimo en pantallas pequeñas.Por culpa de ese widget hay que agregar JS y CSS, y algunos dependen de jQuery. Claro que algunos tienen muchísima configuración, pero por un costo muy bajo se puede obtener un widget que se ve bien en todas partes y se siente nativo, que por lo general cubre los requisitos, y del que uno puede olvidarse sin preocuparse por actualizaciones ni CDN.
Con inputs normales apenas se puede lograr, pero los inputs especiales soportan todavía menos eventos.
Mi producto no pasó una auditoría de accesibilidad por usar validación nativa de formularios HTML, y la recomendación oficial fue implementar una capa de validación propia. Yo también estoy de acuerdo con esa recomendación.
La validación nativa de HTML tiene muchos defectos y, sinceramente, la personalización visual ni siquiera es la mayor preocupación. Aunque sí es casi el último clavo en el ataúd.
Por ejemplo, si quieres mostrar varios errores por campo a la vez, no queda más que concatenar cadenas. Algo como “Se requiere un número. Se requiere un símbolo. Debe tener más de 10 caracteres”, lo cual es una mala experiencia de usuario y también es malo para la accesibilidad, porque no se puede navegar por la cadena concatenada en el árbol de accesibilidad. Esto no es un problema de implementación, sino de la especificación misma. No es divertido para el usuario jugar al topo con los errores de validación, así que debería ser posible mostrar varios errores a la vez.
También es malo que dependa del navegador. No lo puedes controlar y las implementaciones, en general, son bastante deficientes. Chrome muestra un popup al enfocar, parece un modal y no puede mostrar todos los errores del formulario a la vez, así que de por sí no es muy accesible. No mostrar información importante en popups es una regla básica de accesibilidad, pero el navegador no tiene muchas alternativas sin interferir con el documento real.
Los errores de todo el formulario que no pertenecen a un campo específico también siguen requiriendo validación personalizada. Por ejemplo, errores como “los campos A y B no son compatibles”, y ya puestos, es mejor tener un método de validación consistente.
Si tienes una entrada personalizada que no encaja con los tipos de input nativos, por consistencia necesitas usar un input oculto, y eso también rompe la accesibilidad. Arreglarlo termina siendo tan difícil como crear tu propio sistema de validación accesible.
La API
customValidityes imperativa y engorrosa de usar. A menos que quieras mensajes horribles como “este campo no es válido”, casi no es una opción no usar validez personalizada. Por eso la validación de formularios HTML es pésima.Aun así, creo que vale la pena aprovechar los mecanismos de validación nativos. No es necesario usar el tooltip nativo de validez para los mensajes de error; puedes leer directamente el
ValidityStatedeinput.validity, renderizar los mensajes como quieras y, si hace falta, mostrar varios errores.Los navegadores pueden mejorar, y si usas un enfoque estandarizado puedes beneficiarte de esas mejoras. Una conclusión del tipo “si no usas popups se rompe la accesibilidad” suena extraña, pero si necesitas responder a una auditoría, quizá haya que ir por ese camino.
También hay técnicas para manejar errores que no pertenecen a un campo específico. En mi proyecto uso un componente
HiddenValidationInputque crea un input de solo lectura invisible para renderizar errores personalizados que no pertenecen a otro campo. Como solo hay que renderizarlo condicionalmente, en la práctica es bastante cómodo de usar.Estoy totalmente de acuerdo en que la API
customValidityes imperativa y engorrosa. Escribí un artículo que trata justamente esa parte, y espero que esto también mejore con el tiempo.El diseño tosco y poco accesible de Chrome es un problema de Chrome, así que voy a abrir un reporte de bug.
Cuando dices “especificación”, ¿te refieres a esto? https://html.spec.whatwg.org/multipage/dom.html#concept-elem...
No veo ninguna parte que defina cómo deben mostrarse los mensajes de validación. Ni siquiera veo que diga que debe haber un mensaje.
Para ser sincero, la gente que escribe estas especificaciones parece desconectada de la realidad y da la impresión de que no usa realmente lo que especifica. Funciona para cosas muy simples, pero cuando un formulario empieza a evolucionar, terminas dándote cuenta de que conviene escribirlo todo por tu cuenta.
En cuanto empiezas a usar JS, se vuelve mucho más fácil hacerlo todo en código que andar manipulando atributos de validación.
Si un checkbox tiene etiqueta, me gustaría que le pusieran el atributo
foral label para poder activar y desactivar el checkbox haciendo clic en la etiqueta. Personalmente es una de las cosas que más me molestan, aunque quizá soy solo yo.inputcon ellabel. No sé muy bien por qué la gente tiende a ponerlos separados.Y tampoco sé por qué los navegadores empezaron a separarlos. Creo que el texto no debería contener checkboxes y radios, sino que los checkboxes y radios deberían contener el texto.
Aquí hay un ejemplo simple que no usa React:
https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...
La validación de formularios HTML es excelente. Pero tiene una trampa enorme:
Firefox para Android no funciona
https://bugzilla.mozilla.org/show_bug.cgi?id=1510450
No vale la pena ignorar el estándar por menos del 1% de usuarios que no pueden ver los mensajes de error de los controles marcados en rojo. Mientras más sitios web usen esto, más presión habrá para que Mozilla arregle el navegador.
Esto no es una API como WebMIDI, que Chrome o Safari implementaron antes de la estandarización; es parte de la especificación original de HTML5.
Más aún si se considera lo poco tiempo que se dedica a garantizar que las personas que usan software de accesibilidad puedan usar bien los sitios. Si no estamos listos para hablar también de UC Browser, no creo que valga mucho la pena mencionarlo en este tipo de temas.
Aun así, poder usar uBlock Origin junto con otras extensiones es una ventaja muy fuerte.
[1] https://bugzilla.mozilla.org/show_bug.cgi?id=1884282
[2] https://bugzilla.mozilla.org/show_bug.cgi?id=1897707
Si la validación simplemente no estuviera implementada, habría sido mejor; esto es un desastre. Me di cuenta hace unos años después de una depuración larga y dolorosa, y estoy seguro de que no fui ni la primera ni la última persona a la que le pasó lo mismo.
Hasta que lo busqué, no podía imaginar qué me estaba faltando, y fue bastante impactante. Está bien crear una validación propia, pero esto debería funcionar.
Hay que tener cuidado de no usarlo en exceso.
Hace poco intenté obtener un reembolso en Groupon. La empresa donde había comprado el Groupon cambió de administración y no quiso reconocer mi Groupon.
El formulario tenía una condición de “mínimo 15 palabras”, pero no había forma de pasar la validación, así que al final tuve que inspeccionar el HTML.
\wera carácter de palabra,\bera límite de palabra,\sera espacio en blanco, y literalmente no permitía ningún signo de puntuación.patternson geniales, pero no son suficientes.Por ejemplo, el caso de “repetir contraseña” se puede implementar con el atributo
patternincluso sinsetCustomValidity. Para hacerlo, habría que construir dinámicamente una expresión regular a partir del primer valor ingresado.No comparé las soluciones para no alargar demasiado el texto, pero el punto es que con
customValidityla validación se vuelve mucho más clara y legible. En este caso, una mejor API hace una gran diferencia.Una restricción de “mínimo 15 palabras” también podría expresarse de forma mucho más clara como
value.split(/\s+/).length >= 15.Hay razones por las que se usa menos. Muchos frameworks y librerías ofrecen funciones de validación sólidas y estilizables, y algunas son muy sofisticadas y extensibles. Si no hace falta sufrir, es mejor no complicarse la vida.
curlu otra herramienta que no tenga incorporada la misma validación de formularios.No se puede confiar en que el cliente haya validado, ni en que lo haya hecho bien, así que el backend igual debe validar la entrada y poder mostrar el formulario con los errores de validación de todos los campos.
La validación en el frontend solo sirve para ayudar al usuario. Pero si quieres darle estilo o dispararla desde el backend al enviar, al final también tienes que implementar tu propio estilizado.
Tiene demasiadas trampas y, para soportar fácilmente comprobaciones más complejas, al final terminas usando una librería.
Además, usar una librería a veces abre la posibilidad de compartir parte del código de validación entre frontend y backend.
En particular, este artículo parece esquivar un problema con
useLayoutEffect, y eso no es algo que deba tomarse a la ligera.Una de las cosas que no me gustan de la validación de formularios HTML es que se ejecuta desde el momento en que carga la página. Por ejemplo, si vinculas a esto los estilos de estado de error, el formulario puede cargarse lleno de errores desde el inicio y verse intimidante para el usuario.
:user-invalid, que ayuda a evitar esto hasta cierto punto. Aunque tiene partes poco flexibles, así que según el caso de uso puede no ser suficiente.https://developer.mozilla.org/en-US/docs/Web/CSS/:user-inval...
Se puede esquivar con scripts, pero para ese punto esta función ya no aporta gran cosa. Creo que esta es la razón principal por la que no se ha adoptado más ampliamente.