Étape 18 — Formulaire d'ajout de Pokémon
Séquence démo — pas requis dans le projet personnel
Les séquences 6 et 7 (formulaires, authentification, suppression) sont présentées en démonstration par l'enseignant. Ces concepts sont avancés et ne font pas partie des livrables du projet individuel. Ils seront toutefois abordés à l'oral.
Objectifs
- Créer un formulaire d'ajout avec
v-formet@submit.prevent - Valider les champs avec la prop
:rules - Envoyer les données au store (
addPokemon→axios.post) - Afficher un message de confirmation avec
v-snackbar - Rediriger vers l'accueil après un ajout réussi
Prérequis — Point de départ
Cette étape nécessite d'avoir terminé les étapes 1 à 17 (séquences 1-5). Si votre code n'est pas à jour, vous pouvez repartir de la branche etape-18-start :
# Ajouter le dépôt du prof (une seule fois)
git remote add prof https://github.com/fallinov/esig-141-pokedex-vuetify.git
# Récupérer les branches et basculer
git fetch prof
git checkout -b etape-18-start prof/etape-18-start
npm installRésultat attendu

Nouveaux composants Vuetify
| Composant | Rôle | Documentation |
|---|---|---|
v-form | Conteneur de formulaire avec validation intégrée | Forms |
v-text-field | Champ de saisie texte ou numérique | Text fields |
v-textarea | Champ de saisie multiligne | Textareas |
v-select | Liste déroulante (sélection simple ou multiple) | Selects |
v-snackbar | Message temporaire en bas de l'écran | Snackbars |
Tâches
1. Ajouter l'action dans le store
Dans src/stores/pokemonStore.js, ajoutez l'action addPokemon qui envoie les données à l'API via axios.post :
async addPokemon (pokemonData) {
// Validation basique
if (!pokemonData.name || !pokemonData.level) {
return {
success: false,
message: 'Le nom et le niveau du Pokémon sont obligatoires',
}
}
this.isLoading = true
try {
// Envoyer les données à l'API
const response = await api.post('/pokemons', pokemonData)
// Récupérer le Pokémon créé depuis la réponse
let newPokemon = null
if (response.data && response.data.data) {
newPokemon = response.data.data
} else if (response.data) {
newPokemon = response.data
}
// Ajouter le nouveau Pokémon à la liste locale
if (newPokemon) {
this.pokemons.push(newPokemon)
}
return {
success: true,
message: 'Pokémon ajouté avec succès !',
}
} catch (error) {
console.error('Erreur lors de l\'ajout du Pokémon:', error.message)
let errorMessage = 'Erreur lors de l\'ajout du Pokémon'
if (error.response && error.response.data && error.response.data.message) {
errorMessage = error.response.data.message
}
return {
success: false,
message: errorMessage,
}
} finally {
this.isLoading = false
}
},Cette action :
- Valide les données avant l'envoi
- Fait un
POSTvers/pokemonsvia Axios - Ajoute le Pokémon créé à la liste locale (pas besoin de tout recharger)
- Retourne un objet
{ success, message }utilisé par le formulaire
2. Créer la page src/pages/ajouter.vue
Créez un nouveau fichier src/pages/ajouter.vue. Grâce au file-based routing, la page sera automatiquement accessible sur /ajouter.
3. Construire le formulaire
Le formulaire utilise v-form avec @submit.prevent pour empêcher le rechargement de la page lors de la soumission.
Structure du formulaire :
<v-form ref="formRef" @submit.prevent="submitForm">
<!-- Champs ici -->
</v-form>Les champs à créer :
| Champ | Composant | v-model | Props importantes |
|---|---|---|---|
| Nom | v-text-field | form.name | label, :rules, variant="outlined" |
| Niveau | v-text-field | form.level | type="number", min="1", max="100" |
| Types | v-select | form.types | multiple, chips, :items="pokemonStore.types" |
| Description | v-textarea | form.description | rows="3" |
v-model.number pour les champs numériques
Utilisez v-model.number="form.level" pour que la valeur soit automatiquement convertie en nombre (et non en string).
4. Ajouter les règles de validation
Les règles de validation sont des tableaux de fonctions. Chaque fonction reçoit la valeur du champ et retourne true si la valeur est valide, ou un message d'erreur sinon.
// Chaque règle est une fonction : valeur → true ou message d'erreur
const nameRules = [
v => !!v || 'Le nom est obligatoire',
v => v.length >= 2 || 'Le nom doit contenir au moins 2 caractères',
]
const levelRules = [
v => !!v || 'Le niveau est obligatoire',
v => (v >= 1 && v <= 100) || 'Le niveau doit être entre 1 et 100',
]
const typesRules = [
v => v.length > 0 || 'Sélectionnez au moins un type',
]On passe ces règles au composant avec la prop :rules :
<v-text-field v-model="form.name" :rules="nameRules" />5. Soumettre le formulaire
Avant d'envoyer les données, on valide le formulaire via formRef.validate(). Cette méthode retourne { valid: true/false }.
async function submitForm () {
// Valider tous les champs
const { valid } = await formRef.value.validate()
if (!valid) return
// Envoyer au store (qui fait axios.post)
const result = await pokemonStore.addPokemon(form.value)
// Afficher le résultat dans le snackbar
snackbar.value = {
show: true,
message: result.message,
color: result.success ? 'success' : 'error',
}
// Rediriger après succès
if (result.success) {
setTimeout(() => router.push('/'), 1000)
}
}6. Ajouter le snackbar de confirmation
Le v-snackbar affiche un message temporaire après la soumission :
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>Validation côté client vs côté serveur
La validation avec :rules est une validation côté client (dans le navigateur). Elle améliore l'expérience utilisateur en donnant un retour immédiat, mais elle ne remplace jamais la validation côté serveur. Un utilisateur peut toujours contourner la validation côté client (DevTools, requête directe). L'API doit toujours re-vérifier les données.
Tests
- La page
/ajouteraffiche un formulaire avec 4 champs - Les champs vides affichent des messages d'erreur
- Un Pokémon valide est ajouté (vérifier sur la page d'accueil)
- Le snackbar affiche « Pokémon ajouté avec succès ! »
- La page redirige vers l'accueil après 1 seconde
Solution
src/pages/ajouter.vue
<template>
<v-container>
<h1 class="text-h3 text-center my-6">
Ajouter un Pokémon
</h1>
<!--
Formulaire d'ajout de Pokémon
* v-form avec ref pour accéder à la méthode validate()
* @submit.prevent empêche le rechargement de la page lors de la soumission
* max-width="600" et mx-auto centrent le formulaire
-->
<v-card
max-width="600"
class="mx-auto pa-6"
>
<v-form
ref="formRef"
@submit.prevent="submitForm"
>
<!--
Champ nom du Pokémon
* :rules applique les règles de validation
* Le champ est obligatoire et doit contenir au moins 2 caractères
-->
<v-text-field
v-model="form.name"
label="Nom du Pokémon"
:rules="nameRules"
variant="outlined"
class="mb-2"
/>
<!--
Champ niveau du Pokémon
* type="number" restreint la saisie aux chiffres
* v-model.number convertit automatiquement en nombre
* min="1" et max="100" définissent la plage autorisée
-->
<v-text-field
v-model.number="form.level"
label="Niveau"
type="number"
:rules="levelRules"
min="1"
max="100"
variant="outlined"
class="mb-2"
/>
<!--
Sélection des types
* multiple permet de sélectionner plusieurs types
* chips affiche les types sélectionnés sous forme de puces
* :items reçoit la liste des types depuis le store
-->
<v-select
v-model="form.types"
:items="pokemonStore.types"
item-title="name"
item-value="id"
label="Types"
:rules="typesRules"
multiple
chips
variant="outlined"
class="mb-2"
/>
<!--
Champ description
* v-textarea permet une saisie multiligne
* rows="3" définit la hauteur initiale
-->
<v-textarea
v-model="form.description"
label="Description"
rows="3"
variant="outlined"
class="mb-2"
/>
<!--
Boutons d'action
* Annuler ramène à la page d'accueil
* Ajouter soumet le formulaire
-->
<div class="d-flex justify-end ga-2">
<v-btn
variant="text"
to="/"
>
Annuler
</v-btn>
<v-btn
type="submit"
color="primary"
:loading="pokemonStore.isLoading"
>
Ajouter
</v-btn>
</div>
</v-form>
</v-card>
<!--
Snackbar de confirmation
* Affiche un message de succès ou d'erreur après la soumission
* timeout="3000" ferme automatiquement après 3 secondes
-->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.message }}
</v-snackbar>
</v-container>
</template>
<script setup>
import { usePokemonStore } from '@/stores/pokemonStore'
const pokemonStore = usePokemonStore()
const router = useRouter()
/**
* Référence vers le composant v-form
* Permet d'appeler formRef.validate() pour vérifier les règles
*/
const formRef = ref(null)
/**
* Données du formulaire
* Chaque propriété correspond à un champ du formulaire
*/
const form = ref({
name: '',
level: 1,
types: [],
description: '',
})
/**
* État du snackbar (message de confirmation)
*/
const snackbar = ref({
show: false,
message: '',
color: 'success',
})
/**
* Règles de validation pour le champ nom
* Chaque règle est une fonction qui retourne true (valide) ou un message d'erreur
*/
const nameRules = [
v => !!v || 'Le nom est obligatoire',
v => v.length >= 2 || 'Le nom doit contenir au moins 2 caractères',
]
/**
* Règles de validation pour le champ niveau
*/
const levelRules = [
v => !!v || 'Le niveau est obligatoire',
v => (v >= 1 && v <= 100) || 'Le niveau doit être entre 1 et 100',
]
/**
* Règles de validation pour la sélection des types
*/
const typesRules = [
v => v.length > 0 || 'Sélectionnez au moins un type',
]
/**
* Soumission du formulaire
* 1. Valide le formulaire avec les règles définies
* 2. Envoie les données au store (qui fait la requête POST)
* 3. Affiche un message de confirmation
* 4. Redirige vers la page d'accueil si succès
*/
async function submitForm () {
// Étape 1 : Valider le formulaire
const { valid } = await formRef.value.validate()
if (!valid) return
// Étape 2 : Envoyer les données au store
const result = await pokemonStore.addPokemon(form.value)
// Étape 3 : Afficher le résultat
snackbar.value = {
show: true,
message: result.message,
color: result.success ? 'success' : 'error',
}
// Étape 4 : Rediriger si succès
if (result.success) {
setTimeout(() => {
router.push('/')
}, 1000)
}
}
</script>Commit
git add -A
git commit -m "feat: formulaire d'ajout de Pokémon avec validation"