10. Correction : Ajouter un pokémon

J’espère que vous avez réussi à terminer cet exercice. Même si vous n’avez pas validé toutes les étapes, ce n’est pas grave car si vous en avez réussi une, deux ou trois, c’est déjà une excellente chose d’avoir essayé, et je tiens à vous féliciter.

Maintenant nous allons voir la correction de cet exercice étape par étape. Commençons tout de suite !

Tâche n°1 : Ajouter une méthode POST

Pour ajouter un nouveau Pokémon dans notre API Rest, nous allons avoir besoin d’une requête HTTP de type POST. C’est le type de requête dédié à l’ajout de nouveaux éléments. Bien sûr, l’API Fetch permet de faire tout cela. Ajoutez donc la fonction addPokemon dans le service pokemon-service.ts :

  static addPokemon(pokemon: Pokemon): Promise<Pokemon> {
    delete pokemon.created; // On supprime la date du pokémon, car c'est la méthode formatDate qui va s'en occuper.

    return fetch(`http://localhost:3001/pokemons`, {
      method: 'POST',
      body: JSON.stringify(pokemon),
      headers: { 'Content-Type': 'application/json'}
    })
    .then(response => response.json())
    .catch(error => this.handleError(error));
  }

Le code d’ajout est très similaire à ce qu’on a déjà vu, à la différence près qu’ici nous utilisons requête de type POST, à la ligne 4. Bon, ça c’est fait, passons à la suite !

Tâche n°2 : Créer une nouvelle page d’ajout d’un pokémon

Comme nous sommes des développeurs sérieux, nous allons créer un nouveau composant qui aura pour rôle de gérer l’ajout d’un Pokémon (et non en ajoutant du code dans des fichiers existants, avec des NgIf ou autre. Et oui, je vous ai vu venir ! 😉)

Le composant pour ajouter un pokémon se nommera pokemon-add.tsx, que je vous invite à ajouter dans le dosser pages :

import React, { FunctionComponent, useState } from 'react';
import PokemonForm from '../components/pokemon-form';
import Pokemon from '../models/pokemon';
 
const PokemonAdd: FunctionComponent = () => {

  const [id] = useState<number>(new Date().getTime());
  const [pokemon] = useState<Pokemon>(new Pokemon(id));
   
  return (
    <div className="row">
      <h2 className="header center">Ajouter un pokémon</h2>
      <PokemonForm pokemon={pokemon}></PokemonForm>
    </div>
  );
}
 
export default PokemonAdd;

Ce composant est désormais présent dans l’arborescence de notre projet, mais il n’est pas encore relié à l’application. Il faut donc ensuite le déclarer auprès du routeur de React, dans le composant App.tsx :

// Les importations
import PokemonAdd from './pages/pokemon-add';
 
const App: FunctionComponent = () => {
 
  return (
    <Router>
      <div>
      <nav>...</nav>
      <Switch>
        <Route exact path="/" component={PokemonsList} />
        <Route exact path="/pokemons" component={PokemonsList} />
        <Route exact path="/pokemon/add" component={PokemonAdd} />
        <Route exact path="/pokemons/edit/:id" component={PokemonEdit} />
        <Route path="/pokemons/:id" component={PokemonsDetail} />
        <Route component={PageNotFound} />
      </Switch>
      </div>
    </Router>
  );
}
 
export default App;

Maintenant notre composant est accessible à une URL donnée pour nos utilisateurs. Cette route sera pokemon/add.

Tâche n°3 : Ajouter un lien vers le formulaire d’ajout

Ensuite, nous allons simplement ajouter un petit bouton sur la page qui affiche la liste des Pokémons. Ce bouton permettra de rediriger l’utilisateur vers le formulaire d’ajout que nous venons de créer.

On prévoit un bouton pour ajouter un nouveau pokémon.

Ajouter donc le code suivant à la fin du composant pokemon-list.tsx :

// Les importations
import { Link } from 'react-router-dom';

const PokemonList: FunctionComponent = () => {
  // ...

  return (
    <div>
      <h1 className="center">Pokédex</h1>
      <div className="container"> 
        ...
      </div>
      <Link className="btn-floating btn-large waves-effect waves-light red z-depth-3"
        style={{position: 'fixed', bottom: '25px', right: '25px'}}
        to="/pokemon/add">
        <i className="material-icons">add</i>
      </Link>
    </div> 
  );
}

export default PokemonList;

Tâche n°4 : Distinguer l’ajout ou l’édition d’un pokémon

Afin de pouvoir gérer le cas d’ajout d’un pokémon, nous devons adapter notre formulaire d’édition. Pour cela, nous devons détecter si notre formulaire est en mode édition ou ajout, puisque que quand l’utilisateur va soumettre le formulaire, le comportement sera différent.

Hey, mais comment on va détecter si on doit ajouter ou éditer un pokémon nous ? Côté développement ? On ne peut pas deviner ce que l’utilisateur a dans la tête…

Vous avez raison.

Effectivement, on ne peut pas deviner ce que les utilisateurs ont dans la tête, mais on peut deviner ses intentions. Et cela on peut le savoir facilement, grâce à une prop. Si depuis la page d’édition d’un pokémon, on passe la propriété d’entrée isEditForm avec la valeur true, alors l’utilisateur souhaite éditer un pokémon. Et si on passe une autre propriété d’entrée isEditForm avec la valeur false, alors l’utilisateur veut ajouter un nouveau pokémon dans l’application. Avouez que vous n’y aviez pas pensé, si ? 😁

On va donc avoir besoin d’une nouvelle prop, que l’on va commencer à mettre en place sur la page d’édition d’un pokémon pokemon-edit.tsx :

// Les importations
 
const PokemonEdit: FunctionComponent<RouteComponentProps<Params>> = ({ match }) => {
  // ...
   
  return (
    <div>
      { pokemon ? (
        <div className="row">
            <h2 className="header center">Éditer { pokemon.name }</h2>
            <PokemonForm pokemon={pokemon} isEditForm={true}></PokemonForm>
        </div>
      ) : (
        <h4 className="center">Aucun pokémon à afficher !</h4>
      )}
    </div>
  );
}
 
export default PokemonEdit;

Et ensuite on effectue la même chose dans le composant pokemon-add.tsx :

// Les importations
 
const PokemonAdd: FunctionComponent = () => {
  // ...
   
  return (
    <div className="row">
      <h2 className="header center">Ajouter un pokémon</h2>
      <PokemonForm pokemon={pokemon} isEditForm={false}></PokemonForm>
    </div>
  );
}
 
export default PokemonAdd;

Maintenant notre formulaire est capable de savoir si il doit éditer ou modifier un Pokémon ! Enfin, il faut encore l’adapter pour qu’il puisse gérer ces deux cas.

Tâche n°5 : Adapter le formulaire d’édition

Le but de cette cinquième et dernière tâche va être d’adapter notre formulaire d’édition pour qu’il puisse également permettre d’ajouter un nouveau pokémon dans notre application. Il y a quelques différences à prendre en compte :

  • Tout d’abord, récupérer la prop nommée isEditForm, pour savoir si il faut ajouter ou éditer un pokémon.
  • Ensuite, il faut ajouter un nouveau champ nommé Picture pour que l’utilisateur puisse proposer une nouvelle image à son nouveau pokémon, dans le cas d’un ajout.
  • Aussi, qui dit nouveau champ, dit nouvelle règle de validation pour ce champ. 👍
  • Quant au template de notre formulaire, il va également subir des modifications. Premièrement, pour l’ajout nous devons afficher notre nouveau champ dédié à l’image du pokémon. Ensuite, dans le formulaire d’édition nous affichons l’image du Pokémon. Cependant, le pokémon n’a pas d’image par défaut dans le cas d’un ajout. Il faut donc afficher cette image du pokémon seulement dans le cas de l’édition.
  • Pour terminer, il faut gérer la soumission du formulaire différemment dans le cas d’un ajout ou de l’édition. Dans le cas de l’édition nous n’avons rien à modifier, par contre pour l’ajout d’un pokémon, il faut appeler la méthode d’ajout de pokémons de notre service, que nous avons développé lors de la première tâche.

Vous trouverez la liste des modifications que j’ai apportées au formulaire pokemon-form.tsx ci-dessous. Attention, il y en un paquet !

import React, { FunctionComponent, useState } from 'react';
import { useHistory } from 'react-router-dom';
import Pokemon from '../models/pokemon';
import formatType from '../helpers/format-type';
import PokemonService from '../services/pokemon-service';

type Props = {
  pokemon: Pokemon,
  isEditForm: boolean
};

// ...

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

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

  const history = useHistory();

  const [form, setForm] = useState<Form>({
    picture: { value: pokemon.picture },
    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 validateForm = () => {
    let newForm: Form = form;

    // Validator url
    if(isAddForm()) {

      const start = "https://assets.pokemon.com/assets/cms2/img/pokedex/detail/";
      const end = ".png";

      if(!form.picture.value.startsWith(start) || !form.picture.value.endsWith(end)) {
        const errorMsg: string = 'L\'url n\'est pas valide.';
        const newField: Field = { value: form.picture.value, error: errorMsg, isValid: false };
        newForm = { ...newForm, ...{ picture: newField } };
      } else {
        const newField: Field = { value: form.picture.value, error: '', isValid: true };
        newForm = { ...newForm, ...{ picture: newField } };
      }
    }

    // ...

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

  // ...

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const isFormValid = validateForm();
    if(isFormValid) {
      pokemon.picture = form.picture.value;
      pokemon.name = form.name.value;
      pokemon.hp = form.hp.value;
      pokemon.cp = form.cp.value;
      pokemon.types = form.types.value;
      isEditForm ? updatePokemon() : addPokemon();
    }
  }

  const isAddForm = (): boolean => {
    return !isEditForm;
  }

  const addPokemon = () => {
    PokemonService.addPokemon(pokemon).then(() => history.push(`/pokemons`));
  }

  const updatePokemon = () => {
    PokemonService.updatePokemon(pokemon).then(() => history.push(`/pokemons/${pokemon.id}`));
  }

  return (
    <form onSubmit={(e) => handleSubmit(e)}>
      <div className="row">
        <div className="col s12 m8 offset-m2">
          <div className="card hoverable"> 
            {isEditForm && (
            <div className="card-image">
              <img src={pokemon.picture} alt={pokemon.name} style={{width: '250px', margin: '0 auto'}}/>
              <span className="btn-floating halfway-fab waves-effect waves-light">
                <i onClick={deletePokemon} className="material-icons">delete</i>
              </span>
            </div>
            )}
            <div className="card-stacked">
              <div className="card-content">
                {/* Pokemon picture */}
                {isAddForm && (
                  <div className="form-group">
                    <label htmlFor="picture">Image</label>
                    <input id="picture" type="text" name="picture" className="form-control" value={form.picture.value} onChange={e => handleInputChange(e)}></input>
                    {/* error */}
                    {form.picture.error &&
                      <div className="card-panel red accent-1"> 
                    {form.picture.error} 
                  </div>} 
                </div>
                )}
                {/* Pokemon name */}
                ...
                {/* Pokemon hp */}
                ...
                {/* Pokemon cp */}
                ...
                {/* Pokemon types */}
                ...
              </div>
              <div className="card-action center">
                {/* Submit button */}
                <button type="submit" className="btn">Valider</button>
              </div>
            </div>
          </div>
        </div>
      </div>
    </form>
  );
};
 
export default PokemonForm;

Prenez quelques instants pour consulter et mieux comprendre le code. Il n’y a rien de compliqué en soi, simplement tout ces changements sont peut-être indigeste pour vous, c’est pour cela que vous devez prendre un peu de temps si il y a des zones d’ombre pour vous.

Je vous invite maintenant à vous rendre dans votre navigateur, pour admirer notre nouvelle fonctionnalité en action. Allez-y, ajoutez autant de pokémons que vous voulez. Voici ce que j’ai fait de mon côté, je vous donne cinq secondes pour trouver les nouveaux pokémons que j’ai ajouté 😇 :

Je me suis permis d’ajouter deux nouveaux pokémons dans mon application. Les trouverez-vous ? 😉

Voilà, notre formulaire est maintenant capable de traiter les cas d’ajout et d’édition ! Félicitations ! 💪🎊🍾