Skip to content

É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-form et @submit.prevent
  • Valider les champs avec la prop :rules
  • Envoyer les données au store (addPokemonaxios.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 :

bash
# 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 install

Voir la branche sur GitHub

Résultat attendu

Formulaire d'ajout de Pokémon

Nouveaux composants Vuetify

ComposantRôleDocumentation
v-formConteneur de formulaire avec validation intégréeForms
v-text-fieldChamp de saisie texte ou numériqueText fields
v-textareaChamp de saisie multiligneTextareas
v-selectListe déroulante (sélection simple ou multiple)Selects
v-snackbarMessage temporaire en bas de l'écranSnackbars

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 :

js
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 POST vers /pokemons via 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 :

vue
<v-form ref="formRef" @submit.prevent="submitForm">
  <!-- Champs ici -->
</v-form>

Les champs à créer :

ChampComposantv-modelProps importantes
Nomv-text-fieldform.namelabel, :rules, variant="outlined"
Niveauv-text-fieldform.leveltype="number", min="1", max="100"
Typesv-selectform.typesmultiple, chips, :items="pokemonStore.types"
Descriptionv-textareaform.descriptionrows="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.

js
// 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 :

vue
<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 }.

js
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 :

vue
<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 /ajouter affiche 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
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

bash
git add -A
git commit -m "feat: formulaire d'ajout de Pokémon avec validation"

Documentation pour les cours de développement web