8. Ajouter des règles de validation

Pour le moment notre formulaire est fonctionnel, mais nous avons encore une problématique importante à résoudre, c’est que l’utilisateur peut saisir absolument n’importe quelle valeur dans le formulaire. Qu’allons nous faire si l’utilisateur saisie du texte à la place d’un nombre pour les dégâts, ou souhaite nommer un pokémon « !!###$$ » ? Il serait intéressant de valider un minimum les données saisies par l’utilisateur pour s’assurer de la cohérence de nos données par la suite.

Mais avant de voir comment ajouter des règles de validation, je vous propose de définir quelles restrictions nous souhaitons implémenter, champ par champ :

ChampCondition(s) de validité
NameChamp requis, chaîne de caractères de 1 à 25 lettres. Seules des lettres sont autorisées, pas de caractère spéciaux ou de nombre.
HPChamp requis, nombre entre 0 et 999.
CPChamp requis, nombre entre 0 et 99.
TypesChamp requis, l’utilisateur doit sélectionner entre 1 et 3 types différents dans une liste donnée.

Pour implémenter ce comportement, nous allons créer une méthode validateForm dont le rôle sera de vérifier chaque champ respecte les règles que nous avons établit. Ajoutez donc cette nouvelle méthode dans le formulaire pokemon-form.tsx :

  const validateForm = () => {
    let newForm: Form = form;
    
    // Validator name
    if(!/^[a-zA-Zàéè ]{3,25}$/.test(form.name.value)) {
      const errorMsg: string = 'Le nom du pokémon est requis (1-25).';
      const newField: Field = { value: form.name.value, error: errorMsg, isValid: false };
      newForm = { ...newForm, ...{ name: newField } };
    } else {
      const newField: Field = { value: form.name.value, error: '', isValid: true };
      newForm = { ...newForm, ...{ name: newField } };
    }

    // Validator hp
    if(!/^[0-9]{1,3}$/.test(form.hp.value)) {
      const errorMsg: string = 'Les points de vie du pokémon sont compris entre 0 et 999.';
      const newField: Field = {value: form.hp.value, error: errorMsg, isValid: false};
      newForm = { ...newForm, ...{ hp: newField } };
    } else {
      const newField: Field = { value: form.hp.value, error: '', isValid: true };
      newForm = { ...newForm, ...{ hp: newField } };
    }

    // Validator cp
    if(!/^[0-9]{1,2}$/.test(form.cp.value)) {
      const errorMsg: string = 'Les dégâts du pokémon sont compris entre 0 et 99';
      const newField: Field = {value: form.cp.value, error: errorMsg, isValid: false};
      newForm = { ...newForm, ...{ cp: newField } };
    } else {
      const newField: Field = { value: form.cp.value, error: '', isValid: true };
      newForm = { ...newForm, ...{ cp: newField } };
    }

    setForm(newForm);
    return newForm.name.isValid && newForm.hp.isValid && newForm.cp.isValid;
  }

Ce code est effrayant, je vous l’accorde. En fait, on utilise ici une fonctionnalité très puissante, mais bizarre, nommé les expressions régulières. Si vous ne savez pas ce que sait, il s’agit de définir un pattern, c’est-à-dire un moule, qui permet de tester des chaînes de caractère pour savoir si elles respectent ce moule, ou non. Par exemple, à la ligne 5, on définit une expression régulière, alias un moule, comme ceci : /^[a-zA-Zàéè ]{3,25}$.

Ce charabia incompréhensible signifie, « ok, je n’accepte que des chaînes de caractères de 3 à 25 caractères de long, et qui ne doivent contenir que des lettres minuscules ou majuscules, ou ‘à’, ‘é’ et ‘è' ». C’est étrange à première vue, par contre par rapport à notre problème de valider les données de nos champs, c’est parfait.

Pareil pour tester le champ de points de vie à la ligne 15, « /^[0-9]{1,3}$/ » signifie « je ne veux que des chiffres, et entre 1 et 3 de long. Par exemple: 10, 002, ou 999 vont fonctionner ». Parfait, on peut ainsi décrire chacune de nos règles de validation avec une expression régulière !

Ensuite, pour tester la validité d’un champ par rapport à une expression régulière, on utilise la méthode … test ! Voici que cela donne :

// Pour le champ "name" : 
/^[a-zA-Zàéè ]{3,25}$/.test(maValeur);

// Exemples de valeur pour le nom d'un pokémon : 
/^[a-zA-Zàéè ]{3,25}$/.test("Pikachu); // true
/^[a-zA-Zàéè ]{3,25}$/.test("Pikachu1"); // false !
/^[a-zA-Zàéè ]{3,25}$/.test(""); // false !!
/^[a-zA-Zàéè ]{3,25}$/.test("!!###$$"); // false !!!

Ensuite, ce que nous faisons n’est pas très compliqué. Si le champ est validé, alors on met à jour le state du formulaire pour signifier que cette nouvelle valeur est valide. Sinon, on crée un message d’erreur, on indique que le champ n’est plus valide, et on sauvegarde tout cela dans le state.

La dernière ligne 35 de cette méthode est intéressante. Une fois que notre state est à jour, on regarde si notre nouveau formulaire est valide ou non, et on retourne un booléen. Si on retourne vrai, cela signifie que les données du formulaire sont valides, et donc que l’on peut lancer le processus de soumission du formulaire. Sinon, et bien on ne fait rien, car les données actuelles ne nous conviennent pas, et il faut que l’utilisateur les mettent à jour.

Si vous cherchez sur internet, le terme correspondant à expression régulière en anglais est « regex ».

Avec tout ce qu’on a mis en place, on a une méthode de validation pour nos trois premiers champs. Il reste le champ le plus pénible, celui des types d’un pokémon. Pour cela, on va créer une autre méthode de validation dédiée, nommée isTypesValid, dans le formulaire pokemon-form.tsx :

  const isTypesValid = (type: string): boolean => {
    // Cas n°1: Le pokémon a un seul type, qui correspond au type passé en paramètre.
    // Dans ce cas on revoie false, car l'utilisateur ne doit pas pouvoir décoché ce type (sinon le pokémon aurait 0 type, ce qui est interdit)
    if (form.types.value.length === 1 && hasType(type)) {
      return false;
    }
    
    // Cas n°1: Le pokémon a au moins 3 types.
    // Dans ce cas il faut empêcher à l'utilisateur de cocher un nouveau type, mais pas de décocher les types existants.
    if (form.types.value.length >= 3 && !hasType(type)) { 
      return false; 
    } 
    
    // Après avoir passé les deux tests ci-dessus, on renvoie 'true', 
    // c'est-à-dire que l'on autorise l'utilisateur à cocher ou décocher un nouveau type.
    return true;
  }

Cette méthode s’occupe de renvoyer un booléen, pour savoir si une case à cocher doit être verrouillée ou non :

  • Si l’utilisateur a sélectionné une seule case, il faut l’empêcher de pouvoir désélectionner cette case. Sinon il pourrait choisir de ne renseigner aucun type pour un pokémon, et ce n’est pas ce que nous voulons.
  • Si l’utilisateur a déjà sélectionné trois cases, alors il faut l’empêcher de pouvoir sélectionner d’autres cases, mais il doit pouvoir désélectionner les types déjà présents s’il souhaite modifier son pokémon !

On fait aussi appel à la méthode hasType pour vérifier que nous ne verrouillons pas des cases que l’utilisateur a déjà coché, pour lui permettre de les désélectionner.

Ensuite, pour verrouiller les cases à cocher de la liste, il faut lier le résultat de la méthode isTypesValid à la propriété disabled, grâce à la liaison de propriété, à la ligne 7 ci-dessous :

{/* Pokemon types */}
<div className="form-group">
 <label>Types</label>
 {types.map(type => (
 <div key={type} style={{marginBottom: '10px'}}>
  <label>
   <input id={type} type="checkbox" name="types" className="filled-in" value={type} checked={hasType(type)} disabled={!isTypesValid(type)} onChange={e => selectType(type, e)}></input>
   <span>
    <p className={formatType(type)}>{ type }</p>
   </span>
  </label>
 </div>
 ))}
</div>

Lorsque la méthode isTypesValid renvoie false, alors la case à cocher est verrouillée et ne peut plus être sélectionnée ! C’est exactement le comportement souhaité !

Génial, on passe enfin à la soumission du formulaire, car c’est à ce moment que nous allons déclencher la validation. Si le formulaire est valide, rien à signaler, on redirige l’utilisateur vers la page de détail d’un pokémon pour le moment. Sinon, on empêche le formulaire d’être soumis :

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const isFormValid = validateForm(); // On vérifie que le formulaire est valide.
    if(isFormValid) {
      history.push(`/pokemons/${pokemon.id}`);
    }
  }

Nos règles de validation sont bien en place. Nous avons presque terminé le développement de notre formulaire. 👍

Bien sûr, la validation côté client n’est jamais suffisante si vous sauvegardez vos données sur un serveur distant. Pensez à valider les données aussi côté serveur, sous peine d’introduire d’importantes failles de sécurité dans votre application !