6. Modifier l’état du formulaire en fonction des interactions utilisateurs

Précédemment, nous avons relié le state à notre formulaire, et maintenant nous devons faire l’inverse, c’est-à-dire relier notre formulaire au state. À chaque fois que l’utilisateur va modifier les données d’un pokémon, il faudra prendre en compte ces modifications dans le state. Pour commencer, nous allons donc mettre en place une méthode permettant d’enregistrer les modifications des champs name, hp et cp apportées au formulaire pokemon-form.tsx :

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
  const fieldName: string = e.target.name;
  const fieldValue: string = e.target.value;
  const newField: Field = {[fieldName]: { value: fieldValue }};

  setForm({...form, ...newField});
}

Le rôle de cette méthode est simple, elle va réagir à un événement déclenché par l’utilisateur, à chaque fois qu’il va modifier les données des champ name, hp ou cp du pokémon. Nous repérons le nom du champ modifié à la ligne 2, ainsi que la nouvelle valeur saisie par l’utilisateur à la ligne 3. Ensuite, on regroupe ces modifications dans un nouvel objet newField.

Par exemple, à la base le champ correspondant au nom du pokémon a comme valeur « Piafabec ». L’utilisateur interagit avec ce champ, et supprime la dernière lettre. On obtiens alors la valeur « Piafabe ». Dans ce cas, fieldName vaut toujours « name », et fieldValue vaut « Piafabe ».

Ensuite à la ligne 6, on modifie l’état de notre formulaire grâce à la méthode setForm fournit par le Hook d’état. Remarquez bien ce que l’on passe en paramètre. On ne se content pas de passer notre nouveau champ newField, car la méthode setForm attend un nouveau state complet pour le formulaire. Il faut donc recréer tout un state pour notre formulaire, en appliquant dessus la modification apporté par l’utilisateur. Pour cela, on utilise une petite astuce d’ECMAScript 6, le spread operator, afin de fusionner deux objets.

Prenons un petit cas d’exemple, pour démystifier tout cela. 🤯

Voici deux objets représentant des personnes, qui n’ont rien de bien méchants :

const object1 = { name: 'John', age: 33 };
const object2 = { name: 'Joe', job: 'developer' };

Maintenant, on va essayé de fusionner ces deux objets :

const fusion = { ...object1, ...object2 };
console.log(fusion); // { name: 'Joe', age: 33, job: 'developer' };

Comme vous pouvez le constater, on fusionne les propriétés des deux objets, afin de créer un « super-objet ». Mais, dans le cas où les deux objets on des propriétés communes, comme name dans notre cas, alors c’est la propriété de l’objet de droite qui « écrase » la propriété de l’autre objet. C’est pour cela que notre objet final fusion possède trois propriétés et non quatre, alors que les deux objets de départ avait deux propriétés chacun.

Maintenant, vous l’avez compris, avec {…form, …newFields}, on obtiens un nouveau state qui est une copie de l’ancien, avec en plus la modification apporté par l’utilisateur. On peut ensuite passer ce nouveau state à la méthode setForm. React s’occupe ensuite de mettre à jour le state pour nous. C’est parfait !

Il ne nous reste plus qu’a brancher notre superbe méthode handleInputChange sur chaque champ de notre formulaire, lorsque l’utilisateur effectue une modification, grâce à l’événement onChange :

{/* Pokemon name */}
<div className="form-group">
 <label htmlFor="name">Nom</label>
 <input id="name" type="text" name="name" className="form-control" value={form.name.value} onChange={e => handleInputChange(e)}></input>
</div>

Vous devez ensuite lié l’événement onChange avec la méthode handleInputChange pour les champs hp et cp, le fonctionnement est identique.

On vient de faire d’une pierre… trois coups ! 👊👊👊

On vient donc de relier pas mois de trois champs au state de notre formulaire, mais il nous reste encore un petit problème… le champ types. En effet, lorsque l’utilisateur va cocher ou décocher un type, il va falloir mettre à jour notre state en conséquence. A priori, ce n’est pas une mince à faire.

En fait, à chaque fois que l’utilisateur interagit avec les types d’un pokémon, nous allons devoir déterminer si l’utilisateur a coché ou décoché une case, et quel type il a modifié. Ensuite il faudra mettre à jour le state en conséquence. Pour cela, nous allons nous faire aider par une méthode selectType. Ajouter donc cette méthode dans le composant pokemon-form.tsx :

  const selectType = (type: string, e: React.ChangeEvent<HTMLInputElement>): void => {
    const checked = e.target.checked;
    let newField: Field;

    if(checked) {
      // Si l'utilisateur coche un type, à l'ajoute à la liste des types du pokémon.
      const newTypes: string[] = form.types.value.concat([type]);
      newField = { value: newTypes };
    } else {
      // Si l'utilisateur décoche un type, on le retire de la liste des types du pokémon.
      const newTypes: string[] = form.types.value.filter((currentType: string) => currentType !== type);
      newField = { value: newTypes };
    }

    setForm({...form, ...{ types: newField }});
  }

Dans cette méthode, on récupère une information capitale depuis l’événement reçu en paramètre, à savoir si la case a été coché ou décoché, à la ligne 2. Si la case a été coché, alors on ajoute le nouveau type sélectionné aux types existants du pokémon, en créant un nouveau tableau avec les types à jour pour le pokémon. On fait cela grâce à la méthode JavaScript concact, qui permet de fusionner deux tableaux, à la ligne 7. On met ensuite à jour le state de notre formulaire.

On effectue une opération similaire lorsque l’utilisateur décoche un type. Cependant cette fois, on retire le type concerné du tableau des types d’un pokémon, grâce à la méthode filter à la ligne 11, qui renvoie un nouveau tableau sans le type qui a été décoché. On met ensuite à jour le state de notre formulaire également.

Ensuite, comme d’habitude, on branche la méthode selectType sur le champ types de notre formulaire, grâce à l’événement onChange, à la ligne 7 :

{/* 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)} onChange={e => selectType(type, e)}></input>
    <span>
     <p className={formatType(type)}>{ type }</p>
    </span>
   </label>
  </div>
 ))}
</div>

Maintenant, vous pouvez retourner dans votre application, interagir avec tous les champs de votre formulaire, puis cliquer sur « Valider ». Et… il ne se passera rien, car nous n’avons pas encore géré la soumission du formulaire. Je crois que nous avons encore un peu de boulot !