5. Initialiser un état pour le formulaire

Pour le moment, nous avons ajouter un simple formulaire dans notre application, qui s’occupe seulement de l’affichage. Il faut donc envisager d’enrichir ce formulaire, en initialisant les champs avec les valeurs du pokémons à éditer. En regardant le formulaire ci-dessous, vous devriez mieux comprendre :

On définit des valeurs initiales pour notre formulaire.

Comme vous pouvez le voir, le champ name est initialisé avec la valeur Piafabec, le champ points de vie est initialisé avec la valeur 14, et ainsi de suite. En effet, comme il s’agit d’un formulaire d’édition, il est question d’éditer des valeurs déjà existantes. Nous allons donc devoir faire plusieurs choses :

  • Initialiser un state pour notre formulaire, représentant les différents champs avec les données du bon pokémon.
  • Pousser ces valeurs du state vers les différents champs du formulaire.

Nous allons donc ajouter ces nouvelles fonctionnalités dans notre formulaire pokemon-form.tsx :

import React, { FunctionComponent, useState } from 'react';
import Pokemon from '../models/pokemon';
import formatType from '../helpers/format-type';

type Props = {
  pokemon: Pokemon
};

type Field = {
  value: any,
  error?: string,
  isValid?: boolean  
};

type Form = {
  name: Field,
  hp: Field,
  cp: Field,
  types: Field
}

const PokemonForm: FunctionComponent<Props> = ({pokemon}) => {

  const [form, setForm] = useState<Form>({
    name: { value: pokemon.name, isValid: true },
    hp: { value: pokemon.hp, isValid: true },
    cp: { value: pokemon.cp, isValid: true },
    types: { value: pokemon.types, isValid: true }
  });

  const types: string[] = [
    'Plante', 'Feu', 'Eau', 'Insecte', 'Normal', 'Electrik',
    'Poison', 'Fée', 'Vol', 'Combat', 'Psy'
  ];

  const hasType = (type: string): boolean => {
    return form.types.value.includes(type);
  }
 
  return (
    <form>
      <div className="row">
        <div className="col s12 m8 offset-m2">
          <div className="card hoverable"> 
            <div className="card-image">
              <img src={pokemon.picture} alt={pokemon.name} style={{width: '250px', margin: '0 auto'}}/>
            </div>
            <div className="card-stacked">
              <div className="card-content">
                {/* 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}></input>
                </div>
                {/* Pokemon hp */}
                <div className="form-group">
                  <label htmlFor="hp">Point de vie</label>
                  <input id="hp" type="number" name="hp" className="form-control" value={form.hp.value}></input>
                </div>
                {/* Pokemon cp */}
                <div className="form-group">
                  <label htmlFor="cp">Dégâts</label>
                  <input id="cp" type="number" name="cp" className="form-control" value={form.cp.value}></input>
                </div>
                {/* 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)}></input>
                        <span>
                          <p className={formatType(type)}>{ type }</p>
                        </span>
                      </label>
                    </div>
                  ))}
                </div>
              </div>
              <div className="card-action center">
                {/* Submit button */}
                <button type="submit" className="btn">Valider</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </form>
  );
};
 
export default PokemonForm;

La première chose que nous faisons à la ligne 1, c’est d’importer le hook d’état useState. C’est logique, car nous allons avoir besoin de définir un état pour modéliser les champs de notre formulaire.

On déclare ensuite deux nouveaux types pour notre formulaire. De la ligne 9 à 13, on déclare le type Field, pour modéliser un champ de notre formulaire. Chaque champ aura une valeur, un message d’erreur potentiel, et une propriété indiquant si la donnée saisie dans le champ est valide ou non. Ensuite, on déclare le type Form de la ligne 15 à 20. Cela représente le formulaire à proprement parler, avec la liste des champs disponibles. En combinant ces nouveaux types Field et Form, cela nous permet de structurer le state qui sera utilisé par notre formulaire d’édition.

De la ligne 24 à 29, on déclare enfin notre state qui représente les champs et les données de notre formulaire. Par défaut, on initialise la valeur de chaque champ avec les données initial du pokémon reçu en propriété d’entrée, c’est-à-dire en tant que prop. On définit également chaque champ comme valide, car les données du pokémon sont correctes de base, et nous ne voulons pas déclencher des erreurs dès le chargement du formulaire :

  const [form, setForm] = useState<Form>({
    name: { value: pokemon.name, isValid: true },
    hp: { value: pokemon.hp, isValid: true },
    cp: { value: pokemon.cp, isValid: true },
    types: { value: pokemon.types, isValid: true }
  });

Une fois notre state en place, nous allons passer à la deuxième étape, qui consiste à pousser les données du state vers les différents champs du formulaire.

Pousser le state vers les champs du formulaire

L’objectif est simple, pour chaque champ déclaré dans le state, nous allons devoir afficher la valeur de ce champ à l’utilisateur.

Je vais commencer avec les champs les plus simples, c’est-à-dire ceux de type text et number, à savoir les champs name, hp et cp. Pour cela, on effectue la même opération à la ligne 53, 58 et 63. On ajoute un attribut value sur le champ, et on lui associe une expression JSX renvoyant la valeur du champ de notre formulaire correspondant, soit form.name.value :

<input id="name" type="text" name="name" className="form-control" value={form.name.value}></input>

Ainsi, au chargement du formulaire, le state est initialisé avec un objet Form représentant le formulaire, est ses données sont poussées vers les champs correspondants. Parfait de ce côté là !

Le problème est légèrement plus compliqué pour le champ représentant les types d’un pokémon. En effet, pour tous les types affichés dans le formulaire, il faut cocher uniquement ceux du pokémon. On va donc avoir besoin d’un petite méthode outil, qui prend un type en paramètre, et nous renvoie si ce type appartient au pokémon ou non. Si c’est le cas, nous cochons la case du type, sinon nous laissons la case décoché.

Cette méthode « outil » se nomme hasType, de la ligne 36 à 38 :

const hasType = (type: string): boolean => {
  return form.types.value.includes(type);
}

Cette méthode renvoie un simple booléen, permettant de savoir si le type passé en paramètre appartient ou non au pokémon. On utilise la méthode JavaScript native includes pour déterminer si un type appartient au tableau de types d’un pokémon.

On utilise ensuite cette méthode à la ligne 71, afin de pré-cocher chaque checkbox correspondant aux types que possède le pokémon courant, grâce à l’attribut checked et à la méthode hasType :

<input id={type} type="checkbox" name="types" className="filled-in" value={type} checked={hasType(type)}></input>

Vous pouvez retourner dans votre application, et admirez le résultat. Votre formulaire sera bien initialisé avec les valeurs du pokémon demandé. C’est une bonne étape de faite !

Il nous reste cependant une autre fonctionnalité importante à ajouter. Lorsque vos utilisateurs vont modifier les données d’un pokémon, par exemple son nom, il va falloir pousser ces modifications dans le state de notre formulaire. Cela nous permettra d’avoir toujours la dernière version des données disponibles depuis le state. Et c’est ce que nous allons faire dans la prochaine étape.