Javascript non-intrusif, chapitre 5 : un exemple de formulaire amélioré en Javascript

Par Christian Heilmann

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 :

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 :

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 Alerte 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.

  1. 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.
  2. 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 classe c2 dans l'objet o
add
ajoute la classe c1 à l'objet o
remove
supprime la classe c1 de l'objet o
check
teste si la classe c1 est déjà appliquée à l'objet o et renvoie true ou false

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

Fiche technique

À propos de l'auteur

lang="en">Christian Heilmann
est un contributeur de Digital Web Magazine. La plupart de ses
publications sont écrites dans le métro, en voyageant
à travers Londres, parce qu’il n’y a simplement rien d’autre
à y faire. Un jour il aimerait remettre son propre livre
à ceux qui sont coincés là.

Articles du même auteur

Articles similaires

Voici la liste des dix articles les plus récents en rapport avec cet article :

Confirmé

JavaScript

© 2001-2016 Pompage Magazine et les auteurs originaux - Hébergé chez Nursit.com ! - RSS / ATOM - About this site