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-modelsur 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 :
- L'utilisateur tape "Alice"
username.valuedevient "Alice" automatiquement- Le
{{ username }}dans le template se met à jour - 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 !