2 puntos por GN⁺ 2024-10-29 | 1 comentarios | Compartir por WhatsApp
  • Los formularios HTML ya cuentan con funciones nativas de validación como required, type="email", pattern, maxlength y setCustomValidity, 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, useLayoutEffect y onChange, 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" y type="url"
    • usar atributos de restricción como pattern y maxlength
    • usar el método del DOM setCustomValidity del input
  • setCustomValidity es 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 setCustomValidity se 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

  • setCustomValidity se 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 a setCustomValidity cada 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 onChange y 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, useLayoutEffect y onChange se vuelve excesiva a medida que aumentan los inputs, y resulta todavía más confusa cuando solo algunos usan customValidity
  • Esta complejidad se vuelve especialmente evidente al manejar una API imperativa pura dentro de componentes declarativos
  • CustomValidity no 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-validity que 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-validity real en la HTML 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 required para impedir entradas vacías y, según el estado de carga y el resultado de la respuesta, pone el input en estado invalid mediante customValidity
  • La implementación se compone de dos partes
    • el estado de la solicitud para verificar la unicidad del nombre de usuario se gestiona con useQuery de react-query
    • se utiliza un componente personalizado <Input /> que puede recibir la prop customValidity
  • 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

  • setCustomValidity es 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-validity harí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

 
GN⁺ 2024-10-29
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?”

    • El problema aquí no es tanto la falta de estilos personalizados en sí, sino que es bastante fácil leer el estado validity nativo 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() o input.value = '...', esos eventos no se activan.
      Creo que este sería un buen tema para investigar por separado y proponer a la plataforma web.
    • No entiendo muy bien por qué alguien querría estilizar incluso esto. Está bien expresarlo con colores o layout, pero a partir de cierto punto la usabilidad importa más que el branding.
    • Exacto. Aunque también se puede ocultar el mensaje predeterminado y reemplazarlo por uno propio. Aun así, se conservan los beneficios de la validación de formularios.
    • En ese sentido, estilizar un select simple tampoco es tan fácil como debería ser.
  • Lo más grande, fácil de implementar y aun así poco usado es emplear valores específicos del atributo type, como email, number o url.
    En móviles, puede mostrar el teclado óptimo y mejorar mucho la experiencia de usuario.

    • Evito type=number y uso en su lugar type=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.
    • Sorprende la cantidad de sitios que muestran “email” en el formulario de login y aun así no configuran el type correcto.
    • También he visto muchísimo esto con inputs de 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.
    • Lamentablemente, los inputs de tipo number se quedan cortos y no son consistentes entre navegadores. Al final siempre terminé volviendo a la validación con JavaScript.
    • Últimamente tuve que asegurarme de no usar elementos de entrada más complejos. Por varios motivos necesitábamos controlar la entrada con un teclado en pantalla implementado dentro de la página.
      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 customValidity es 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.

    • Es definitivamente irónico que una funcionalidad provista por el navegador pueda no cumplir con los requisitos de accesibilidad. Siempre nos enseñaron que una de las razones para usar la plataforma y seguir los elementos semánticos era la accesibilidad.
      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 ValidityState de input.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 HiddenValidationInput que 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 customValidity es imperativa y engorrosa. Escribí un artículo que trata justamente esa parte, y espero que esto también mejore con el tiempo.
    • Interesante, pero en mi app implemento toda esa validación. La validación de todo el formulario se maneja en el servidor. Básicamente es un enfoque con varias capas de validación, usando todo lo que se pueda.
      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.
    • Me da curiosidad saber por qué los auditores lo evaluaron así.
  • 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.

    • Sí. Incluso algo relativamente común como hacer referencias cruzadas con otros campos requiere JS casi de inmediato. Por ejemplo, si el usuario especificó el país, puedes validar el código postal que acaba de escribir; si no, tienes que esperar a que elija el país.
      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.
    • Sí. Es genial hasta el momento en que necesitas un selector de fechas cross-browser; desde ese punto en adelante tienes que implementar muchas cosas por tu cuenta. Es frustrante que, después de tantos años, los formularios HTML sigan siendo tan primitivos.
    • La mayor parte de la funcionalidad de formularios viene de principios de los 90. Puede que no estemos trabajando en el mismo milenio que algunos de los autores de la especificación.
  • Si un checkbox tiene etiqueta, me gustaría que le pusieran el atributo for al 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.

    • También funciona envolver el input con el label. 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.
    • No eres solo tú. En los sitios accesibles que cumplen con ADA/WCAG, es una funcionalidad necesaria.
  • Aquí hay un ejemplo simple que no usa React:
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...

    • Me pareció muy raro que, hablando de validación HTML estándar, todos los ejemplos se mostraran con React. Si quieres enseñar cómo funciona el estándar, no deberías asumir React como predeterminado.
    • Hoy en día parece que la mayor parte se podría hacer solo con CSS. Tal vez solo haga falta JavaScript para impedir que aparezca el popup predeterminado.
  • 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

    • Uso Firefox en todos mis dispositivos móviles, pero en este tema es difícil culpar a los desarrolladores por ignorar Firefox para Android. Es una API que todos los demás navegadores hacen funcionar desde hace 10 años, y la responsabilidad de ordenar este lío es del equipo de Firefox.
      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.
    • Firefox para Android tiene una base de usuarios menor que Samsung Internet u Opera, alrededor de 0.5%. Dedicar tiempo a soportarlo es casi un desperdicio.
      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.
    • Como alguien que usa Firefox a diario en Android, lo que más duele es no estar al día con los estándares. En especial problemas relacionados con WebGL como [1] y molestias menores como [2].
      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
    • Es mucho peor que “no funciona”. La validación funciona, pero no muestra ningún error en absoluto.
      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.
    • Recién me di cuenta de esto después de cambiarme hace poco a Firefox en Android. Mientras hacía una app, intentaba enviar un campo obligatorio vacío y literalmente no aparecía nada.
      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.
    \w era carácter de palabra, \b era límite de palabra, \s era espacio en blanco, y literalmente no permitía ningún signo de puntuación.

    • No creo que este sea un problema exclusivo de la validación HTML. Pasa lo mismo con cualquier tipo de validación. Las mismas reglas podrían haberse aplicado en el servidor, y en ese caso no habría habido esperanza.
    • Esto también es una buena prueba del punto que quería plantear. Los atributos de validación existentes como pattern son geniales, pero no son suficientes.
      Por ejemplo, el caso de “repetir contraseña” se puede implementar con el atributo pattern incluso sin setCustomValidity. 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 customValidity la 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.

    • De todos modos también hay que implementar validación en el backend. Siempre habrá alguien que intente manipular el formulario con un navegador raro, curl u 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.
    • Estoy de acuerdo. Intento usar funciones estándar del navegador cuando es posible, pero la validación integrada nunca me ha parecido valiosa después de revisarla.
      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.

    • Existe la pseudoclase :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...
    • Esta es una de las principales razones por las que la gente empezó a usar JavaScript: para mostrar errores solo después de que el formulario haya sido “tocado” o justo antes de enviarlo.
    • Nunca entendí por qué se eligió esto como experiencia predeterminada. A la mayoría de los usuarios no les gusta que todos los elementos del formulario les griten cuando todavía ni siquiera tuvieron oportunidad de hacer nada.
      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.