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 !
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 !
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.
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.
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;
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.
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 :
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é 😇 :
Voilà, notre formulaire est maintenant capable de traiter les cas d’ajout et d’édition ! Félicitations ! 💪🎊🍾