Cours Vue.js · Module 5: Événements et formulaires

Formulaires - v-model pour les données de mission

6 min de lecture
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. 1. Qu'est-ce que v-model dans Vue.js ?

  2. 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.