Cours Vue.js · Module 5: Événements et formulaires
Formulaires - v-model pour les données de mission
Dans cette leçon4
Le panneau d'enregistrement de l'équipage a un champ pour le nom et un paragraphe qui répète ce nom. Pour que cela fonctionne, il faut deux liaisons : la variable doit arriver dans le champ, et ce que l'opérateur tape doit revenir dans la variable. Écrire cela à la main pour chacun des dix champs, c'est vingt liaisons et vingt occasions de se tromper. Vue réunit les deux sens dans une seule directive : v-model, une liaison de données bidirectionnelle entre un champ de formulaire et une variable.
Ce que fait v-model en coulisses
v-model est un raccourci. Sur un champ texte, il se déploie en deux choses que tu connais déjà : la liaison :value, qui écrit la variable dans le champ, et l'écouteur @input, qui après chaque modification lit $event.target.value et enregistre le résultat dans la variable.
1<template>
2 <input :value="crewName" @input="crewName = $event.target.value" />
3 <input v-model="crewName" />
4</template>
5
6<script setup>
7import { ref } from 'vue'
8
9const crewName = ref('')
10</script>Les deux champs fonctionnent de façon identique et restent synchronisés : tape dans l'un, et le texte apparaît dans l'autre, car les deux lisent et écrivent le même ref. crewName reste un ref ordinaire, donc v-model n'a besoin d'aucun état particulier. Retiens une règle : v-model ignore les attributs value, checked et selected écrits dans le HTML. La source de vérité est toujours la variable, c'est pourquoi tu fixes la valeur initiale en JavaScript.
Les bases de v-model
La même directive fonctionne sur tous les champs de formulaire, mais en coulisses elle choisit la propriété et l'événement adaptés au type de champ. Les champs texte et <textarea> utilisent value et l'événement input, les cases à cocher et les boutons radio utilisent checked et change, et <select> utilise value et change. Parcourons le formulaire de mission par étapes, en commençant par le texte.
1<template>
2 <div class="mission-form" style="background: #0a0e27; color: #00ff88;">
3 <!-- Champ texte -->
4 <input v-model="crewName" placeholder="Nom de l'équipage" style="background: #1a1e37; color: #00ff88;" />
5 <p>Équipage : {{ crewName }}</p>
6
7 <!-- Zone de texte -->
8 <textarea v-model="missionNotes" placeholder="Notes de mission" style="background: #1a1e37; color: #00ff88;"></textarea>
9 <p>Notes : {{ missionNotes }}</p>Le champ et la zone de texte se comportent de la même façon : le paragraphe en dessous change à chaque lettre. Ne mets pas {{ missionNotes }} entre les balises de <textarea>, car l'interpolation ne fonctionne pas à cet endroit. Relie toujours une zone de texte avec v-model. Passons aux cases à cocher.
1 <!-- Case à cocher - booléen -->
2 <label style="color: #00ff88;">
3 <input type="checkbox" v-model="isAutomated" />
4 Mode automatique
5 </label>
6 <p>Automatique : {{ isAutomated }}</p>
7
8 <!-- Case à cocher - tableau -->
9 <label style="color: #00ff88;">
10 <input type="checkbox" v-model="activeSystems" value="oxygen" />
11 Générateur d'oxygène
12 </label>
13 <label style="color: #00ff88;">
14 <input type="checkbox" v-model="activeSystems" value="water" />
15 Recycleur d'eau
16 </label>
17 <p>Systèmes actifs : {{ activeSystems }}</p>Une case unique liée à isAutomated donne une valeur booléenne : true quand elle est cochée et false quand elle est décochée. Quand plusieurs cases partagent un même tableau, chacune a son propre attribut value, et activeSystems reçoit les valeurs des cases cochées, dans l'ordre où elles ont été cochées. Place aux boutons radio et à la liste déroulante.
1 <!-- Bouton radio -->
2 <label style="color: #00ff88;">
3 <input type="radio" v-model="powerMode" value="eco" />
4 Éco
5 </label>
6 <label style="color: #00ff88;">
7 <input type="radio" v-model="powerMode" value="performance" />
8 Performance
9 </label>
10 <p>Mode d'alimentation : {{ powerMode }}</p>
11
12 <!-- Menu déroulant -->
13 <select v-model="selectedModule" style="background: #1a1e37; color: #00ff88;">
14 <option disabled value="">Sélectionne un module</option>
15 <option value="alpha">Oxygène Alpha</option>
16 <option value="beta">Plasma Beta</option>
17 <option value="gamma">Eau Gamma</option>
18 </select>
19 <p>Module : {{ selectedModule }}</p>
20 </div>
21</template>Le groupe radio écrit dans powerMode la value du bouton sélectionné, donc la variable contient toujours une seule valeur. <select> fait la même chose avec l'option choisie. La première option avec disabled et une valeur vide est le modèle recommandé : quand la variable démarre avec une chaîne vide, la liste affiche une invitation à choisir, et sur iOS elle n'empêche pas de sélectionner la première vraie option. Il ne reste plus que le script.
1<script setup>
2import { ref } from 'vue'
3
4const crewName = ref('')
5const missionNotes = ref('')
6const isAutomated = ref(false)
7const activeSystems = ref([])
8const powerMode = ref('')
9const selectedModule = ref('')
10</script>Chaque champ reçoit une valeur de départ adaptée à son type : une chaîne vide pour les champs texte, le groupe radio et la liste, false pour la case unique et un tableau vide pour le groupe de cases. Une erreur de type, par exemple ref('') pour un groupe de cases, est la cause la plus fréquente d'un comportement étrange du formulaire.
Options d'un select depuis un tableau
Le catalogue des modules de la station ne cesse de grandir, donc on ne tape pas les options à la main. Tu les génères avec la directive v-for à partir d'un tableau d'objets, et tu fournis la valeur de l'option par la liaison :value. Comme pour toute liste v-for, chaque option reçoit aussi une :key unique.
1<template>
2 <select v-model="selectedModuleId">
3 <option disabled value="">Sélectionne un module</option>
4 <option v-for="mod in modules" :key="mod.id" :value="mod.id">
5 {{ mod.name }}
6 </option>
7 </select>
8 <p>Id choisi : {{ selectedModuleId }}</p>
9</template>
10
11<script setup>
12import { ref } from 'vue'
13
14const modules = ref([
15 { id: 1, name: 'Oxygen Alpha' },
16 { id: 2, name: 'Plasma Beta' }
17])
18const selectedModuleId = ref('')
19</script>Quand tu choisis Plasma Beta, le paragraphe affiche 2. Fais attention au type : grâce à :value, la variable reçoit le nombre 2, exactement celui du tableau, alors qu'un simple attribut value="2" donnerait toujours du texte. La liaison :value accepte n'importe quelle valeur, même un objet entier.
Des données jusqu'à l'envoi
Tout formulaire se construit dans le même ordre : d'abord l'état dans ref ou reactive, ensuite la liaison des champs avec v-model, puis @submit.prevent sur le formulaire, et enfin la validation avant l'envoi. Tu connais déjà les trois premières étapes, et nous ajouterons la validation dans la leçon sur les formulaires avancés. Je te recommande de donner aux variables des noms qui disent ce qu'elles contiennent plutôt que le type de champ : activeSystems survit au remplacement des cases à cocher par une liste à sélection multiple, checkboxValues non. Dans la prochaine leçon, tu régleras le signal des champs avec les modificateurs .lazy, .number et .trim.
Souviens-toi : v-model est un canal de communication bidirectionnel - le champ du panneau et la variable de l'ordinateur de mission affichent toujours la même valeur, quel que soit le côté qui l'a modifiée.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const astronautName = ref('')
5const missionCode = ref('')
6const missionDescription = ref('')
7const statusReport = ref('')
8
9const characterCount = computed(() => missionDescription.value.length)
10const wordCount = computed(() => {
11 return missionDescription.value.trim().split(/\s+/).filter(word => word.length > 0).length
12})
13
14const isFormValid = computed(() => {
15 return astronautName.value.length >= 3 &&
16 missionCode.value.length >= 4 &&
17 missionDescription.value.length >= 10
18})
19</script>
20
21<template>
22 <div class="data-transmission">
23 <div class="transmission-header">
24 <h2>Système de Transmission de Données</h2>
25 <p class="subtitle">v-model - Liaison de données bidirectionnelle</p>
26 </div>
27
28 <div class="form-container">
29 <div class="form-group">
30 <label for="astronaut">Nom de l'astronaute :</label>
31 <input
32 id="astronaut"
33 v-model="astronautName"
34 type="text"
35 placeholder="Saisis le nom..."
36 class="data-input"
37 />
38 <div class="input-feedback">
39 Saisi : <strong>{{ astronautName || '(vide)' }}</strong>
40 </div>
41 </div>
42
43 <div class="form-group">
44 <label for="mission-code">Code de Mission :</label>
45 <input
46 id="mission-code"
47 v-model="missionCode"
48 type="text"
49 placeholder="ex. MARS-2087-A1"
50 class="data-input"
51 />
52 <div class="input-feedback">
53 Code : <strong>{{ missionCode || '(vide)' }}</strong>
54 </div>
55 </div>
56
57 <div class="form-group">
58 <label for="description">Description de la Mission :</label>
59 <textarea
60 id="description"
61 v-model="missionDescription"
62 placeholder="Décris les détails de la mission..."
63 class="data-textarea"
64 rows="5"
65 ></textarea>
66 <div class="textarea-stats">
67 <span>Caractères : {{ characterCount }}</span>
68 <span>Mots : {{ wordCount }}</span>
69 </div>
70 </div>
71
72 <div class="form-group">
73 <label for="status">Rapport de Statut (optionnel) :</label>
74 <textarea
75 id="status"
76 v-model="statusReport"
77 placeholder="Informations supplémentaires..."
78 class="data-textarea"
79 rows="3"
80 ></textarea>
81 </div>
82
83 <div class="validation-status" :class="{ valid: isFormValid }">
84 <span v-if="isFormValid">Formulaire prêt pour la transmission</span>
85 <span v-else>Remplis tous les champs obligatoires</span>
86 </div>
87 </div>
88
89 <div class="data-preview">
90 <h3>Aperçu des Données Transmises</h3>
91 <div class="preview-box">
92 <pre>{{ {
93 astronaut: astronautName || null,
94 missionCode: missionCode || null,
95 description: missionDescription || null,
96 status: statusReport || null,
97 valid: isFormValid
98} }}</pre>
99 </div>
100 </div>
101
102 <div class="vmodel-info">
103 <h3>Comment fonctionne v-model :</h3>
104 <div class="info-content">
105 <p><code>v-model</code> est un raccourci pour :</p>
106 <code class="code-example">:value="variable" @input="variable = $event.target.value"</code>
107 <p>Synchronise automatiquement les données entre l'input et la variable !</p>
108 </div>
109 </div>
110 </div>
111</template>
112
113<style scoped>
114.data-transmission {
115 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
116 color: #00ff88;
117 padding: 30px;
118 border-radius: 15px;
119 border: 2px solid #00b4d8;
120 font-family: 'Courier New', monospace;
121 max-width: 900px;
122 margin: 0 auto;
123}
124
125.transmission-header {
126 text-align: center;
127 margin-bottom: 30px;
128 padding-bottom: 20px;
129 border-bottom: 2px solid #00b4d8;
130}
131
132.transmission-header h2 {
133 color: #00ff88;
134 margin: 0 0 10px 0;
135 font-size: 28px;
136 text-shadow: 0 0 10px #00ff88;
137}
138
139.subtitle {
140 color: #00b4d8;
141 margin: 0;
142 font-size: 14px;
143}
144
145.form-container {
146 margin-bottom: 30px;
147}
148
149.form-group {
150 margin-bottom: 25px;
151}
152
153.form-group label {
154 display: block;
155 color: #00b4d8;
156 margin-bottom: 10px;
157 font-size: 14px;
158 font-weight: bold;
159}
160
161.data-input,
162.data-textarea {
163 width: 100%;
164 background: rgba(0, 180, 216, 0.1);
165 border: 2px solid #00b4d8;
166 border-radius: 8px;
167 padding: 12px 15px;
168 color: #00ff88;
169 font-size: 16px;
170 font-family: 'Courier New', monospace;
171 box-sizing: border-box;
172 transition: all 0.3s ease;
173}
174
175.data-input:focus,
176.data-textarea:focus {
177 outline: none;
178 border-color: #00ff88;
179 box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
180 background: rgba(0, 180, 216, 0.15);
181}
182
183.data-textarea {
184 resize: vertical;
185}
186
187.input-feedback {
188 margin-top: 8px;
189 color: #00b4d8;
190 font-size: 12px;
191 padding: 8px 12px;
192 background: rgba(0, 180, 216, 0.05);
193 border-radius: 4px;
194}
195
196.input-feedback strong {
197 color: #00ff88;
198}
199
200.textarea-stats {
201 margin-top: 8px;
202 display: flex;
203 gap: 20px;
204 color: #00b4d8;
205 font-size: 12px;
206}
207
208.validation-status {
209 padding: 15px;
210 background: rgba(255, 107, 53, 0.1);
211 border: 2px solid #ff6b35;
212 border-radius: 8px;
213 text-align: center;
214 font-weight: bold;
215 color: #ff6b35;
216 transition: all 0.3s ease;
217}
218
219.validation-status.valid {
220 background: rgba(0, 255, 136, 0.1);
221 border-color: #00ff88;
222 color: #00ff88;
223}
224
225.data-preview {
226 margin-bottom: 30px;
227}
228
229.data-preview h3 {
230 color: #00ff88;
231 margin-bottom: 15px;
232}
233
234.preview-box {
235 background: rgba(0, 0, 0, 0.4);
236 border: 1px solid #00b4d8;
237 border-radius: 8px;
238 padding: 20px;
239 overflow-x: auto;
240}
241
242.preview-box pre {
243 margin: 0;
244 color: #00ff88;
245 font-size: 14px;
246 line-height: 1.6;
247}
248
249.vmodel-info {
250 background: rgba(0, 255, 136, 0.05);
251 padding: 20px;
252 border-radius: 8px;
253 border: 1px solid #00ff88;
254}
255
256.vmodel-info h3 {
257 color: #00ff88;
258 margin-top: 0;
259}
260
261.info-content {
262 color: #00b4d8;
263}
264
265.info-content p {
266 margin: 10px 0;
267}
268
269.info-content code {
270 background: rgba(0, 255, 136, 0.2);
271 padding: 2px 6px;
272 border-radius: 4px;
273 color: #00ff88;
274 font-size: 13px;
275}
276
277.code-example {
278 display: block;
279 background: rgba(0, 180, 216, 0.2);
280 padding: 10px 15px;
281 border-radius: 6px;
282 margin: 10px 0;
283 font-size: 12px;
284}
285</style>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Qu'est-ce que v-model dans Vue.js ?
2. v-model est un raccourci pour quelle combinaison ?
Voici 2 des 7 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Utilise v-model avec un champ texte et affiche la valeur saisie dans un paragraphe.
- Tri horizontal
Ordonne la syntaxe correcte pour un input avec v-model :
- Clique dans l'ordre
Ordonne la syntaxe pour une case à cocher avec v-model :
- Éditeur de code
Utilise une case à cocher (tableau) pour sélectionner l'équipement et un radio pour choisir un type de mission.
- Tri vertical
Ordonne les étapes de création d'un formulaire Vue réactif, des données à la validation :
- Tri horizontal
Ordonne la syntaxe correcte pour une option de select avec v-for :
- Éditeur de code
Construis un select avec v-for et :value pour une liste d'objets.