TP : Un formulaire interactif

Nous sommes presque au bout de cette deuxième partie du cours ! Cette dernière aura été très volumineuse et il se peut que vous ayez oublié pas mal de choses depuis votre lecture, ce TP va donc se charger de vous rappeler l'essentiel de ce que nous avons appris ensemble.

Le sujet va porter sur la création d'un formulaire dynamique. Qu'est-ce nous entendons par formulaire dynamique ? Eh bien, un formulaire dont une partie des vérifications est effectuée par le JavaScript, côté client. On peut par exemple vérifier que l'utilisateur a bien complété tous les champs, ou bien qu'ils contiennent des valeurs valides (si le champ « âge » ne contient pas des lettres au lieu de chiffres par exemple).

À ce propos, nous allons tout de suite faire une petite précision très importante pour ce TP et tous vos codes en JavaScript :

Bien, nous pouvons maintenant commencer !

Présentation de l'exercice

Faire un formulaire c'est bien, mais encore faut-il savoir quoi demander à l'utilisateur. Dans notre cas, nous allons faire simple et classique : un formulaire d'inscription. Notre formulaire d'inscription aura besoin de quelques informations concernant l'utilisateur, cela nous permettra d'utiliser un peu tous les éléments HTML spécifiques aux formulaires que nous avons vus jusqu'à présent. Voici les informations à récupérer ainsi que les types d'éléments HTML :

Information à relever

Type d'élément à utiliser

Sexe

<input type="radio">

Nom

<input type="text">

Prénom

<input type="text">

Âge

<input type="text">

Pseudo

<input type="text">

Mot de passe

<input type="password">

Mot de passe (confirmation)

<input type="password">

Pays

<select></select>

Si l'utilisateur souhaite
recevoir des mails

<input type="checkbox">

Bien sûr, chacune de ces informations devra être traitée afin que l'on sache si le contenu est bon. Par exemple, si l'utilisateur a bien spécifié son sexe ou bien s'il n'a pas entré de chiffres dans son prénom, etc. Dans notre cas, nos vérifications de contenu ne seront pas très poussées pour la simple et bonne raison que nous n'avons pas encore étudié les « regex » à ce stade du cours, nous nous limiterons donc à la vérification de la longueur de la chaîne ou bien à la présence de certains caractères. Bref, rien d'incroyable, mais cela suffira amplement car le but de ce TP n'est pas vraiment de vous faire analyser le contenu mais plutôt de gérer les événements et le CSS de votre formulaire.

Voici donc les conditions à respecter pour chaque information :

Information à relever

Condition à respecter

Sexe

Un sexe doit être sélectionné

Nom

Pas moins de 2 caractères

Prénom

Pas moins de 2 caractères

Âge

Un nombre compris entre 5 et 140

Pseudo

Pas moins de 4 caractères

Mot de passe

Pas moins de 6 caractères

Mot de passe (confirmation)

Doit être identique au premier mot de passe

Pays

Un pays doit être sélectionné

Si l'utilisateur souhaite
recevoir des mails

Pas de condition

Concrètement, l'utilisateur n'est pas censé connaître toutes ces conditions quand il arrive sur votre formulaire, il faudra donc les lui indiquer avant même qu'il ne commence à entrer ses informations, comme ça il ne perdra pas de temps à corriger ses fautes. Pour cela, il va vous falloir afficher chaque condition d'un champ de texte quand l'utilisateur fera une erreur. Pourquoi parlons-nous ici uniquement des champs de texte ? Tout simplement parce que nous n'allons pas dire à l'utilisateur « Sélectionnez votre sexe » alors qu'il n'a qu'une case à cocher, cela paraît évident.

Autre chose, il faudra aussi faire une vérification complète du formulaire lorsque l'utilisateur aura cliqué sur le bouton de soumission. À ce moment-là, si l'utilisateur n'a pas coché de case pour son sexe on pourra lui dire qu'il manque une information, pareil s'il n'a pas sélectionné de pays.

Vous voilà avec toutes les informations nécessaires pour vous lancer dans ce TP. Nous vous laissons concevoir votre propre code HTML, mais vous pouvez très bien utiliser celui de la correction si vous le souhaitez.

Correction

Bien, vous avez probablement terminé si vous lisez cette phrase. Ou bien vous n'avez pas réussi à aller jusqu'au bout, ce qui peut arriver !

Le corrigé au grand complet : HTML, CSS et JavaScript

Nous pouvons maintenant passer à la correction. Pour ce TP, il vous fallait créer la structure HTML de votre page en plus du code JavaScript ; voici le code que nous avons réalisé pour ce TP :

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TP : Un formulaire interactif</title>
</head>
<body>
<form id="myForm">
<span class="form_col">Sexe :</span>
<label><input name="sex" type="radio" value="H" />Homme</label>
<label><input name="sex" type="radio" value="F" />Femme</label>
<span class="tooltip">Vous devez sélectionnez votre sexe</span>
<br /><br />
<label class="form_col" for="lastName">Nom :</label>
<input name="lastName" id="lastName" type="text" />
<span class="tooltip">Un nom ne peut pas faire moins de 2 caractères</span>
<br /><br />
<label class="form_col" for="firstName">Prénom :</label>
<input name="firstName" id="firstName" type="text" />
<span class="tooltip">Un prénom ne peut pas faire moins de 2 caractères</span>
<br /><br />
<label class="form_col" for="age">Âge :</label>
<input name="age" id="age" type="text" />
<span class="tooltip">L'âge doit être compris entre 5 et 140</span>
<br /><br />
<label class="form_col" for="login">Pseudo :</label>
<input name="login" id="login" type="text" />
<span class="tooltip">Le pseudo ne peut pas faire moins de 4 caractères</span>
<br /><br />
<label class="form_col" for="pwd1">Mot de passe :</label>
<input name="pwd1" id="pwd1" type="password" />
<span class="tooltip">Le mot de passe ne doit pas faire moins de 6 caractères</span>
<br /><br />
<label class="form_col" for="pwd2">Mot de passe (confirmation) :</label>
<input name="pwd2" id="pwd2" type="password" />
<span class="tooltip">Le mot de passe de confirmation doit être identique à celui d'origine</span>
<br /><br />
<label class="form_col" for="country">Pays :</label>
<select name="country" id="country">
<option value="none">Sélectionnez votre pays de résidence</option>
<option value="en">Angleterre</option>
<option value="us">États-Unis</option>
<option value="fr">France</option>
</select>
<span class="tooltip">Vous devez sélectionner votre pays de résidence</span>
<br /><br />
<span class="form_col"></span>
<label><input name="news" type="checkbox" /> Je désire recevoir la newsletter chaque mois.</label>
<br /><br />
<span class="form_col"></span>
<input type="submit" value="M'inscrire" /> <input type="reset" value="Réinitialiser le formulaire" />
</form>
</body>
</html>

Vous remarquerez que de nombreuses balises<span>possèdent une classe nommée.tooltip. Elles contiennent le texte à afficher lorsque le contenu du champ les concernant ne correspond pas à ce qui est souhaité.

Nous allons maintenant passer au CSS. D'habitude nous ne vous le fournissons pas directement, mais cette fois il fait partie intégrante de ce TP, donc le voici :

body {
padding-top: 50px;
}
.form_col {
display: inline-block;
margin-right: 15px;
padding: 3px 0px;
width: 200px;
min-height: 1px;
text-align: right;
}
input {
padding: 2px;
border: 1px solid #CCC;
border-radius: 2px;
outline: none; /* Retire les bordures appliquées par certains navigateurs (Chrome notamment) lors du focus des éléments <input> */
}
input:focus {
border-color: rgba(82, 168, 236, 0.75);
box-shadow: 0 0 8px rgba(82, 168, 236, 0.5);
}
.correct {
border-color: rgba(68, 191, 68, 0.75);
}
.correct:focus {
border-color: rgba(68, 191, 68, 0.75);
box-shadow: 0 0 8px rgba(68, 191, 68, 0.5);
}
.incorrect {
border-color: rgba(191, 68, 68, 0.75);
}
.incorrect:focus {
border-color: rgba(191, 68, 68, 0.75);
box-shadow: 0 0 8px rgba(191, 68, 68, 0.5);
}
.tooltip {
display: inline-block;
margin-left: 20px;
padding: 2px 4px;
border: 1px solid #555;
background-color: #CCC;
border-radius: 4px;
}

Notez bien les deux classes.correctet.incorrect: elles seront appliquées aux<input>de typetextetpasswordafin de bien montrer si un champ est correctement rempli ou non.

Nous pouvons maintenant passer au plus compliqué, le code JavaScript :

// Fonction de désactivation de l'affichage des "tooltips"
function deactivateTooltips() {
var tooltips = document.querySelectorAll('.tooltip'),
tooltipsLength = tooltips.length;
for (var i = 0; i < tooltipsLength; i++) {
tooltips[i].style.display = 'none';
}
}
// La fonction ci-dessous permet de récupérer la "tooltip" qui correspond à notre input
function getTooltip(elements) {
while (elements = elements.nextSibling) {
if (elements.className === 'tooltip') {
return elements;
}
}
return false;
}
// Fonctions de vérification du formulaire, elles renvoient "true" si tout est ok
var check = {}; // On met toutes nos fonctions dans un objet littéral
check['sex'] = function() {
var sex = document.getElementsByName('sex'),
tooltipStyle = getTooltip(sex[1].parentNode).style;
if (sex[0].checked || sex[1].checked) {
tooltipStyle.display = 'none';
return true;
} else {
tooltipStyle.display = 'inline-block';
return false;
}
};
check['lastName'] = function(id) {
var name = document.getElementById(id),
tooltipStyle = getTooltip(name).style;
if (name.value.length >= 2) {
name.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
name.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};
check['firstName'] = check['lastName']; // La fonction pour le prénom est la même que celle du nom
check['age'] = function() {
var age = document.getElementById('age'),
tooltipStyle = getTooltip(age).style,
ageValue = parseInt(age.value);
if (!isNaN(ageValue) && ageValue >= 5 && ageValue <= 140) {
age.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
age.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};
check['login'] = function() {
var login = document.getElementById('login'),
tooltipStyle = getTooltip(login).style;
if (login.value.length >= 4) {
login.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
login.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};
check['pwd1'] = function() {
var pwd1 = document.getElementById('pwd1'),
tooltipStyle = getTooltip(pwd1).style;
if (pwd1.value.length >= 6) {
pwd1.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
pwd1.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};
check['pwd2'] = function() {
var pwd1 = document.getElementById('pwd1'),
pwd2 = document.getElementById('pwd2'),
tooltipStyle = getTooltip(pwd2).style;
if (pwd1.value == pwd2.value && pwd2.value != '') {
pwd2.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
pwd2.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};
check['country'] = function() {
var country = document.getElementById('country'),
tooltipStyle = getTooltip(country).style;
if (country.options[country.selectedIndex].value != 'none') {
tooltipStyle.display = 'none';
return true;
} else {
tooltipStyle.display = 'inline-block';
return false;
}
};
// Mise en place des événements
(function() { // Utilisation d'une IIFE pour éviter les variables globales.
var myForm = document.getElementById('myForm'),
inputs = document.querySelectorAll('input[type=text], input[type=password]'),
inputsLength = inputs.length;
for (var i = 0; i < inputsLength; i++) {
inputs[i].addEventListener('keyup', function(e) {
check[e.target.id](e.target.id); // "e.target" représente l'input actuellement modifié
});
}
myForm.addEventListener('submit', function(e) {
var result = true;
for (var i in check) {
result = check[i](i) && result;
}
if (result) {
alert('Le formulaire est bien rempli.');
}
e.preventDefault();
});
myForm.addEventListener('reset', function() {
for (var i = 0; i < inputsLength; i++) {
inputs[i].className = '';
}
deactivateTooltips();
});
})();
// Maintenant que tout est initialisé, on peut désactiver les "tooltips"
deactivateTooltips();

Essayer le code complet de ce TP

Les explications

Les explications vont essentiellement porter sur le code JavaScript qui est, mine de rien, plutôt long (plus de deux cents lignes de code, ça commence à faire pas mal).

La désactivation des bulles d'aide

Dans notre code HTML nous avons créé des balises<span>avec la classe.tooltip. Ce sont des balises qui vont nous permettre d'afficher des bulles d'aide, pour que l'utilisateur sache quoi entrer comme contenu. Seulement, elles sont affichées par défaut et il nous faut donc les cacher par le biais du JavaScript.

Et pourquoi ne pas les cacher par défaut puis les afficher grâce au JavaScript ?

Si vous faites cela, vous prenez le risque qu'un utilisateur ayant désactivé le JavaScript ne puisse pas voir les bulles d'aide, ce qui serait plutôt fâcheux, non ? Après tout, afficher les bulles d'aide par défaut et les cacher avec le JavaScript ne coûte pas grand-chose, autant le faire… De plus, nous allons avoir besoin de cette fonction plus tard quand l'utilisateur voudra réinitialiser son formulaire.

Venons-en donc au code :

function deactivateTooltips() {
var tooltips = document.querySelectorAll('.tooltip'),
tooltipsLength = tooltips.length;
for (var i = 0; i < tooltipsLength; i++) {
tooltips[i].style.display = 'none';
}
}

Est-il vraiment nécessaire de vous expliquer ce code en détail ? Il ne s'agit que de cacher tous les éléments qui ont une classe.tooltip.

Récupérer la bulle d'aide correspondant à un<input>

Il est facile de parcourir toutes les bulles d'aide, mais il est un peu plus délicat de récupérer celle correspondant à un<input>que l'on est actuellement en train de traiter. Si nous regardons bien la structure de notre document HTML, nous constatons que les bulles d'aide sont toujours placées après l'<input>auquel elles correspondent, nous allons donc partir du principe qu'il suffit de chercher la bulle d'aide la plus « proche » après l'<input>que nous sommes actuellement en train de traiter. Voici le code :

function getTooltip(element) {
while (element = element.nextSibling) {
if (element.className === 'tooltip') {
return element;
}
}
return false;
}

Notre fonction prend en argument l'<input>actuellement en cours de traitement. Notre bouclewhilese charge alors de vérifier tous les éléments suivants notre<input>(d'où l'utilisation dunextSibling). Une fois qu'un élément avec la classe.tooltipa été trouvé, il ne reste plus qu'à le retourner.

Analyser chaque valeur entrée par l'utilisateur

Nous allons enfin entrer dans le vif du sujet : l'analyse des valeurs et la modification du style du formulaire en conséquence. Tout d'abord, quelles valeurs faut-il analyser ? Toutes, sauf la case à cocher pour l'inscription à la newsletter. Maintenant que cela est clair, passons à la toute première ligne de code :

var check = {};

Alors oui, au premier abord, cette ligne de code ne sert vraiment pas à grand-chose, mais en vérité elle a une très grande utilité : l'objet créé va nous permettre d'y stocker toutes les fonctions permettant de « checker » (d'où le nom de l'objet) chaque valeur entrée par l'utilisateur. L'intérêt de cet objet est triple :

  • Nous allons pouvoir exécuter la fonction correspondant à un champ de cette manière :check['id_du_champ']();. Cela va grandement simplifier notre code lors de la mise en place des événements.

  • Il sera possible d'exécuter toutes les fonctions de « check » juste en parcourant l'objet, ce sera très pratique lorsque l'utilisateur cliquera sur le bouton d'inscription et qu'il faudra alors revérifier tout le formulaire.

  • L'ajout d'un champ de texte et de sa fonction d'analyse devient très simple si on concentre tout dans cet objet, vous comprendrez très rapidement pourquoi !

Nous n'allons pas étudier toutes les fonctions d'analyse, elles se ressemblent beaucoup, nous allons donc uniquement étudier deux fonctions afin de mettre les choses au clair :

check['login'] = function() {
var login = document.getElementById('login'),
tooltipStyle = getTooltip(login).style;
if (login.value.length >= 4) {
login.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
login.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};

Il est très important que vous constatiez que notre fonction est contenue dans l'indexloginde l'objetcheck, l'index n'est rien d'autre que l'identifiant du champ de texte auquel la fonction appartient. Le code n'est pas bien compliqué : on récupère l'<input>et la propriétéstylede la bulle d'aide qui correspondent à notre fonction et on passe à l'analyse du contenu.

Si le contenu remplit bien la condition, alors on attribue à notre<input>la classe.correct, on désactive l'affichage de la bulle d'aide et on retournetrue.
Si le contenu ne remplit pas la condition, notre<input>se voit alors attribuer la classe.incorrectet la bulle d'aide est affichée. En plus de cela, on renvoie la valeurfalse.

Passons maintenant à une deuxième fonction que nous tenions à aborder :

check['lastName'] = function(id) {
var name = document.getElementById(id),
tooltipStyle = getTooltip(name).style;
if (name.value.length >= 2) {
name.className = 'correct';
tooltipStyle.display = 'none';
return true;
} else {
name.className = 'incorrect';
tooltipStyle.display = 'inline-block';
return false;
}
};

Cette fonction diffère de la précédente sur un seul point : elle possède un argumentid! Cet argument sert à récupérer l'identifiant de l'<input>à analyser. Pourquoi ? Tout simplement parce qu'elle va nous servir à analyser deux champs de texte différents : celui du nom et celui du prénom. Puisqu'ils ont tous les deux la même condition, il aurait été stupide de créer deux fois la même fonction.

Donc, au lieu de faire appel à cette fonction sans aucun argument, il faut lui passer l'identifiant du champ de texte à analyser, ce qui donne deux possibilités :

check['lastName']('lastName');
check['lastName']('firstName');

Cependant, ce fonctionnement pose un problème, car nous étions partis du principe que nous allions faire appel à nos fonctions d'analyse selon le principe suivant :

check['id_du_champ']();

Or, si nous faisons ça, cela veut dire que nous ferons aussi appel à la fonctioncheck['firstName']()qui n'existe pas… Nous n'allons pas créer cette fonction sinon nous perdrons l'intérêt de notre système d'argument sur la fonctioncheck['lastName'](). La solution est donc de faire une référence de la manière suivante :

check['firstName'] = check['lastName'];

Ainsi, lorsque nous appellerons la fonctioncheck['firstName'](), implicitement ce sera la fonctioncheck['lastName']()qui sera appelée. Si vous n'avez pas encore tout à fait compris l'utilité de ce système, vous allez voir que tout cela va se montrer redoutablement efficace dans la suite du code.

La mise en place des événements : partie 1

La mise en place des événements se décompose en deux parties : les événements à appliquer aux champs de texte et les événements à appliquer aux deux boutons en bas de page pour envoyer ou réinitialiser le formulaire.

Nous allons commencer par les champs de texte. Tout d'abord, voici le code :

var inputs = document.querySelectorAll('input[type=text], input[type=password]'),
inputsLength = inputs.length;
for (var i = 0; i < inputsLength; i++) {
inputs[i].addEventListener('keyup', function(e) {
check[e.target.id](e.target.id); // "e.target" représente l'input actuellement modifié
});
}

Comme nous l'avons déjà fait plus haut, nous n'allons pas prendre la peine de vous expliquer le fonctionnement de cette boucle et de la condition qu'elle contient, il ne s'agit que de parcourir les<input>de typetextoupassword.

En revanche, il va falloir des explications sur les lignes 5 à 7. Elles permettent d'assigner une fonction anonyme à l'événementkeyupde l'<input>actuellement traité. Quant à la ligne 6, elle fait appel à la fonction d'analyse qui correspond à l'<input>qui a exécuté l'événement. Ainsi, si l'<input>#logindéclenche son événement, il appellera alors la fonctioncheck['login']().

Cependant, un argument est passé à chaque fonction d'analyse que l'on exécute. Pourquoi ? Eh bien il s'agit de l'argument nécessaire à la fonctioncheck['lastName'](), ainsi lorsque les<input>#lastNameet#firstNamedéclencheront leur événement, ils exécuteront alors respectivement les lignes de codes suivantes :

check['lastName']('lastName');

et

check['firstName']('firstName');

Mais là on passe l'argument à toutes les fonctions d'analyse, cela ne pose pas de problème normalement ?

Pourquoi cela en poserait-il un ? Imaginons que l'<input>#logindéclenche son événement, il exécutera alors la ligne de code suivante :

check['login']('login');

Cela fera passer un argument inutile dont la fonction ne tiendra pas compte, c'est tout.

La mise en place des événements : partie 2

Nous pouvons maintenant aborder l'attribution des événements sur les boutons en bas de page :

(function() { // Utilisation d'une IIFE pour éviter les variables globales.
var myForm = document.getElementById('myForm'),
inputs = document.querySelectorAll('input[type=text], input[type=password]'),
inputsLength = inputs.length;
for (var i = 0; i < inputsLength; i++) {
inputs[i].addEventListener('keyup', function(e) {
check[e.target.id](e.target.id); // "e.target" représente l'input actuellement modifié
});
}
myForm.addEventListener('submit', function(e) {
var result = true;
for (var i in check) {
result = check[i](i) && result;
}
if (result) {
alert('Le formulaire est bien rempli.');
}
e.preventDefault();
});
myForm.addEventListener('reset', function() {
for (var i = 0; i < inputsLength; i++) {
inputs[i].className = '';
}
deactivateTooltips();
});
})();

Comme vous pouvez le constater, nous n'avons pas appliqué d'événementsclicksur les boutons mais avons directement appliquésubmitetresetsur le formulaire, ce qui est bien plus pratique dans notre cas.

Alors concernant notre événementsubmit, celui-ci va parcourir notre tableauchecket exécuter toutes les fonctions qu'il contient (y compris celles qui ne sont pas associées à un champ de texte commecheck['sex']()etcheck['country']()). Chaque valeur retournée par ces fonctions est « ajoutée » à la variableresult, ce qui fait que si une des fonctions a renvoyéfalsealorsresultsera aussi àfalseet l'exécution de la fonctionalert()ne se fera pas.

Concernant notre événementreset, c'est très simple : on parcourt les champs de texte, on retire leur classe et ensuite on désactive toutes les bulles d'aide grâce à notre fonctiondeactivateTooltips().

Voilà, ce TP est maintenant terminé.