Validation avancée des formulaires

Openweb.eu.org > Articles  > Validation avancée des formulaires

Abstract

La simple mise en place de formulaires sur un site peut devenir une opération assez complexe dès lors que l’on souhaite effectuer des contrôles sur les données saisies par les utilisateurs. Pour effectuer ce type de contrôles, deux possibilités nous sont offertes :

  • Vérifier les données côté serveur après validation du formulaire ;
  • Vérifier les données côté client avant transmission des données au serveur.

Dans cet article, nous allons montrer l’intérêt que peut apporter le Modèle Objet de Document (DOM) pour effectuer des contrôles côté client. Cette deuxième solution ne devra toutefois pas nous faire oublier la nécessité absolue d’effectuer dans le même temps des contrôles côté serveur. Ces derniers permettront d’une part d’éviter la soumission complète des données dans un format invalide, et d’autre part nous permettront d’assurer l’accessibilité de nos formulaires aux utilisateurs n’ayant pas la possibilité ou ayant fait le choix de ne pas activer Javascript sur leur poste de travail.

Si la solution proposée ici est un plus par rapport aux indispensables contrôles côté serveur, elle ne manque pas d’intérêt : elle permet d’éviter des échanges de données fastidieux avec le serveur, d’accélérer la validation des formulaires, et également d’enrichir et de faciliter la saisie pour les utilisateurs.

Mais avant de commencer, que devons nous savoir :

Article

Principes

Le traitement principal va consister à associer des classes prédéfinies sur les éléments à contrôler, ces classes correspondant aux noms des fonctions à exécuter pour tester les valeurs des champs.

Soit un champ de formulaire "prénom" devant obligatoirement être renseigné ;

<p>
  <label for="prenom">Prénom :</label>
  <input type="text" name="prenom" id="prenom">
</p>

et une fonction "isNotEmpty" qui vérifie si une chaîne est vide ou pas ;

function isNotEmpty (s) {
  return s!="";
}

La mise en place du contrôle sur le caractère obligatoire se limitera à la spécification de la classe "isNotEmpty" sur un élément label associé explicitement au champ "prénom" :

<p>
  <label for="prenom" class="isNotEmpty">Prénom :</label>
  <input type="text" name="prenom" id="prenom">
</p>

Note : Un label est dit "explicite" lorsqu’il possède un attribut "for" dont la valeur correspond à la valeur de l’attribut "id" du champ qui lui est associé. Un label qui inclut directement le champ est dit "implicite". Bien que tout à fait valide au niveau formel, les associations implicites ne sont pas supportées par l’ensemble des agents utilisateurs du moment dont Internet Explorer. Ce défaut d’implémentation nous encourage à nous limiter aux associations explicites.

Pourquoi choisir les labels comme support des classes de contrôle ?

Tout script de contrôle doit gérer un certain nombre de messages en cas d’erreur sur la saisie. Ces messages ont pour objectif de renseigner l’utilisateur sur la nature de l’erreur et de lui préciser clairement le champ fautif. Dans le cadre d’un script mutualisé, la spécification du champ en cause n’est pas toujours simple à gérer. Le rôle des éléments label étant de donner une information textuelle pour chacun des champs, il nous semble logique et pertinent d’exploiter leurs contenus pour fournir cette information.
La mise en oeuvre des classes sur ces éléments permet de nous assurer qu’ils seront systématiquement mis en place faute de quoi le contrôle ne pourra être réalisé.

La constitution des messages reposera sur la transformation d’une certaine séquence de caractères (%s) présente au sein des messages génériques du type "le champ %s doit être renseigné" par le contenu textuel du label. Sur l’exemple du champ obligatoire "prénom" nous obtiendrons ainsi une alerte de la forme "Le champ prénom doit être renseigné".

Ce processus repose sur l’implémentation de labels courts et explicites contenant une description pertinente des champs ce qui permet d’améliorer sensiblement l’ergonomie et l’accessibilité des formulaires mis en oeuvre.

Mise en place d’un formulaire

Pour commencer, il nous faut créer un formulaire contenant au minimum l’ensemble des éléments nécessaires : une balise form, des éléments label associés explicitement à des contrôles et un véritable bouton de soumission.

<form action="">
  <p>Les champs marqués d'une * doivent obligatoirement être renseignés.</p>
  <fieldset>
    <legend>Votre identité</legend>
    <p>
      <label for="nom">* Nom :</label>
      <input type="text" name="nom" id="nom">
    </p>
    <p>
      <label for="prenom">* Prénom :</label>
      <input type="text" name="prenom" id="prenom">
    </p>
    <p>
      <label for="age">Age :</label>
      <input type="text" name="age" id="age">
    </p>
  </fieldset>
  <fieldset>
    <legend>Vos identifiants de connexion</legend>
    <p>
      <label for="login">* Nom d'utilisateur :</label>
      <input type="text" name="login" id="login">
    </p>
    <p>
      <label for="password">* Mot de passe :</label>
      <input type="text" name="password" id="password">
    </p>
    <p>
      <label for="confirmpass">* Confirmation du mot de passe :</label>
      <input type="text" name="confirmpass" id="confirmpass">
    </p>
  </fieldset>
  <p>
    <input type="checkbox" name="newsletter" id="newsletter">
    <label for="newsletter">
      S'inscrire à la <span lang="en">newsletter</span>
    </label>
  </p>
  <p><input type="submit" value="Enregistrer votre profil"></p>
</form>

Interception de l’événement onsubmit

La première étape pour mettre en oeuvre la validation du formulaire est d’intercepter l’événement déclenché juste avant que celui-ci ne soit soumis.
Nous allons commencer par intercepter cet événement en définissant l’attribut HTML onsubmit.

<form action="" onsubmit="return formControlListener(this);">
...
</form>

Petit rappel :

  • en cas d’échec sur la validation des données, la fonction formControlListener doit retourner le booléen false pour empêcher que le formulaire ne soit envoyé ;
  • le mot clef this permet de passer l’objet formulaire en paramètre à la fonction.

Initialisation des variables

Pour partir d’un bon pied, commençons par recenser l’ensemble des variables qui va nous être nécessaire :

  • un booléen précisant l’état courant de la validation qui sera utilisé comme valeur de retour de la fonction. La validation devant s’arrêter à la première erreur de saisie, l’ensemble des contrôles ne sera exécuté que si cette variable à la valeur true.
  • une variable tableau contenant l’ensemble des classes devant déclencher un contrôle ainsi que les messages d’erreurs associés. Pour cet exercice nous allons limiter nos contrôles aux valeurs de champs :
    • non vide ;
    • représentant un entier.
  • Pour tester les différentes classes, nous allons avoir besoin de récupérer l’ensemble des labels grâce à la méthode getElementsByTagName. Cette méthode définie par le DOM permet de récupérer au sein d’un objet tous les éléments dont le nom correspond à la chaîne passée en paramètre. Cette méthode retourne la liste des éléments concernés sous forme de tableau javascript.
  • L’objectif étant de contrôler les données saisies, nous allons avoir besoin d’une variable faisant référence au champ de formulaire en cours de validation pour récupérer sa valeur. Pour lever toutes ambiguïtés quant à l’origine de cette variable, nous allons la déclarer au niveau local en début de fonction.

Après cette première étape, notre fonction devient :

function formControlListener(nForm) {
  var bIsValide = true;
  var aFormCtrlSchemes = [["isNotEmpty","Le champ \"%s\" doit être renseigné."],
    ["isInt","Le champ \"%s\" ne correspond pas à un entier valide."]];
  var cLabels = nForm.getElementsByTagName("label");
  var nField;
  return bIsValide;
}

Les classes devant correspondre aux noms des fonctions réalisant le contrôle, nous pouvons en profiter pour les initialiser également.

function isNotEmpty() {}
function isInt() {}

Note à propos de la création de tableaux :
L’initialisation d’une variable de type tableau par l’utilisation de [] est identique à une initialisation par l’opérateur new associée au constructeur Array().

Ainsi l’écriture

var w = [['x', 'y'],'z'];

est strictement équivalente à

var w = new Array();
w[0] = new Array('x','y');
w[1] = 'z';

Test sur l’existence des champs

Une fois l’ensemble des éléments correctement initialisé nous pouvons parcourir par une boucle "for" les éléments label pour vérifier qu’ils soient correctement associés à un champ, c’est à dire qu’il est possible de récupérer un objet en utilisant la méthode getElementById. Cette méthode du DOM permet de récupérer l’objet dont valeur de l’attribut "id" correspond à la chaîne passée en paramètre. Dans notre cas, ce paramètre correspondra à la valeur de l’attribut "for" de l’élément label. Si cette valeur n’existe pas ou que la méthode getElementById retourne la valeur null (c’est à dire qu’il n’existe pas d’objet avec l’id spécifié) nous passerons à l’élément suivant de la boucle à l’aide du mot clef "continue". "for" correspondant à un mot réservé dans de nombreux langages dont javascript, la récupération de la valeur de cet attribut est réalisée en accédant à la propriété "htmlFor".

for (var i=0; bIsValide && i<cLabels.length; i++) {
  if ((cLabels[i].htmlFor=="") ||
    !(nField=document.getElementById(cLabels[i].htmlFor))) continue;
  //...........
}

Test sur les classes prédéfinies

A la suite de cette vérification d’usage, nous sommes assurés d’avoir - à chacune des itérations de la boucle - un objet nField faisant référence au champ associé à un élément label. Pour chacun de ces labels et tant qu’aucune erreur de saisie ne sera détectée nous allons déterminer les classes prédéfinies qui sont spécifiées au sein de l’élément.

for (var i=0; bIsValide && i<cLabels.length; i++) {
  if ((cLabels[i].htmlFor=="") ||
    !(nField=document.getElementById(cLabels[i].htmlFor))) continue;
  for (var j=0; bIsValide && aFormCtrlSchemes[j]; j++) {
    if (hasClassName(cLabels[i],aFormCtrlSchemes[j][0])) {
    //...........
    }
  }
}

Le test utilisé pour déterminer si une classe particulière est implémentée correspond à la ligne :

if (hasClassName(cLabels[i],aFormCtrlSchemes[j][0])) {...

La méthode hasClassName n’est ni plus ni moins qu’une fonction de notre cru permettant de gagner un peu en lisibilité au milieu de toutes ces références d’objet.

function hasClassName(oNode,className) {
    return ((" "+oNode.className+" ").indexOf(" "+className+" ")!=-1);
}

Elle se limite à retourner le booléen true si une classe particulière (className) est spécifiée au sein d’un élément (oNode). Dans le cas contraire, la fonction retourne le booléen false. Comme pour l’attribut "for", le terme "class" étant un mot réservé, c’est l’accès à la propriété "className" qui permet de récupérer la valeur de cet attribut. La forme particulière de l’attribut "class" nous force à prendre quelques précautions. En effet, cet attribut autorise la déclaration de plusieurs valeurs séparées par un caractère espace. Dans une telle situation, l’opérateur d’égalité (oNode.className==classname) ne peut être utilisé pour réaliser le test. L’astuce consiste donc à déterminer si la valeur retournée par cette propriété contient une sous-chaîne correspondant à la classe recherchée. Nous pouvons par exemple utiliser la méthode "indexof" qui retourne l’indice de la position éventuelle d’une sous-chaîne. Si la sous-chaîne n’est pas trouvée, la méthode retourne la valeur -1. Afin d’éviter les faux positifs correspondant aux classes contenant la valeur de test, nous encadrons cette valeur ainsi que la valeur de la propriété className par le séparateur de l’attribut, c’est à dire le caractère espace.

Evaluation des contrôles

Dans le cas où une classe prédéfinie est spécifiée au sein de l’élément label, il reste à exécuter la fonction de même nom en lui passant en paramètre la valeur du champ à contrôler.
La méthode utilisée consiste à faire évaluer par le moteur javascript du navigateur - et grâce à la méthode eval - une chaîne dont la valeur sera interprétée comme un élément de script.

if (!eval(aFormCtrlSchemes[j][0]+"(nField.value)")) {
  bIsValide = false;
}

Si le retour de la fonction évaluée correspond à la valeur false, nous considérons que la validation a échoué.
Afin d’éviter toute erreur d’exécution, il est à présent nécessaire d’implémenter les fonctions de contrôles sur la valeur des champs

  • non vide ;
    function isNotEmpty(s) {return s!=''}
  • représentant un entier ;
    function isInt(s) {
      if (s!= '') {
        return parseInt(s, 10)==s;
      } else {
        return true;
      }
    }

Génération des messages d’erreurs

Récupération du contenu textuel de l’élément label

En cas d’erreur sur la saisie nous allons récupérer le contenu textuel de l’élément label pour personnaliser le message d’erreur générique correspondant au type de contrôle.
La première étape va consister à récupérer ce contenu grâce à la représentation du document offerte par le DOM. Dans cette représentation, les constituants du document (balises HTML, textes, commentaires, etc.) correspondent à des noeuds tels ceux disponible dans un arbre généalogique. Chacun ayant des propriétés propres (type, nom, valeur) et un contexte d’insertion (parent, frères, enfants).
Sur l’exemple du label "S’inscrire à la newsletter" de notre formulaire, la représentation arborescente du DOM correspond sensiblement à :

Représentation arborescente du DOM pour le label "S’inscrire à la newsletter"

  • un premier noeud de type élément a pour nom LABEL ;
  • ce noeud possède une collection ordonnée d’enfants ;
  • le premier enfant est de type texte et a pour valeur "S'inscrire à la" ;
  • le second enfant est de type élément et a pour nom SPAN ;
  • l’élément span possède lui-même un enfant de type texte dont la valeur est "newsletter".

Notre objectif est de concaténer les valeurs des différents noeuds de type texte qui sont descendants de l’élément LABEL.

La troisième spécification du DOM définit la propriété textContent qui réalise cette opération. Cependant tous les clients n’implémentent pas cette propriété. Il va donc être nécessaire de proposer une fonction récursive que nous appellerons "getTextContent" produisant un résultat identique.

Note :
Internet Explorer propose une propriété permettant de récupérer le contenu textuel d’un élément via la propriété "innerText". Mais le caractère propriétaire nous encourage à ne pas l’utiliser lorsqu’il existe des solutions alternatives afin d’éviter une nouvelle balkanisation du Web.

getTextContent

function getTextContent(oNode) {
  if (typeof(oNode.textContent)!="undefined") {return oNode.textContent;}
  switch (oNode.nodeType) {
    case 3: // TEXT_NODE
    case 4: // CDATA_SECTION_NODE
      return oNode.nodeValue;
      break;
    case 7: // PROCESSING_INSTRUCTION_NODE
    case 8: // COMMENT_NODE
      if (getTextContent.caller!=getTextContent) {
        return oNode.nodeValue;
      }
      break;
    case 9: // DOCUMENT_NODE
    case 10: // DOCUMENT_TYPE_NODE
    case 12: // NOTATION_NODE
      return null;
      break;
  }
  var _textContent = "";
  oNode = oNode.firstChild;
  while (oNode) {
    _textContent += getTextContent(oNode);
    oNode = oNode.nextSibling;
  }
  return _textContent;
}

Pour commencer, la fonction vérifie si le client implémente la propriété "textContent" en vérifiant que le type de la valeur retournée est différent de "undefined". Si tel est le cas, la fonction retourne directement la valeur de cette propriété.

Dans le cas contraire et pour rester en accord avec la propriété textContent, la fonction va avoir un comportement spécifique au type du noeud (oNode.nodeType) reçus en paramètre. Pour un noeud de type texte (TEXT_NODE) ou d’une section CDATA (CDATA_SECTION_NODE) la fonction va retourner la valeur du noeud. Pour un noeud de type commentaire (COMMENT_NODE) ou instruction de traitement (PROCESSING_INSTRUCTION_NODE) la fonction retourne la valeur du noeud si et seulement si la fonction qui a appelé getTextContent (getTextContent.caller) ne correspond pas à getTextContent (c’est à dire que nous ne somme pas dans le cas d’une boucle récursive). En effet, selon la spécification DOM la propriété textContent ne doit pas prendre en compte ces noeuds lorsque la récupération du contenu textuel se fait initialement sur un autre type de noeud - par exemple un élément. Dans le dernier cas d’un noeud de type document (DOCUMENT_NODE), doctype (DOCUMENT_TYPE_NODE) et notation (NOTATION_NODE), la fonction retourne la valeur null.

Après les traitements spécifiques à certains types de noeud, la fonction récupère le premier enfant du noeud (oNode.firstChild) passé en paramètre. Si cet enfant existe, la fonction concatène le retour de getTextContent appliquée récursivement à cet enfant et aux noeuds qui le suivent (oNode.nextSibling).

Petit toilettage du contenu et génération de la boîte de dialogue

Un simple getTextContent appliqué à un élément permet de récupérer l’ensemble de son contenu textuel.

if (!eval(aFormCtrlSchemes[j][0]+"(nField.value)")) {
  bIsValide = false;
  var textContent = getTextContent(nLabel);
}

Sans autre modification, il est possible que la valeur de la variable textContent inclue les sauts de ligne et les espaces multiples présents dans la source HTML. Nous allons remplacer ces éléments disgracieux par un simple espace en utilisant une expression régulière.

if (!eval(aFormCtrlSchemes[j][0]+"(nField.value)")) {
  bIsValide = false;
  var textContent = getTextContent(cLabels[i]).replace(/\s{2,}/g," ");
}

Le point suivant concerne les caractères souvent présents dans les étiquettes de formulaire mais qu’il n’est pas souhaitable de retrouver dans un message d’erreur. Parmi ces derniers nous retrouvons par exemple les caractères " :" ou espaces utilisés comme séparateur et "*" pour marquer les champs obligatoires.
Nous allons appliquer une seconde expression régulière pour supprimer ces éléments présents en début ou fin de chaîne :

if (!eval(aFormCtrlSchemes[j][0]+"(nField.value)")) {
  bIsValide = false;
  var textContent = getTextContent(cLabels[i]).replace(/\s{2,}/g," ");
  textContent = textContent.replace(/^[\s:*]+|[\s:*]+$/g,"");
}

Une fois le contenu textuel épuré de l’ensemble des éléments non souhaitables il ne reste plus qu’à générer la boîte de dialogue en remplaçant la séquence "%s" du message générique par la valeur de la variable textContent.

if (!eval(aFormCtrlSchemes[j][0]+"(nField.value)")) {
  bIsValide = false;
  var textContent = getTextContent(cLabels[i]).replace(/\s{2,}/g," ");
  textContent = textContent.replace(/^[\s:*]+|[\s:*]+$/g,"");
  alert(aFormCtrlSchemes[j][1].replace('%s',textContent));
}

Après la notification de cette éventuelle erreur, il nous faut terminer par la mise en focus du champ concerné. Ceci afin de l’utilisateur ait toutes les informations pour rectifier sa saisie.

for (var i=0; bIsValide && i<cLabels.length; i++) {
  // ...
  for (var j=0; bIsValide && aFormCtrlSchemes[j]; j++) {
    // ...
  }
}
if (!bIsValide) {nField.focus();}

Une première version de la validation avancée des formulaires peut être consultée en annexe.

Ajout de contrôle plus spécifique

Conscient qu’un script mutualisé ne peut pas (ne devrait pas) prendre en compte de manière satisfaisante l’ensemble des cas de figures proposons une manière d’implémenter des contrôles plus spécifiques.

Imaginons que nous souhaitons vérifier que la valeur saisie pour le champ "Confirmation du mot de passe" soit identique à celle du champ "Mot de passe".

La première étape est de spécifier au script qu’un contrôle spécifique doit être réalisé pour certains éléments. Sur le même principe que les contrôles génériques, nous allons ajouter une classe aux labels concernés - par exemple la classe "extendedCtrl".
La seconde étape est de mettre en place une nomenclature dans le nommage des fonctions de contrôles à évaluer. En utilisant le caractère unique des attributs "id" nous sommes assurés qu’aucune des dites fonctions ne rentrera en concurrence avec une autre. Afin d’améliorer un peu la lisibilité du code et sa maintenance nous allons ajouter un préfixe commun à l’ensemble de ces fonctions - par exemple le libellé de la classe rajoutée sur l’élément label.

Les fonctions recevront en paramètre une référence du champ qu’elles doivent valider et devront gérer les boîtes de dialogue en cas d’erreur sur la saisie. Elles se limiteront à retourner un booléen indiquant le succès ou l’échec du contrôle.

Sur l’exemple du champ de confirmation, nous obtenons un label de la forme :

<label for="confirmpass" class="isNotEmpty extendedCtrl">
  * Confirmation du mot de passe :
</label>
<input type="text" name="confirmpass" id="confirmpass">

et une fonction de contrôle :

function extendedCtrl_confirmpass(nField) {
  var result = true;
  if (nField.value!=document.getElementById("password").value) {
    result = false;
    alert('La valeur du champ "Confirmation du mot de passe" doit être identique à la valeur saisie pour le champ "Mot de passe".');
  }
  return result;
}

Il ne reste plus qu’à intégrer les contrôles spécifiques à la fonction "formControlListener" juste après les validations génériques.

for (var j=0; bIsValide && j<aFormCtrlSchemes.length; j++) {
  if (hasClassName(cLabels[i],aFormCtrlSchemes[j][0])) {
  //...........
  }
}
if (bIsValide && hasClassName(cLabels[i],"extendedCtrl")) {
  bIsValide = eval("extendedCtrl_"+cLabels[i].htmlFor+"(nField);");
}

Une version de la validation avancée des formulaires intégrant les contrôles spécifiques est disponible en annexe.

Externaliser la gestion des évènements

Pour une plus grande simplicité de mise en oeuvre, nous nous devons d’apporter un dernier détail. Nous souhaitons limiter l’intégration au strict minimum (ajout des classes sur les éléments à contrôler uniquement) et proposer une séparation entre la structure et le comportement plus importante. Cette séparation a pour objectif de nous libérer des dépendances fortes lors des évolutions sur le script ou les formulaires.

Pour le moment, l’intégration nécessite de définir - dans la source HTML - la fonction déclenchée lors de la soumission du formulaire. Nous allons automatiser cette tâche directement dans le script externe.
Pour cela, nous allons exécuter une fonction au chargement de la page qui va récupérer l’ensemble des formulaires grâce à la collection "forms" de l’objet "document" et leur définir la propriété onsubmit.

window.onload = function() {
  var cForms = document.forms;
  for (var i=0; cForms[i]; i++) {
    cForms[i].onsubmit = formControlListener;
  }
}

Cette petite modification a une légère répercussion sur la fonction "formControlListener", puisque dans ce cas l’objet formulaire ne peut plus être passé en paramètre. Le fait d’affecter la fonction formControlListerner a la propriété onsubmit à pour conséquence d’associer directement cette fonction à l’objet formulaire. L’utilisation du paramètre nForm doit être substituée par le mot clef this.

Ainsi l’initialisation de la variable cLabels :

var cLabels = nForm.getElementsByTagName("label");

doit être remplacée par :

var cLabels = this.getElementsByTagName("label");

Une version de la validation des formulaires séparant la structure et le comportement est disponible en annexe.

Conclusion

La mise en place et la maintenance des contrôles javascript sur les formulaires représente habituellement une tâche relativement ennuyeuse à réaliser. Tellement ennuyeuse, que certains sites et applications en ligne se limitent à l’indispensable contrôle coté serveur.

Mais nous venons de voir à travers cet exemple comment une utilisation judicieuse des technologies standards peut nous aider à rationaliser nos développements. En quelques secondes, il devient possible d’ajouter ou de modifier un contrôle quelle que soit la complexité du formulaire mise en oeuvre.

Annexes

Voici la liste des annexes citées dans cet exercice :

À propos de cet article

  • Openweb.eu.org
  • Profil : Expert, Gourou
  • Technologie : DOM
  • Thème : Accessibilité, Pages dynamiques
  • Auteur :
  • Publié le :
  • Mise à jour : 1er juillet 2008
  • 4 commentaires

Vos commentaires

  • Xavier Le 7 décembre 2011 à 18:59

    Super article, merci. Il va falloir que je le relise plusieurs fois à tête reposée avant de pouvoir le mettre en pratique.
    Encore merci

  • Edouard D. Le 31 octobre 2012 à 15:51

    bonjour !
    je suis à recherche d’un code javascript pouvant permette d’envoyer un formulaire rempli, via le pdf
    en effet, je veux loger dans le site un bulletin d’Adhésion afin de permettre à tout ceux qui souhaiterait adhérer à notre association (ONGJE ),
    de le remplir depuis le site, de cliquer sur le bouton envoyer directement le formulaire, avec un mot de confirmation d’envoie.

    merci d’avance !
    Edouard .D

  • n Le 28 août 2013 à 11:00

    bnnnnnnnnnnnnnnnnnn

  • fff Le 30 avril 2018 à 12:43

    ffffffffffffffffffffffffffffffffffffff

Vos commentaires

modération a priori

Attention, votre message n’apparaîtra qu’après avoir été relu et approuvé.

Qui êtes-vous ?
Ajoutez votre commentaire ici

Ce champ accepte les raccourcis SPIP {{gras}} {italique} -*liste [texte->url] <quote> <code> et le code HTML <q> <del> <ins>. Pour créer des paragraphes, laissez simplement des lignes vides.

Suivre les commentaires : RSS 2.0 | Atom