Aller au contenu

Les Formulaires Vue : v-model et Two-Way Binding

Jusqu'ici, tes données vont dans un seul sens : du script vers le template. C'est bien, mais quand l'utilisateur tape dans un input, comment tu récupères ce qu'il a écrit ? 🤔

Avec v-model, Vue crée une synchronisation magique entre l'input et ta ref. L'utilisateur tape → la ref se met à jour. La ref change → l'input se met à jour. C'est le two-way binding.

Objectifs du chapitre

À la fin de ce chapitre, tu seras capable de :

  • ✅ Comprendre le two-way binding (liaison bidirectionnelle)
  • ✅ Utiliser v-model sur tous les types d'inputs
  • ✅ Créer des formulaires interactifs
  • ✅ Filtrer des données selon les choix de l'utilisateur

C'est quoi le Two-Way Binding ?

One-way binding (ce qu'on a fait jusqu'ici) :

ref → template (affichage uniquement)

Two-way binding (avec v-model) :

ref ↔ input (synchronisation dans les deux sens)

Sans v-model (la méthode longue)

<script setup>
import { ref } from 'vue'

const message = ref('')

function updateMessage(event) {
  message.value = event.target.value
}
</script>

<template>
  <input :value="message" @input="updateMessage" />
</template>

Ça marche, mais c'est verbeux. 😩

Avec v-model (la magie)

<script setup>
import { ref } from 'vue'

const message = ref('')
</script>

<template>
  <input v-model="message" />
</template>

Une ligne. C'est tout. Vue fait le :value et le @input pour toi automatiquement.

V-model sur les différents types d’inputs

Input Text : Le plus courant

Exemple : Preview live

<script setup>
import { ref } from 'vue'

const username = ref('')
</script>

<template>
  <div>
    <input 
      v-model="username" 
      placeholder="Entre ton pseudo"
      class="border rounded px-3 py-2"
    />
    
    <p class="mt-4">
      Bienvenue, <strong>{{ username || '...' }}</strong> !
    </p>
  </div>
</template>

Ce qui se passe :

  1. L'utilisateur tape "Alice"
  2. username.value devient "Alice" automatiquement
  3. Le {{ username }} dans le template se met à jour
  4. Tout ça sans écrire une seule fonction !

Textarea : Pareil mais multilignes

<script setup>
import { ref } from 'vue'

const bio = ref('')
</script>

<template>
  <textarea 
    v-model="bio" 
    placeholder="Parle-nous de toi..."
    rows="4"
    class="border rounded px-3 py-2 w-full"
  ></textarea>
  
  <p class="text-sm text-gray-500">
    {{ bio.length }} caractères
  </p>
</template>

⚠️ Attention : Pour <textarea>, on utilise v-model, pas le contenu entre les balises !

<!-- ❌ FAUX -->
<textarea>{{ bio }}</textarea>

<!-- ✅ CORRECT -->
<textarea v-model="bio"></textarea>

Select : Les dropdowns

Exemple : Sélection simple

<script setup>
import { ref } from 'vue'

const selectedColor = ref('')
</script>

<template>
  <select v-model="selectedColor" class="border rounded px-3 py-2">
    <option value="">-- Choisir une couleur --</option>
    <option value="red">Rouge</option>
    <option value="green">Vert</option>
    <option value="blue">Bleu</option>
  </select>
  
  <p v-if="selectedColor">
    Tu as choisi : {{ selectedColor }}
  </p>
</template>

Exemple : Options dynamiques (avec v-for)

<script setup>
import { ref } from 'vue'

const selectedCountry = ref('')
const countries = ['France', 'Belgique', 'Suisse', 'Canada']
</script>

<template>
  <select v-model="selectedCountry" class="border rounded px-3 py-2">
    <option value="">-- Choisir un pays --</option>
    <option v-for="country in countries" :key="country" :value="country">
      {{ country }}
    </option>
  </select>
</template>

Checkbox : Cases à cocher

Checkbox unique (toggle on/off)

Quand tu as une seule checkbox, v-model stocke un booléen.

<script setup>
import { ref } from 'vue'

const acceptTerms = ref(false)
</script>

<template>
  <label class="flex items-center gap-2">
    <input type="checkbox" v-model="acceptTerms" />
    J'accepte les conditions générales
  </label>
  
  <button 
    :disabled="!acceptTerms"
    :class="acceptTerms ? 'bg-blue-500' : 'bg-gray-300'"
    class="mt-4 px-4 py-2 rounded text-white"
  >
    Continuer
  </button>
</template>

Checkboxes multiples (sélection)

Quand tu as plusieurs checkboxes liées au même v-model, il stocke un tableau.

<script setup>
import { ref } from 'vue'

const selectedSkills = ref([])
const skills = ['Vue', 'React', 'Tailwind', 'Figma', 'Photoshop']
</script>

<template>
  <div class="space-y-2">
    <label v-for="skill in skills" :key="skill" class="flex items-center gap-2">
      <input type="checkbox" :value="skill" v-model="selectedSkills" />
      {{ skill }}
    </label>
  </div>
  
  <p class="mt-4">
    Sélectionnés : {{ selectedSkills.join(', ') || 'Aucun' }}
  </p>
</template>

Important : Chaque checkbox a un :value différent, mais elles partagent le même v-model.

Radio : Choix unique

Les radio buttons servent quand l'utilisateur doit choisir une seule option parmi plusieurs.

<script setup>
import { ref } from 'vue'

const selectedPlan = ref('')
const plans = [
  { id: 'free', name: 'Gratuit', price: '0€' },
  { id: 'pro', name: 'Pro', price: '9€/mois' },
  { id: 'enterprise', name: 'Enterprise', price: '29€/mois' }
]
</script>

<template>
  <div class="space-y-2">
    <label 
      v-for="plan in plans" 
      :key="plan.id" 
      class="flex items-center gap-2 p-3 border rounded cursor-pointer hover:bg-gray-50"
      :class="{ 'border-blue-500 bg-blue-50': selectedPlan === plan.id }"
    >
      <input type="radio" :value="plan.id" v-model="selectedPlan" />
      <span class="font-medium">{{ plan.name }}</span>
      <span class="text-gray-500">- {{ plan.price }}</span>
    </label>
  </div>
</template>

Les Modifiers : Petits bonus utiles

Modifier Effet Exemple
.trim Supprime les espaces début/fin v-model.trim="name"
.number Convertit en nombre v-model.number="age"
.lazy Update au blur au lieu de input v-model.lazy="email"

Exemples

<script setup>
import { ref } from 'vue'

const name = ref('')
const age = ref(0)
const email = ref('')
</script>

<template>
  <!-- .trim : pas d'espaces parasites -->
  <input v-model.trim="name" placeholder="Nom" />
  
  <!-- .number : toujours un nombre, pas une string -->
  <input v-model.number="age" type="number" placeholder="Âge" />
  
  <!-- .lazy : update seulement quand on quitte le champ -->
  <input v-model.lazy="email" type="email" placeholder="Email" />
</template>

Importer des données JSON avec Vite

Pour les exercices suivants, tu auras besoin d'importer des données depuis un fichier JSON. Avec Vite, c'est ultra simple !

Créer le fichier JSON

// src/data/products.json
[
  {
    "id": 1,
    "name": "MacBook Pro",
    "category": "laptop",
    "price": 2399
  },
  {
    "id": 2,
    "name": "iPhone 15",
    "category": "phone",
    "price": 999
  }
]

Importer dans ton composant

<script setup>
// Import direct du JSON - Vite gère ça automatiquement !
import products from '@/data/products.json'

// products est maintenant un tableau utilisable
console.log(products) // [{ id: 1, name: "MacBook Pro", ... }, ...]
</script>

C'est tout ! Pas de fetch, pas de async/await, Vite transforme le JSON en objet JavaScript directement.

Exercices pratiques

Exercice 1 : Live Preview Card ⭐

Objectif : Créer une carte de visite qui se met à jour en temps réel

Ta mission : Créer un composant BusinessCard.vue avec :

  • Un input pour le nom
  • Un input pour le job title
  • Un textarea pour la bio
  • Une preview de la carte qui se met à jour en live

Exemple d'utilisation :

<BusinessCard />

Rendu attendu :

Exercice 2 : Theme Switcher ⭐⭐

Objectif : Changer le thème de la page avec un select

Ta mission : Créer un composant ThemeSwitcher.vue avec :

  • Un <select> avec 3 thèmes : Light, Dark, Colorful
  • Le fond et les couleurs de texte changent selon le thème sélectionné
  • Un aperçu avec du contenu (titre, paragraphe, bouton)

Coup de pouce - Structure suggérée :

<script setup>
import { ref } from 'vue'

const selectedTheme = ref('light')

// Définir les classes pour chaque thème
const themes = {
  light: {
    bg: 'bg-white',
    text: 'text-gray-800',
    button: 'bg-blue-500 text-white'
  },
  dark: {
    bg: 'bg-gray-900',
    text: 'text-white',
    button: 'bg-purple-500 text-white'
  },
  colorful: {
    bg: 'bg-gradient-to-br from-pink-500 to-yellow-500',
    text: 'text-white',
    button: 'bg-white text-pink-500'
  }
}
</script>

<template>
  <div>
    <!-- Select pour choisir le thème -->
    <select v-model="selectedTheme">
      <!-- ... options ... -->
    </select>
    
    <!-- Aperçu avec les classes dynamiques -->
    <div :class="[themes[selectedTheme].bg, themes[selectedTheme].text]">
      <!-- Contenu preview -->
    </div>
  </div>
</template>

Exercice 3 : Formulaire Contact ⭐⭐

Objectif : Créer un formulaire complet avec validation visuelle

Ta mission : Créer un composant ContactForm.vue avec :

  • Input : Nom (requis)
  • Input : Email (requis)
  • Select : Sujet (3-4 options)
  • Textarea : Message (requis)
  • Checkbox : "J'accepte d'être recontacté"
  • Bouton Envoyer (désactivé si champs vides)
  • Preview du message avant envoi

Props à implémenter : Aucune, tout est interne

Comportement :

  • Le bouton est grisé tant que les champs requis sont vides
  • La preview montre un résumé de ce qui sera envoyé
  • Au clic sur "Envoyer", afficher une alerte de confirmation (pas de vrai envoi)

Coup de pouce - Validation simple :

<script setup>
import { ref, computed } from 'vue'

const name = ref('')
const email = ref('')
const message = ref('')

// computed pour vérifier si le form est valide
const isFormValid = computed(() => {
  return name.value.trim() !== '' && 
         email.value.trim() !== '' && 
         message.value.trim() !== ''
})
</script>

<template>
  <button :disabled="!isFormValid">
    Envoyer
  </button>
</template>

Exercice 4 : Filtres Produits ⭐⭐⭐

Objectif : Filtrer une liste de produits avec plusieurs critères

Ta mission : Créer un composant ProductFilter.vue avec :

  • Une liste de produits (depuis un fichier JSON)
  • Un input de recherche (filtre par nom)
  • Des checkboxes pour filtrer par catégorie
  • Un select pour trier (prix croissant/décroissant, nom A-Z)

Étape 1 : Créer le fichier de données

Crée le fichier src/data/products.json :

[
  { "id": 1, "name": "MacBook Pro", "category": "laptop", "price": 2399, "image": "💻" },
  { "id": 2, "name": "iPhone 15", "category": "phone", "price": 999, "image": "📱" },
  { "id": 3, "name": "iPad Air", "category": "tablet", "price": 699, "image": "📱" },
  { "id": 4, "name": "Dell XPS", "category": "laptop", "price": 1299, "image": "💻" },
  { "id": 5, "name": "Samsung Galaxy", "category": "phone", "price": 899, "image": "📱" },
  { "id": 6, "name": "Surface Pro", "category": "tablet", "price": 999, "image": "📱" },
  { "id": 7, "name": "ThinkPad X1", "category": "laptop", "price": 1599, "image": "💻" },
  { "id": 8, "name": "Pixel 8", "category": "phone", "price": 699, "image": "📱" }
]

Étape 2 : Structure du composant

<script setup>
import { ref, computed } from 'vue'
import products from '@/data/products.json'

// Filtres
const searchQuery = ref('')
const selectedCategories = ref([])
const sortBy = ref('name-asc')

// Liste des catégories uniques (extraites des produits)
const categories = [...new Set(products.map(p => p.category))]

// Produits filtrés et triés (computed)
const filteredProducts = computed(() => {
  let result = products

  // 1. Filtre par recherche
  if (searchQuery.value) {
    result = result.filter(p => 
      p.name.toLowerCase().includes(searchQuery.value.toLowerCase())
    )
  }

  // 2. Filtre par catégories
  if (selectedCategories.value.length > 0) {
    result = result.filter(p => 
      selectedCategories.value.includes(p.category)
    )
  }

  // 3. Tri
  // ... à toi de jouer !

  return result
})
</script>

Rendu attendu :

Exercice BONUS : Quiz Interactif ⭐⭐⭐

Objectif : Créer un quiz multi-pages avec Vue Router

Ta mission : Créer un mini quiz avec :

  • Page 1 : Accueil avec bouton "Commencer"
  • Pages 2-4 : 3 questions avec radio buttons
  • Page 5 : Résultats avec score

Prérequis : Avoir installé Vue Router (vu dans un chapitre précédent)

Structure des fichiers :

src/
├── views/
│   ├── QuizHome.vue
│   ├── QuizQuestion.vue
│   └── QuizResults.vue
├── data/
│   └── questions.json    (à toi de créer tes questions !)
└── router/
    └── index.js

Coup de pouce - Structure des questions :

[
  {
    "id": 1,
    "question": "Quelle directive Vue permet le two-way binding ?",
    "options": ["v-bind", "v-model", "v-if", "v-for"],
    "correct": "v-model"
  },
  {
    "id": 2,
    "question": "...",
    "options": ["...", "...", "...", "..."],
    "correct": "..."
  }
]

Coup de pouce - Stocker les réponses entre pages :

Tu peux utiliser un composable simple pour partager l'état entre les pages :

// src/composables/useQuiz.js
import { ref } from 'vue'

const answers = ref({})
const score = ref(0)

export function useQuiz() {
  function setAnswer(questionId, answer) {
    answers.value[questionId] = answer
  }

  function calculateScore(questions) {
    score.value = questions.filter(q => 
      answers.value[q.id] === q.correct
    ).length
  }

  function resetQuiz() {
    answers.value = {}
    score.value = 0
  }

  return {
    answers,
    score,
    setAnswer,
    calculateScore,
    resetQuiz
  }
}

Invente tes propres questions ! Ça peut être sur Vue, sur le design, sur la culture générale... Sois créatif !