Javascript non-intrusif, chapitre 5 : un exemple de formulaire amélioré en Javascript
Cet article est le dernier d'une série qui en compte 5.
C'est une très bonne idée d'améliorer les formulaires grâce à Javascript. Il n'y a rien de plus frustrant que de remplir un formulaire, de l'envoyer au serveur, d'attendre que la page se recharge et d'avoir un message expliquant qu'on a oublié de remplir tel ou tel champ.
C'est bien mieux pour l'utilisateur d'avoir un retour immédiat avant d'envoyer les données, et pour nous d'avoir moins de trafic serveur.
Formulaires et Javascript - puissant mais trompeur
En terme d'amélioration de l'utilisabilité, Javascript peut rendre un formulaire bien meilleur, mais il y a quelques détails sur lesquels nous devons être vigilants :
- Il faudra quand même effectuer une validation côté serveur, pour éviter que des données invalides - envoyées par des utilisateurs ayant désactivé Javascript - ne soient transmises au serveur.
- Il n'existe pas de « script de formulaire générique ». Chaque formulaire est unique et suit un certain nombre de règles de validation. Essayer de s'adapter à chaque cas donnerait un script bancal et lent, c'est pourquoi il vaut mieux réutiliser des bibliothèques ou des scripts existants. Cela améliorera les temps d'exécution et facilitera la maintenance.
- On essaiera de conserver les mêmes techniques de validation du Javascript côté client et côté serveur. Nous pourrions utiliser des classes pour la validation côté client, comme par exemple
<input class="required" ... />
, mais ces infos ne seront pas transmises lorsque le formulaire sera envoyé.
Le balisage de notre formulaire
<form action="formsend.php" method="post" onsubmit="return checkform(this);"> <p> <input type="hidden" name="required" id="required" value="name,surname,email,tac,msg,contactform" /> <label for="name">Prénom</label> <input type="text" name="name" id="name" /><span>*</span> </p> <p> <label for="surname">Nom</label> <input type="text" name="surname" id="surname" /><span>*</span> </p> <p> <label for="email">E-mail</label> <input type="text" name="email" id="email" /><span>*</span> </p> <p> <label for="phone">Numéro de téléphone</label> <input type="text" name="phone" id="phone" /> </p> <p> <label for="contactform">Moyen de contact préféré</label> <select id="contactform" name="contactform"> <option value="">Veuillez choisir</option> <option value="p">téléphone</option> <option value="e">e-mail</option> </select><span>*</span> </p> <p> <label for="msg">Votre message</label> <textarea name="msg" id="msg"></textarea><span>*</span> </p> <p> <input type="checkbox" name="tac" id="tac" /> J'ai lu les <label for="tac">conditions générales</label> et suis d'accord avec elles.</label><span>*</span> </p> <p> <input type="submit" value="Envoyer" /> </p> </form>
C'est un formulaire parfaitement balisé, contenant des éléments label
facilitant l'accès aux utilisateurs non-voyants ainsi qu'à ceux qui pourraient avoir des soucis pour cocher des cases à l'aide de leur dispositif de pointage. Pour la validation, nous utilisons un champ caché appelé required
qui liste tous les champs requis dans une liste d'éléments séparés par des virgules. Ceci est devenu depuis des années un standard dans les scripts de validation de formulaires (vous vous souvenez de formmail.pl
?).
Les règles de vérification qu'on veut appliquer sont les suivantes :
- S'assurer que chaque champ requis a bien été rempli, sélectionné ou coché ;
- S'assurer que l'e-mail donné est au bon format.
La plupart des scripts de validation dressent une liste des noms des champs requis ayant un problème dans une alerte Javascript (alert
). Cela a un sens si le formulaire est gigantesque et complexe, mais la fenêtre d'alerte est agaçante et assez moche. Nous allons donc essayer une autre approche pour ce petit formulaire : chaque champ erroné possédera une petite icône d'avertissement et un fond rouge et on affichera un message au-dessus du bouton d'envoi qui expliquera qu'il y a des erreurs.
Notre script checkform()
On commence notre script en vérifiant la disponibilité du DOM et l'existence d'un champ ayant l'ID required
. Si ce n'est pas le cas, on revient au document et c'est le script PHP de secours formsend.php
qui prendra en charge le reste.
function checkform(of) { if(!document.getElementById || !document.createTextNode){return;} if(!document.getElementById('required')){return;}
On continue en définissant toutes les variables utilisées dans l'affichage des erreurs et en séparant tous les champs requis dans un tableau.
var errorID='errormsg'; var errorClass='error' var errorMsg='Merci d\'entrer ou de modifier le contenu des champs marqués d'un '; var errorImg='img/alert.gif'; var errorAlt='Erreur'; var errorTitle='Ce champ est erroné !'; var reqfields=document.getElementById('required').value.split(',');
Comme nous allons ajouter un élément dont l'ID sera défini dans errorID
et qu'une image sera ajoutée à chaque champ erroné, il faut qu'on efface ceux déjà présents au cas où le script serait exécuté une seconde fois. Si on néglige de faire cela, on se retrouvera avec plusieurs messages et plusieurs images.
// Nettoyage des anciens messages // s'il y a un ancien champ errormessage, on le supprime if(document.getElementById(errorID)) { var em=document.getElementById(errorID); em.parentNode.removeChild(em); } // supprime les anciennes images et classes des champs requis for(var i=0;i<reqfields.length;i++) { var f=document.getElementById(reqfields[i]); if(!f){continue;} if(f.previousSibling && /img/i.test(f.previousSibling.nodeName)) { f.parentNode.removeChild(f.previousSibling); } f.className=''; }
On peut maintenant faire ce qu'on vient de défaire. On boucle sur les champs requis et on commence par tester que le champ existe bel et bien. Si ce n'est pas le cas, on saute une des parties de la boucle. C'est juste pour éviter les messages d'erreur, le balisage réel de notre formulaire possédant tous les champs requis.
// boucle sur les champs requis for(var i=0;i<reqfields.length;i++) { // vérifie que le champs requis est présent var f=document.getElementById(reqfields[i]); if(!f){continue;}
On vérifie ensuite chaque champ en fonction de son type. Pour les champs de texte (textarea
et text
) il faut vérifier la valeur value
, pour les cases à cocher il faut vérifier l'attribut checked
et pour les listes de sélection il faut vérifier qu'un selectedIndex
a bien été défini et qu'il est supérieur à 0.
Si l'un des champs est erroné, on l'envoie en tant qu'objet à la méthode cf_adderr()
. Le champ d'e-mail est un cas particulier puisqu'il faut en plus vérifier que son format est valide. Cette vérification est effectuée par une autre méthode appelée cf_isEmailAddr()
utilisant les expressions rationnelles.
// teste si le champ requis est erroné, // en fonction de son type switch(f.type.toLowerCase()) { case 'text': if(f.value=='' && f.id!='email'){cf_adderr(f)} // email est un champ spécial nécessitant une vérification if(f.id=='email' && !cf_isEmailAddr(f.value)){cf_adderr(f)} break; case 'textarea': if(f.value==''){cf_adderr(f)} break; case 'checkbox': if(!f.checked){cf_adderr(f)} break; case 'select-one': if(!f.selectedIndex && f.selectedIndex==0){cf_adderr(f)} break; } }
Si l'un des tests ci-dessus déclenche un rapport d'erreur, cf_adderr()
génère un message d'erreur (un div
dont l'ID est errorid
). On retourne donc à la procédure d'envoi du formulaire seulement si cet élément n'existe pas.
return !document.getElementById(errorID);
Ceci clôt la fonction principale ; maintenant nous devons nous concentrer sur les méthodes utilisées, la première étant celle qui ajoute les messages et les images d'erreur.
/* Méthodes outils */ function cf_adderr(o) {
On crée l'image, on définit son texte alternatif et son titre et on l'insère avant l'élément. On applique la classe CSS stockée dans errorClass
à l'élément afin de le colorier.
// crée l'image, l'ajoute et colorie les champs erronés var errorIndicator=document.createElement('img'); errorIndicator.alt=errorAlt; errorIndicator.src=errorImg; errorIndicator.title=errorTitle; o.className=errorClass; o.parentNode.insertBefore(errorIndicator,o);
On vérifie ensuite s'il existe déjà un message d'erreur et on le crée si nécessaire. Une fois cet élément créé, cette condition ne sera plus exécutée.
// Vérifie qu'il n'y a pas de message d'erreur if(!document.getElementById(errorID)) { // crée "errormessage" et l'insère avant le bouton d'envoi var em=document.createElement('div'); em.id=errorID; var newp=document.createElement('p'); newp.appendChild(document.createTextNode(errorMsg)) // duplique et insère le message d'erreur newp.appendChild(errorIndicator.cloneNode(true)); em.appendChild(newp);
On trouve le bouton d'envoi (en examinant le type de chaque élément input
) et on insère le nouveau message avant son élément parent (c'est-à-dire le paragraphe dans lequel le bouton d'envoi se situe).
// trouve le bouton d'envoi for(var i=0;i<of.getElementsByTagName('input').length;i++) { if(/submit/i.test(of.getElementsByTagName('input')[i].type)) { var sb=of.getElementsByTagName('input')[i]; break; } } if(sb) { sb.parentNode.insertBefore(em,sb); } } }
Enfin, nous avons besoin de la méthode qui vérifie que l'adresse e-mail saisie est au bon format :
function cf_isEmailAddr(str) { return str.match(/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/); } }
Voilà, c'est tout ! Jetez un oeil à la page d'exemple pour voir par vous-même comment tout cela fonctionne.
Pourquoi ne pas essayer ?
Il vous suffit de télécharger notre exemple, et de réaliser les tâches fixées. Suivez les liens vers les solutions pour voir une des solutions possibles. Le code Javascript est directement intégré dans le HTML de mes exemples, alors qu'il devrait être inséré dans un fichier externe. J'ai en effet tout mis dans un seul fichier pour simplifier votre lecture.
- Modifiez le code pour afficher une liste de tous les champs posant problème dans
errormessage
. Récupérez le test de leurs libellés à afficher. Voir la solution. - Ajoutez un lien vers tous les éléments qui posent problème (cf. premier exercice). Voir la solution.
Annexe A : séparation du CSS et du Javascript
Vous avez sans doute remarqué dans les exemples précédents que nous n'avons pas fait exactement ce que nous prêchions.
Notre erreur
Bon, d'accord, nous avons séparé la structure du comportement, mais nous avons aussi défini la présentation via Javascript, ce qui est à peu près aussi mauvais d'un point de vue sémantique.
Nous avons défini la présentation en accédant à la propriété style
des éléments :
if(!document.getElementById('errormsg')){ var em=document.createElement('p'); em.id='errormsg'; em.style.border='2px solid #c00'; em.style.padding='5px'; em.style.width='20em'; em.appendChild(document.createTextNode('Merci d\'entrer ou de modifier le contenu des champs marqués d'un ')) i=document.createElement('img'); i.src='img/alert.gif'; i.alt='Erreur'; i.title='Ce champ est erroné !'; em.appendChild(i); }
Cela signifie que si nous souhaitions modifier l'aspect de nos améliorations, il nous faudrait modifier le Javascript, ce qui n'est ni pratique ni très malin.
Il existe en Javascript plusieurs façons d'appliquer à un élément un style défini en CSS. Une première méthode serait d'appliquer un ID à l'élément en changeant son attribut ID. Une façon beaucoup plus flexible est d'appliquer une classe (class
) à l'élément, d'autant que les éléments peuvent avoir plus d'une classe.
Syntaxe de classes multiples
Pour un élément qui possède plus d'une classe, nous séparons simplement les noms de classe avec une espace : <div class="special highlight kids">
. Cela fonctionne dans la plupart des navigateurs modernes, mais certains affichent parfois quelques bugs. IE sur Mac n'apprécie pas vraiment le fait d'avoir plusieurs classes quand une des classes contient le nom d'une des autres, et agit bizarrement quand l'attribut class
commence ou se termine par une espace.
Appliquer des classes via Javascript
Pour ajouter une classe à un élément donné, nous devons changer son attribut className
. Par exemple, si nous voulons modifier des éléments de navigation uniquement lorsque Javascript et le DOM sont activés, nous pouvons faire ceci :
HTML : <ul id="nav"> [...] </ul> Javascript : if(document.getElementById && document.createTextNode) { if(document.getElementById('nav')) { document.getElementById('nav').className='isDOM'; } }
Cela nous permet de définir deux états différents dans notre feuille de style :
ul#nav{ [...] } ul#nav.isDOM{ [...] }
Toutefois, cette méthode écraserait toute autre classe appliquée à l'élément. C'est pourquoi il nous faut vérifier si une classe est déjà appliquée puis ajouter la nouvelle précédée d'une espace si tel est le cas :
if(document.getElementById && document.createTextNode) { var n=document.getElementById('nav'); if(n) { n.className+=n.className?' isDOM':'isDOM'; } }
La même vérification s'impose quand on souhaite supprimer une classe qui a été ajoutée de façon dynamique, puisque certains navigateurs ne supportent pas de définition telle que class="foo bar "
. Cela peut être assez agaçant, et il est plus simple de réutiliser une fonction qui va faire ça pour nous :
function jscss(a,o,c1,c2) { switch (a){ case 'swap': o.className=!jscss('check',o,c1)?o.className.replace(c2,c1): o.className.replace(c1,c2); break; case 'add': if(!jscss('check',o,c1)){o.className+=o.className?' '+c1:c1;} break; case 'remove': var rep=o.className.match(' '+c1)?' '+c1:c1; o.className=o.className.replace(rep,''); break; case 'check': return new RegExp('\\b'+c1+'\\b').test(o.className) break; } }
Cette fonction requiert quatre paramètres :
a
- définit l'action qui doit être faite par la fonction
o
- l'objet en question
c1
- le nom de la première classe
c2
- le nom de la seconde classe
Les actions possibles sont les suivantes :
swap
- remplace la classe
c1
par la classec2
dans l'objeto
add
- ajoute la classe
c1
à l'objeto
remove
- supprime la classe
c1
de l'objeto
check
- teste si la classe
c1
est déjà appliquée à l'objeto
et renvoietrue
oufalse
Prenons un exemple pour comprendre comment l'utiliser. Nous voulons que tous les titres de rang 2 puissent déclencher l'affichage et le masquage des éléments qui les suivent. Une classe doit donc être appliquée au titre pour indiquer qu'il s'agit d'un élément dynamique et à l'élément suivant pour le cacher. Une fois que le titre est activé, on lui applique un autre style et l'élément qui le suit doit être affiché. Pour rendre le tout encore plus intéressant, nous désirons appliquer un effet de survol sur le titre.
CSS : .hidden{ display:none; } .shown{ display:block; } .trigger{ background:#ccf; } .open{ background:#66f; } .hover{ background:#99c; } JS : function collapse() { // vérifie si DOM est activé, s'arrête sinon if(!document.createTextNode){return;} // crée un nouveau paragraphe expliquant qu'on peut cliquer // sur les titres var p=document.createElement('p'); p.appendChild(document.createTextNode('Cliquez sur les titres pour afficher ou masquer cette section')); // boucle sur tous les titres de rang 2 var heads=document.getElementsByTagName('h2'); for(var i=0;i<heads.length;i++) { // sélectionne l'élément suivant (la boucle est requise // à cause des problèmes d'espaces) var tohide=heads[i].nextSibling; while(tohide.nodeType!=1) { tohide=tohide.nextSibling; } // masque l'élément en appliquant la classe 'hidden' et // montre que le titre est cliquable en appliquant // la classe 'trigger' cssjs('add',tohide,'hidden') cssjs('add',heads[i],'trigger') // stocke l'élément à masquer dans un attribut heads[i].tohide=tohide; // ajoute la classe 'hover' lorsque la souris passe sur le titre heads[i].onmouseover=function() { cssjs('add',this,'hover'); } // enlève la classe 'hover' lorsque la souris quitte le titre heads[i].onmouseout=function() { cssjs('remove',this,'hover'); } // si l'utilisateur active le titre heads[i].onclick=function() { // teste si la classe 'hidden' est déjà appliquée à l'élément suivant if(cssjs('check',this.tohide,'hidden')) { // si c'est le cas, la remplace par 'shown' et remplace // la classe de titre par 'open' cssjs('swap',this,'trigger','open'); cssjs('swap',this.tohide,'hidden','shown'); } else { // et vice versa cssjs('swap',this,'open','trigger'); cssjs('swap',this.tohide,'shown','hidden'); } } // insère le nouveau paragraphe avant le premier h2 document.body.insertBefore(p,document.getElementsByTagName('h2')[0]); } function cssjs(a,o,c1,c2) { [...] } } window.onload=collapse;
Cliquez ici pour le voir en action.
Nous avons ainsi séparé avec succès la structure, la présentation et le comportement tout en créant un effet relativement complexe. La maintenance de cet effet est facile, et ne nécessite aucune connaissance en Javascript.
Enregistrez css.js pour votre usage personnel en sauvegardant le lien suivant sur votre ordinateur. css.js
Annexe B : articles et sites Internet utiles
- Les spécifications DOM du W3C DOM.
- BrainJar.com (Mike Hall) a écrit un article concis à propos de la gestion des événements.
- Quirksmode.org (Peter-Paul Koch) est sans aucun doute l'une des ressources sur Javascript les plus recherchées et les plus compactes que l'on puisse trouver aujourd'hui. Ce site couvre aussi les problèmes liés aux navigateurs ainsi que divers sujets plus théoriques concernant Javascript.
- Scott Andrew a développé une fonction d'ajout d'événements très pratique.
- Kryogenix.org (Stuart Langridge) dispose d'exemples de scripts non-intrusifs très intéressants.
- DOM vs. innerHTML (Tim Scarfe) parle des avantages et des inconvénients de chacun d'eux.
http://www.onlinetools.org/articles/unobtrusivejavascript/chapter5.html