2 points par GN⁺ 2024-10-29 | 1 commentaires | Partager sur WhatsApp
  • Les formulaires HTML disposent déjà de fonctions de validation natives comme required, type="email", pattern, maxlength et setCustomValidity, mais leur usage réel reste faible au regard de l’étendue de ces possibilités
  • Le plus puissant, setCustomValidity, peut gérer une logique de validation arbitraire et des cas complexes, mais il n’est proposé que comme méthode DOM, ce qui s’accorde mal avec les composants déclaratifs
  • Dans des environnements comme React, aligner la validation avec la valeur initiale implique souvent d’entremêler useRef, useLayoutEffect et onChange, et la même logique de validation tend à être dupliquée entre le rendu initial et le gestionnaire de changement
  • Avec une abstraction sous forme d’attribut comme custom-validity, on pourrait exprimer au même endroit des validations dépendantes de l’état et de l’entrée, comme la vérification asynchrone de disponibilité d’un nom d’utilisateur ou la confirmation de mot de passe
  • La faible adoption de la validation native des formulaires tient moins à un manque de fonctionnalités qu’à un problème d’ergonomie de l’API ; si une API déclarative entrait dans la spécification HTML, son usage pourrait nettement progresser

Ce que la validation des formulaires HTML fournit déjà

  • La façon la plus simple d’empêcher une saisie vide consiste à ajouter l’attribut required
  • On peut imposer des contraintes de saisie de trois grandes façons
    • utiliser des types de champ comme type="email", type="number" ou type="url"
    • utiliser des attributs de contrainte comme pattern ou maxlength
    • utiliser la méthode DOM setCustomValidity du champ
  • setCustomValidity est le moyen le plus puissant, car il permet de traiter une logique de validation arbitraire et des cas complexes
  • Les deux premières approches peuvent être déclarées via des attributs HTML, alors que setCustomValidity exige un appel de méthode
  • Une ressource de référence sur la différence entre attributs et propriétés DOM est également liée : Attributes vs Properties

Là où setCustomValidity devient peu pratique

  • setCustomValidity n’est exposé que comme méthode, sans attribut HTML correspondant
  • Si on lui passe une chaîne, le champ devient invalide et le navigateur affiche cette chaîne comme raison de l’échec de validation
  • Si on lui passe une chaîne vide, le champ redevient valide en l’absence d’autres contraintes
  • Pour réimplémenter soi-même un comportement comme required, il faut appeler setCustomValidity à chaque changement de valeur
  • Toutefois, un champ est valide au départ ; si l’on clique sur le bouton d’envoi juste après avoir réinitialisé le composant, la soumission du formulaire peut passer
    • car même si la valeur est vide, le code de validation ne s’exécute pas tant qu’aucun événement de changement n’a eu lieu
    • pour couvrir aussi la valeur initiale, il faut donc exécuter la même validation au montage du composant

Le boilerplate qui s’accumule dans les composants déclaratifs

  • Dès qu’il faut aussi gérer la validation de la valeur initiale, le code devient vite répétitif et peu confortable
  • Trois problèmes apparaissent en particulier
    • la logique de validation est dupliquée entre le gestionnaire onChange et l’étape de rendu initial
    • le code de validation initiale est séparé de l’élément de saisie, ce qui réduit la cohésion et crée un risque de ne modifier qu’un seul côté
    • la combinaison useRef, useLayoutEffect et onChange devient excessive à mesure que le nombre de champs augmente, et encore plus déroutante quand seuls certains champs utilisent customValidity
  • Cette complexité est particulièrement visible lorsqu’on manipule une API impérative pure dans des composants déclaratifs
  • CustomValidity ne dispose d’aucun attribut de champ permettant de définir sa valeur de façon déclarative, contrairement aux attributs de validation natifs
  • Quand une API est pénible à utiliser, l’adoption peut rester faible même si la fonctionnalité est puissante ; c’est là la cause principale de la sous-utilisation de la validation native des formulaires

La pièce manquante : l’attribut custom-validity

  • La forme nécessaire serait un attribut déclaratif tel que custom-validity, directement définissable sur le champ
  • Dans un framework déclaratif, cet attribut permettrait de garder l’état de validation au plus près de l’élément de saisie
  • À l’heure actuelle, la spécification HTML ne contient pas de véritable custom-validity
  • Il est possible d’imiter ce comportement via une implémentation côté utilisateur à des fins de démonstration
  • Un exemple d’implémentation plus abouti est aussi fourni pour un composant de production

Validation asynchrone et dépendances entre champs

  • Dans une application réelle, la validation peut devenir plus complexe que de simples vérifications locales
  • Un champ de nom d’utilisateur doit vérifier côté serveur si le nom est déjà utilisé, et pendant la requête le formulaire ne doit pas être dans un état valide
  • L’exemple utilise required pour empêcher une saisie vide, puis rend le champ invalide via customValidity selon l’état de chargement et le résultat de la réponse
  • L’implémentation se compose de deux parties
    • l’état de la requête de vérification d’unicité du nom d’utilisateur est géré avec useQuery de react-query
    • un composant personnalisé <Input /> capable de recevoir une prop customValidity est utilisé
  • Cette approche permet d’exprimer dans un seul attribut l’ensemble du flux de validation asynchrone, y compris les états de chargement, d’erreur et de succès
  • Un autre exemple montre un formulaire qui demande de ressaisir le mot de passe, en traitant la validation de champs dépendants les uns des autres

Conclusion

  • setCustomValidity est un outil puissant capable de répondre à des besoins de validation variés
  • L’usage réel d’une fonctionnalité dépend non seulement de son existence, mais aussi d’une API qui la rende facile à utiliser
  • Une abstraction déclarative comme custom-validity permettrait d’utiliser plus naturellement la validation native des formulaires
  • On peut espérer qu’une telle fonctionnalité entre un jour nativement dans la spécification HTML

1 commentaires

 
GN⁺ 2024-10-29
Avis de Hacker News
  • La dernière fois que j’ai vérifié, les navigateurs web modernes ne permettaient toujours pas de styliser l’apparence des messages de validation HTML intégrés https://stackoverflow.com/questions/5328883/how-do-i-style-t...
    Ce serait moins gênant si Chrome et Firefox suivaient au moins les directives d’interface de la plateforme de l’OS, de sorte qu’ils ressemblent à quelque chose d’affiché par le système, comme les infobulles title="". Mais Chrome utilise une icône jaune/orange, du texte noir sur fond blanc et une bulle aux coins arrondis fixes, ce qui jure fortement avec l’esthétique du projet en cours
    Les anciennes versions de Chrome permettaient de styliser les messages de validation via des sélecteurs de pseudo-éléments préfixés par le fournisseur, mais après avoir supprimé cette fonctionnalité, ils ne l’ont jamais réintroduite. Ça rejoint ma liste arbitraire d’agacements du genre « donnez-nous une vraie combobox HTML native » ou « pourquoi a-t-on encore cette boîte Ctrl+clic pénible à utiliser au lieu d’une liste de cases à cocher »

    • Le problème ici n’est pas tant l’absence de style personnalisé en elle-même, mais le fait qu’il est assez facile de lire l’état validity natif d’une entrée et de l’afficher comme on veut
      Le vrai problème, c’est qu’il est délicat de s’abonner précisément aux changements de cet état de validité. Il existe bien des événements de validation, mais ils ne se déclenchent pas toujours. Si l’on modifie programmatiquement l’état du formulaire, par exemple avec un appel à form.reset() ou input.value = '...', ces événements ne sont pas déclenchés
      Je pense que c’est un bon sujet à étudier séparément et à proposer à la plateforme web
    • Je ne comprends pas très bien pourquoi on voudrait styliser ce genre de choses aussi finement. L’exprimer par la couleur ou la mise en page, très bien, mais à partir d’un certain point, l’utilisabilité prime sur le branding
    • Exact. Cela dit, on peut masquer le message par défaut et le remplacer par son propre message. On conserve tout de même les avantages de la validation de formulaire
    • Dans le même ordre d’idées, le style d’un select simple n’est pas aussi facile qu’il devrait l’être
  • Le plus gros gain, facile à implémenter mais sous-utilisé, consiste à employer des valeurs d’attribut type précises comme email, number ou url
    Sur mobile, cela peut afficher le clavier optimal et améliorer fortement l’expérience utilisateur

    • J’évite type=number et j’utilise plutôt type=text inputmode=numeric. On n’a pas les flèches dont la plupart des utilisateurs n’ont pas besoin pour saisir des nombres, et le clavier est aussi meilleur sur iOS
    • Il est étonnamment fréquent de voir des formulaires de connexion afficher « email » sans définir le bon type
    • J’ai très souvent vu la même chose avec les champs de saisie date. Chaque bibliothèque frontend a son propre widget de date, et beaucoup rendent très mal sur les petits écrans
      À cause de ce seul widget, il faut ajouter du JS et du CSS, et certains dépendent de jQuery. Bien sûr, certains sont très configurables, mais pour un coût minime on peut obtenir partout un widget correct, d’apparence native, qui répond généralement aux besoins, et qu’on peut oublier sans se soucier des mises à jour ou d’un CDN
    • Malheureusement, les champs de saisie number sont insuffisants et incohérents d’un navigateur à l’autre. Au final, je suis toujours revenu à une validation en JavaScript
    • Récemment, j’ai dû vérifier qu’on n’utilisait pas d’éléments de saisie plus complexes. Pour diverses raisons, il fallait contrôler la saisie avec un clavier à l’écran implémenté dans la page
      C’est déjà tout juste possible avec une entrée classique, mais les entrées spécialisées prennent en charge encore moins d’événements
  • Mon produit n’a pas passé l’audit d’accessibilité parce qu’il utilisait la validation native des formulaires HTML, et la recommandation officielle était d’implémenter notre propre couche de validation. Je suis d’accord avec cette recommandation.
    La validation native HTML a beaucoup de défauts et, franchement, la personnalisation visuelle n’est même pas mon principal souci. C’est toutefois assez proche du dernier clou dans le cercueil.
    Par exemple, si l’on veut afficher plusieurs erreurs par champ en une seule fois, la seule option est de concaténer des chaînes. Du genre : « Un chiffre est requis. Un symbole est requis. Doit faire plus de 10 caractères », ce qui donne une mauvaise expérience utilisateur et n’est pas bon non plus pour l’accessibilité. En effet, on ne peut pas naviguer dans une chaîne concaténée dans l’arbre d’accessibilité. Ce n’est pas un problème propre à une implémentation, mais à la spécification elle-même. Les utilisateurs n’aiment pas jouer à la chasse aux erreurs de validation, il faut donc pouvoir afficher plusieurs erreurs en une seule fois.
    La dépendance au navigateur est aussi un problème. On ne contrôle rien et les implémentations sont globalement médiocres. Chrome affiche une pop-up à la prise de focus ; c’est une pop-up, elle ressemble à une modale, et comme elle ne peut pas afficher toutes les erreurs du formulaire à la fois, elle est en soi peu accessible. Ne pas afficher des informations importantes dans une pop-up est une base de l’accessibilité, mais le navigateur n’a pas vraiment d’autre moyen de faire sans perturber le document réel.
    Les erreurs qui concernent l’ensemble du formulaire et pas un champ précis nécessitent toujours une validation custom. Par exemple : « Les champs A et B ne sont pas compatibles ». À ce stade, autant avoir une méthode de validation cohérente.
    Si vous avez une saisie custom qui ne correspond pas aux types d’entrée natifs, il faut ajouter un champ caché pour garder une certaine cohérence, ce qui casse aussi l’accessibilité. Corriger cela finit par être aussi difficile que de construire son propre système de validation compatible avec l’accessibilité.
    L’API customValidity est impérative et pénible à utiliser. À moins de vouloir des messages horribles du type « Ce champ est invalide », ne pas utiliser de validité custom n’est quasiment pas une option. C’est pour cela que la validation des formulaires HTML est mauvaise.

    • C’est effectivement ironique qu’une fonctionnalité fournie par le navigateur puisse ne pas satisfaire les exigences d’accessibilité. On nous a toujours appris que l’une des motivations pour utiliser la plateforme et respecter les éléments sémantiques était justement l’accessibilité.
      Je pense quand même qu’il vaut la peine d’exploiter le mécanisme de validation natif. Pour les messages d’erreur, il n’est pas nécessaire d’utiliser l’infobulle de validité native : on peut lire directement le ValidityState de input.validity, puis rendre les messages comme on le souhaite et afficher plusieurs erreurs si nécessaire.
      Les navigateurs peuvent s’améliorer, et en utilisant une approche standardisée on peut bénéficier de ces améliorations. Une conclusion du genre « si l’on n’utilise pas de pop-up, l’accessibilité est cassée » semble étrange, mais si l’objectif est de répondre à un audit, il faut peut-être en passer par là.
      Il existe aussi des techniques pour gérer les erreurs qui ne relèvent pas d’un champ précis. Dans mon projet, nous utilisons un composant HiddenValidationInput qui crée un champ invisible en lecture seule afin de rendre les erreurs custom qui ne sont rattachées à aucun autre champ. Il suffit de le rendre conditionnellement, et en pratique c’est assez agréable à utiliser.
      Je suis totalement d’accord sur le fait que l’API customValidity est impérative et pénible. J’ai écrit un article qui traite précisément ce point, et j’espère que cela s’améliorera avec le temps.
    • C’est intéressant, mais dans mon appli j’implémente toutes ces validations. La validation globale du formulaire est gérée côté serveur. En gros, on met plusieurs couches de validation et on exploite tout ce qui est possible.
      Le design grossier et peu accessible de Chrome est un problème de Chrome, donc je vais ouvrir un rapport de bug.
      Quand vous parlez de « spécification », c’est bien de ceci qu’il s’agit ? https://html.spec.whatwg.org/multipage/dom.html#concept-elem...
      Je ne vois pas de partie définissant comment les messages de validation doivent être affichés. Il n’est même pas indiqué qu’un message doit obligatoirement exister.
    • Je serais curieux de savoir pourquoi les auditeurs ont rendu ce jugement.
  • Honnêtement, les gens qui écrivent ce genre de spécifications semblent déconnectés de la réalité et ne pas utiliser eux-mêmes ce qu’ils spécifient. Ça fonctionne pour des choses très simples, mais dès que les formulaires commencent à évoluer, on finit par comprendre qu’il vaut mieux tout écrire soi-même.

    • Oui. Même une chose relativement courante comme une référence croisée avec un autre champ nécessite presque immédiatement du JS. Par exemple, si l’utilisateur a indiqué son pays, on peut valider le code postal qu’il vient de saisir ; sinon, il faut attendre qu’il choisisse un pays.
      À partir du moment où l’on commence à utiliser du JS, il devient beaucoup plus simple de tout faire en code plutôt que de bricoler avec les attributs de validation.
    • Oui. C’est très bien jusqu’au moment où l’on a besoin d’un sélecteur de date cross-browser ; à partir de là, il faut implémenter beaucoup de choses soi-même. C’est frustrant qu’après toutes ces années les formulaires HTML soient encore aussi primitifs.
    • La plupart des fonctionnalités de formulaire datent du début des années 90. Il est possible qu’elles ne travaillent pas dans le même millénaire que certains auteurs de la spécification.
  • Si une case à cocher a un libellé, j’aimerais qu’on mette un attribut for sur le libellé pour permettre d’activer ou de désactiver la case en cliquant dessus. C’est personnellement l’une des choses qui m’agacent le plus, même si je suis peut-être le seul.

    • On peut aussi envelopper l’input dans le label, et cela fonctionne. Je ne sais pas trop pourquoi les gens ont tendance à les séparer.
      Et je ne sais pas non plus pourquoi les navigateurs ont commencé à dissocier ces éléments. Selon moi, ce n’est pas le texte qui devrait contenir les cases à cocher et les boutons radio, mais les cases à cocher et les boutons radio qui devraient contenir le texte.
    • Vous n’êtes pas le seul. Sur les sites accessibles conformes à l’ADA/WCAG, c’est une fonctionnalité nécessaire.
  • Voici un exemple simple qui n’utilise pas React :
    https://developer.mozilla.org/en-US/docs/Web/API/HTMLObjectE...

    • J’ai trouvé très bizarre que, tout en parlant de validation HTML standard, tous les exemples soient montrés avec React. Si l’on veut enseigner le fonctionnement d’un standard, il ne faut pas prendre React comme valeur par défaut.
    • Aujourd’hui, il semble qu’on puisse faire la plupart de cela uniquement avec CSS. Il faut peut-être seulement un peu de JavaScript pour empêcher l’apparition de la pop-up par défaut.
  • La validation des formulaires HTML est excellente. Mais elle comporte un énorme piège
    Elle ne fonctionne pas dans Firefox pour Android
    https://bugzilla.mozilla.org/show_bug.cgi?id=1510450

    • J’utilise Firefox sur tous mes appareils mobiles, mais il est difficile de reprocher aux développeurs d’ignorer Firefox pour Android sur ce point. C’est une API que tous les autres navigateurs font fonctionner depuis 10 ans, et c’est à l’équipe Firefox de régler ce désordre
      Cela ne vaut pas la peine d’ignorer le standard pour moins de 1 % d’utilisateurs qui ne voient pas les messages d’erreur des contrôles affichés en rouge. Plus de sites web l’utiliseront, plus Mozilla subira de pression pour corriger son navigateur
      Ce n’est pas une API comme WebMIDI, implémentée par Chrome ou Safari avant la standardisation, mais une partie de la spécification HTML5 d’origine
    • Firefox pour Android a une base d’utilisateurs plus petite que Samsung Internet ou Opera, autour de 0,5 %. Passer du temps à le prendre en charge est quasiment une perte de temps
      C’est d’autant plus vrai quand on pense au peu de temps consacré à permettre aux personnes utilisant des logiciels d’accessibilité d’utiliser correctement les sites. À moins d’être prêt à parler aussi d’UC Browser, je ne pense pas que ce problème mérite vraiment d’être mentionné
    • En tant qu’utilisateur quotidien de Firefox sur Android, ce qui fait le plus mal est le retard par rapport aux standards. En particulier les problèmes liés à WebGL comme [1] et les petits désagréments comme [2]
      Cela dit, pouvoir utiliser uBlock Origin avec d’autres extensions reste un atout majeur
      [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1884282
      [2] https://bugzilla.mozilla.org/show_bug.cgi?id=1897707
    • C’est bien pire que « ça ne fonctionne pas ». La validation fonctionne, mais n’affiche aucune erreur
      Si la validation n’avait simplement pas été implémentée, cela aurait presque été préférable, mais là c’est vraiment catastrophique. Je l’ai découvert il y a quelques années après un débogage long et pénible, et je suis certain de ne pas avoir été le premier ni le dernier à vivre la même chose
    • Je ne m’en suis rendu compte que récemment, après être passé à Firefox sur Android. Pendant que je construisais une app, lorsque j’essayais de soumettre un champ obligatoire vide, il ne se passait littéralement rien
      Avant de chercher, je n’imaginais pas ce que j’avais pu rater, et c’était assez choquant. Faire sa propre validation, c’est acceptable, mais ça, ça devrait fonctionner
  • Il faut faire attention à ne pas en abuser
    J’ai récemment essayé d’obtenir un remboursement auprès de Groupon. L’entreprise chez qui j’avais acheté le Groupon avait changé de direction et refusait de reconnaître mon Groupon
    Le formulaire imposait une condition « au moins 15 mots », mais impossible de passer la validation, quoi que je fasse, et j’ai fini par devoir inspecter le HTML
    \w correspondait aux caractères de mot, \b aux limites de mot, \s aux espaces, et la ponctuation n’était tout simplement pas autorisée, littéralement

    • Je ne pense pas que ce soit un problème propre à la validation HTML. La même chose peut arriver avec n’importe quel type de validation. La même règle aurait pu être appliquée côté serveur, et dans ce cas il n’y aurait eu aucun espoir
    • C’est aussi un bon argument pour ce que je voulais dire. Les attributs de validité existants comme pattern sont intéressants, mais insuffisants
      Par exemple, l’exemple de « répétition du mot de passe » peut être implémenté avec l’attribut pattern sans setCustomValidity. Pour cela, il faut construire dynamiquement une expression régulière à partir de la première valeur saisie
      Je n’ai pas comparé les solutions pour éviter que l’article ne devienne trop long, mais l’idée centrale est qu’avec customValidity, la validation devient beaucoup plus claire et lisible. Ici, une meilleure API fait une grande différence
      La contrainte « au moins 15 mots » pourrait aussi être exprimée de façon bien plus lisible, par exemple avec value.split(/\s+/).length >= 15
  • Il y a une raison si c’est moins utilisé. Beaucoup de frameworks et de bibliothèques fournissent des fonctionnalités de validation robustes et stylables, certaines étant très sophistiquées et extensibles. Si l’on peut éviter de se compliquer la vie, mieux vaut ne pas s’infliger ça

    • De toute façon, il faut aussi implémenter soi-même la validation backend. Il y aura toujours quelqu’un pour manipuler le formulaire avec un navigateur bizarre, curl, ou un autre outil qui n’intègre pas la même validation de formulaire
      On ne peut pas faire confiance au client pour savoir s’il a validé les données, ni s’il l’a fait correctement ; le backend doit donc toujours valider les entrées et pouvoir réafficher le formulaire avec les erreurs de validation pour tous les champs
      La validation frontend sert seulement à aider l’utilisateur. Mais si l’on veut la styliser ou la déclencher côté backend à la soumission, on finit de toute façon par devoir implémenter son propre style
    • D’accord. J’essaie d’utiliser les fonctionnalités standards du navigateur quand c’est possible, mais la validation intégrée ne m’a jamais semblé en valoir la peine après examen
      Il y a trop de pièges, et pour prendre facilement en charge des vérifications plus complexes, on finit par utiliser une bibliothèque
      En plus, utiliser une bibliothèque ouvre parfois la possibilité de partager une partie du code de validation entre le frontend et le backend
      En particulier, cet article semble contourner un problème avec useLayoutEffect, ce qui n’est pas quelque chose à faire à la légère
  • Une des choses que je n’aime pas dans la validation des formulaires HTML, c’est qu’elle s’exécute dès le chargement de la page. Par exemple, si l’on y associe les styles d’état d’erreur, le formulaire se charge dès le départ rempli d’erreurs, ce qui peut paraître intimidant pour l’utilisateur

    • Il existe la pseudo-classe :user-invalid, qui permet d’éviter cela dans une certaine mesure. Elle manque toutefois de flexibilité, et peut ne pas suffire selon le cas d’usage
      https://developer.mozilla.org/en-US/docs/Web/CSS/:user-inval...
    • C’est l’une des principales raisons pour lesquelles les gens ont commencé à utiliser JavaScript : n’afficher les erreurs qu’après que le formulaire a été « touché », ou juste avant la soumission
    • Je n’ai jamais compris pourquoi cela avait été choisi comme expérience par défaut. La plupart des utilisateurs n’aiment pas que tous les éléments du formulaire leur crient dessus alors qu’ils n’ont même pas encore eu l’occasion d’agir
      On peut le contourner avec des scripts, mais à ce stade cette fonctionnalité n’apporte plus grand-chose. Je pense que c’est la principale raison pour laquelle elle n’est pas adoptée plus largement