Cours Vue.js · Module 5: Événements et formulaires
Les formulaires en pratique - Enregistrement d'un module
Le service d'ingénierie de NOVA LAB a reçu une commande : un seul formulaire pour enregistrer chaque nouveau module de la station, du sas au laboratoire. Un module a un nom, un type, une année et un statut, des paramètres techniques, des catégories, une description et des options de maintenance. Une douzaine de champs empilés les uns sous les autres forment un mur que personne ne lira. Nous allons donc construire un formulaire découpé en groupes, avec des champs qui n'apparaissent que lorsqu'ils ont un sens, une réinitialisation et un aperçu des données.
Groupes de champs : fieldset et legend
L'élément <fieldset> regroupe des champs liés, et <legend> est la légende de ce groupe. Les lecteurs d'écran la lisent avant les champs, donc un opérateur qui utilise un lecteur d'écran sait dans quelle section il se trouve. Un <label> avec l'attribut for désigne l'id d'un champ, si bien qu'un clic sur le libellé place le curseur dans le champ. Le premier groupe commence par le nom et le type du module.
1<template>
2 <form @submit.prevent="submitModule" class="module-form" style="background: #0a0e27; color: #00ff88;">
3 <h2>Enregistrement d'un module de mission</h2>
4
5 <!-- Informations de base -->
6 <fieldset style="border-color: #00b4d8;">
7 <legend>Informations de base</legend>
8
9 <div class="form-group">
10 <label for="name">Nom du module *</label>
11 <input
12 id="name"
13 v-model.trim="module.name"
14 required
15 minlength="3"
16 style="background: #1a1e37; color: #00ff88;"
17 />
18 </div>
19
20 <div class="form-group">
21 <label for="type">Type *</label>
22 <input id="type" v-model.trim="module.type" required style="background: #1a1e37; color: #00ff88;" />
23 </div>Le nom a les attributs required et minlength="3", c'est-à-dire la validation HTML intégrée. Le navigateur vérifie ces attributs avant même d'émettre l'événement submit, et en cas d'erreur il affiche son propre message : @submit.prevent ne désactive rien de tout cela. v-model.trim retire au passage les espaces superflus. Vient ensuite la rangée avec l'année et le statut.
1 <div class="form-row">
2 <div class="form-group">
3 <label for="year">Année de déploiement</label>
4 <input
5 id="year"
6 v-model.number="module.year"
7 type="number"
8 min="2020"
9 max="2100"
10 style="background: #1a1e37; color: #00ff88;"
11 />
12 </div>
13
14 <div class="form-group">
15 <label for="status">Statut</label>
16 <select id="status" v-model="module.status" style="background: #1a1e37; color: #00ff88;">
17 <option value="">Sélectionne...</option>
18 <option value="ACTIVE">Actif</option>
19 <option value="STANDBY">En veille</option>
20 <option value="MAINTENANCE">Maintenance</option>
21 <option value="OFFLINE">Hors ligne</option>
22 </select>
23 </div>
24 </div>
25 </fieldset>L'année a min et max pour le navigateur et .number pour Vue. La computed isValid ne vérifie pas l'année, donc une saisie de 1999 n'est arrêtée que par le navigateur, après un clic sur le bouton d'envoi. La liste des statuts commence par une option vide qui invite à choisir. Le deuxième groupe montre que v-model fonctionne aussi sur des chemins imbriqués comme module.params.maxPower.
1 <!-- Paramètres -->
2 <fieldset style="border-color: #00b4d8;">
3 <legend>Paramètres</legend>
4 <div class="form-row">
5 <div class="form-group">
6 <label>Puissance max (MW)</label>
7 <input v-model.number="module.params.maxPower" type="number" style="background: #1a1e37; color: #00ff88;" />
8 </div>
9 <div class="form-group">
10 <label>Température (K)</label>
11 <input v-model.number="module.params.temperature" type="number" style="background: #1a1e37; color: #00ff88;" />
12 </div>
13 <div class="form-group" v-if="needsPressure">
14 <label>Pression (kPa)</label>
15 <input v-model.number="module.params.pressure" type="number" style="background: #1a1e37; color: #00ff88;" />
16 </div>
17 </div>
18 </fieldset>Le champ de pression a v-if="needsPressure", il n'existe donc que lorsque la computed retourne true. Avec false, Vue le retire du DOM au lieu de simplement le cacher. Retirer le champ n'efface pourtant pas les données : une pression saisie plus tôt reste dans module.params.pressure. Les catégories du troisième groupe sont générées par v-for à partir d'un tableau.
1 <!-- Catégories -->
2 <fieldset>
3 <legend>Catégories</legend>
4 <div class="checkbox-group">
5 <label v-for="cat in availableCategories" :key="cat.value">
6 <input
7 type="checkbox"
8 v-model="module.categories"
9 :value="cat.value"
10 />
11 {{ cat.label }}
12 </label>
13 </div>
14 </fieldset>Chaque case prend sa valeur dans :value="cat.value", et toutes écrivent dans un même tableau, module.categories, exactement comme le groupe de cases à cocher de la leçon sur v-model. Le quatrième groupe est la description dans une zone de texte.
1 <!-- Description -->
2 <fieldset>
3 <legend>Description</legend>
4 <textarea
5 v-model="module.description"
6 rows="4"
7 placeholder="Décris le module de mission..."
8 ></textarea>
9 <p class="char-count">
10 {{ module.description.length }} / 500 caractères
11 </p>
12 </fieldset>L'attribut rows="4" fixe la hauteur visible du champ. Le compteur en dessous indique seulement la longueur de la description et ne limite rien : une vraie limite demanderait l'attribut maxlength="500". Le cinquième groupe contient les options et un champ conditionnel.
1 <!-- Options -->
2 <fieldset>
3 <legend>Options</legend>
4 <label class="checkbox-label">
5 <input type="checkbox" v-model="module.isActive" />
6 Module actif
7 </label>
8 <label class="checkbox-label">
9 <input type="checkbox" v-model="module.needsMaintenance" />
10 Nécessite une maintenance
11 </label>
12
13 <div v-if="module.needsMaintenance" class="form-group">
14 <label>Budget de maintenance (USD)</label>
15 <input v-model.number="module.maintenanceCost" type="number" min="0" />
16 </div>
17 </fieldset>Voici le modèle du champ conditionnel : une case à cocher avec v-model pilote une variable, et v-if sur cette variable affiche un champ supplémentaire pour le budget de maintenance. isActive démarre à true, donc la case est cochée dès le départ, car v-model prend son état dans le JavaScript. À la fin du template se trouvent les boutons et l'aperçu des données.
1 <!-- Boutons -->
2 <div class="form-actions">
3 <button type="button" @click="resetForm">Effacer</button>
4 <button type="submit" :disabled="!isValid">Enregistrer le module</button>
5 </div>
6
7 <!-- Aperçu -->
8 <details>
9 <summary>Aperçu des données</summary>
10 <pre>{{ JSON.stringify(module, null, 2) }}</pre>
11 </details>
12 </form>
13</template>type="button" sur le bouton d'effacement est essentiel, car un bouton placé dans un formulaire a le type submit par défaut, et sans cet attribut l'effacement enverrait le formulaire. <details> avec <summary> crée un aperçu dépliable, et JSON.stringify(module, null, 2) met en forme les données avec une indentation de deux espaces. Passons au script.
1<script setup>
2import { reactive, computed } from 'vue'
3
4const initialState = {
5 name: '',
6 type: '',
7 year: null,
8 status: '',
9 params: { maxPower: null, temperature: null, pressure: null },
10 categories: [],
11 description: '',
12 isActive: true,
13 needsMaintenance: false,
14 maintenanceCost: null
15}
16
17const module = reactive({
18 ...initialState,
19 params: { ...initialState.params },
20 categories: []
21})initialState décrit le formulaire vide, et reactive en fait un état. Attention au piège : l'étalement ...initialState ne copie que le premier niveau de l'objet. Si params ne recevait pas sa propre copie, saisir une puissance modifierait aussi initialState.params, et la réinitialisation ramènerait les nombres saisis au lieu de champs vides. C'est pourquoi params et categories reçoivent leurs propres copies. Viennent ensuite la liste des catégories et deux computed.
1const availableCategories = [
2 { value: 'lifesupport', label: 'Support de vie' },
3 { value: 'energy', label: 'Énergie' },
4 { value: 'research', label: 'Recherche' },
5 { value: 'communication', label: 'Communication' },
6 { value: 'navigation', label: 'Navigation' }
7]
8
9const needsPressure = computed(() => {
10 return ['lifesupport', 'research'].some(c => module.categories.includes(c))
11})
12
13const isValid = computed(() => {
14 return module.name.length >= 3 && module.type
15})availableCategories est un simple tableau, car il ne change jamais et n'a pas besoin de réactivité. needsPressure utilise la méthode some, qui retourne true dès qu'au moins un élément remplit la condition. isValid joue le rôle de isFormValid de la leçon précédente : elle exige un nom d'au moins trois caractères et un type non vide. Il reste la réinitialisation et l'envoi.
1function resetForm() {
2 Object.assign(module, {
3 ...initialState,
4 params: { ...initialState.params },
5 categories: []
6 })
7}
8
9function submitModule() {
10 if (isValid.value) {
11 console.log('Enregistrement :', JSON.parse(JSON.stringify(module)))
12 alert('Le module a été enregistré !')
13 }
14}
15</script>Object.assign écrit les valeurs initiales dans l'objet module existant, donc le template reste relié au même état. Remplacer tout l'objet par un nouveau couperait le template de ses données. params et categories reçoivent de nouveau des copies fraîches. JSON.parse(JSON.stringify(module)) donne à la console un simple instantané des données au lieu d'un objet réactif. Je te recommande d'aller un pas plus loin et de transformer initialState en une fonction qui retourne un nouvel objet à chaque appel : le piège de la copie superficielle disparaît alors complètement. Dans le projet du module, tu suivras le même ordre : l'état, v-model, la validation sur @blur, la computed isFormValid et @submit.prevent.
Souviens-toi : un bon formulaire d'enregistrement fonctionne comme un module de station bien conçu - chaque groupe a son compartiment, et la réinitialisation rétablit les réglages d'usine sans laisser de traces.
Code de cette leçon : App.vue
1<script setup>
2import { ref, computed } from 'vue'
3
4const missionForm = ref({
5 commander: '',
6 destination: '',
7 duration: '',
8 crewSize: '',
9 missionType: 'exploration',
10 equipment: [],
11 emergencyProtocol: false,
12 launchDate: '',
13 objectives: ''
14})
15
16const errors = ref({})
17const submitted = ref(false)
18const submittedData = ref(null)
19
20const missionTypes = [
21 { value: 'exploration', label: 'Exploration' },
22 { value: 'research', label: 'Recherche' },
23 { value: 'construction', label: 'Construction' },
24 { value: 'rescue', label: 'Sauvetage' }
25]
26
27const equipmentOptions = [
28 { id: 'rover', name: 'Rover' },
29 { id: 'drill', name: 'Foreuse' },
30 { id: 'lab', name: 'Laboratoire' },
31 { id: 'satellite', name: 'Satellite' },
32 { id: 'drone', name: 'Drone' }
33]
34
35const validateForm = () => {
36 errors.value = {}
37
38 if (!missionForm.value.commander.trim()) {
39 errors.value.commander = 'Le commandant est obligatoire'
40 }
41
42 if (!missionForm.value.destination.trim()) {
43 errors.value.destination = 'La destination est obligatoire'
44 }
45
46 const duration = Number(missionForm.value.duration)
47 if (!duration || duration < 1 || duration > 365) {
48 errors.value.duration = 'Durée : 1-365 jours'
49 }
50
51 const crew = Number(missionForm.value.crewSize)
52 if (!crew || crew < 1 || crew > 50) {
53 errors.value.crewSize = 'Équipage : 1-50 personnes'
54 }
55
56 if (!missionForm.value.launchDate) {
57 errors.value.launchDate = 'La date de lancement est obligatoire'
58 }
59
60 if (missionForm.value.objectives.trim().length < 20) {
61 errors.value.objectives = 'Les objectifs doivent comporter au moins 20 caractères'
62 }
63
64 return Object.keys(errors.value).length === 0
65}
66
67const handleSubmit = () => {
68 if (validateForm()) {
69 submitted.value = true
70 submittedData.value = {
71 ...missionForm.value,
72 submittedAt: new Date().toLocaleString('fr-FR')
73 }
74
75 // Simulation de l'envoi
76 setTimeout(() => {
77 resetForm()
78 }, 5000)
79 }
80}
81
82const resetForm = () => {
83 missionForm.value = {
84 commander: '',
85 destination: '',
86 duration: '',
87 crewSize: '',
88 missionType: 'exploration',
89 equipment: [],
90 emergencyProtocol: false,
91 launchDate: '',
92 objectives: ''
93 }
94 errors.value = {}
95 submitted.value = false
96 submittedData.value = null
97}
98
99const isFormValid = computed(() => {
100 return missionForm.value.commander.trim() &&
101 missionForm.value.destination.trim() &&
102 missionForm.value.duration &&
103 missionForm.value.crewSize &&
104 missionForm.value.launchDate &&
105 missionForm.value.objectives.trim().length >= 20
106})
107</script>
108
109<template>
110 <div class="mission-control">
111 <div class="control-header">
112 <h2>Centre de Contrôle de Mission NOVA LAB</h2>
113 <p class="subtitle">Système complet d'enregistrement de mission</p>
114 </div>
115
116 <div v-if="submitted" class="success-panel">
117 <div class="success-message">
118 <h3>Mission enregistrée avec succès !</h3>
119 <p>Les données ont été envoyées au Centre de Commande NOVA LAB</p>
120 </div>
121
122 <div class="submitted-data">
123 <h4>Détails de la mission :</h4>
124 <div class="data-grid">
125 <div><strong>Commandant :</strong> {{ submittedData.commander }}</div>
126 <div><strong>Destination :</strong> {{ submittedData.destination }}</div>
127 <div><strong>Durée :</strong> {{ submittedData.duration }} jours</div>
128 <div><strong>Équipage :</strong> {{ submittedData.crewSize }} personnes</div>
129 <div><strong>Type :</strong> {{ missionTypes.find(t => t.value === submittedData.missionType)?.label }}</div>
130 <div><strong>Équipement :</strong> {{ submittedData.equipment.length }} éléments</div>
131 <div><strong>Date de lancement :</strong> {{ submittedData.launchDate }}</div>
132 <div><strong>Envoyé :</strong> {{ submittedData.submittedAt }}</div>
133 </div>
134 </div>
135
136 <button @click="resetForm" class="reset-btn">
137 Nouvelle Mission
138 </button>
139 </div>
140
141 <form v-else @submit.prevent="handleSubmit" class="mission-form">
142 <div class="form-section">
143 <h3>Informations de base</h3>
144
145 <div class="form-field">
146 <label for="commander">Commandant de mission :</label>
147 <input
148 id="commander"
149 v-model.trim="missionForm.commander"
150 type="text"
151 placeholder="Nom complet"
152 class="form-input"
153 :class="{ error: errors.commander }"
154 />
155 <span v-if="errors.commander" class="error-text">{{ errors.commander }}</span>
156 </div>
157
158 <div class="form-field">
159 <label for="destination">Destination :</label>
160 <input
161 id="destination"
162 v-model.trim="missionForm.destination"
163 type="text"
164 placeholder="ex. Cratère Gale, Mars"
165 class="form-input"
166 :class="{ error: errors.destination }"
167 />
168 <span v-if="errors.destination" class="error-text">{{ errors.destination }}</span>
169 </div>
170
171 <div class="form-row">
172 <div class="form-field">
173 <label for="duration">Durée (jours) :</label>
174 <input
175 id="duration"
176 v-model.number="missionForm.duration"
177 type="number"
178 placeholder="1-365"
179 class="form-input"
180 :class="{ error: errors.duration }"
181 />
182 <span v-if="errors.duration" class="error-text">{{ errors.duration }}</span>
183 </div>
184
185 <div class="form-field">
186 <label for="crew">Taille de l'équipage :</label>
187 <input
188 id="crew"
189 v-model.number="missionForm.crewSize"
190 type="number"
191 placeholder="1-50"
192 class="form-input"
193 :class="{ error: errors.crewSize }"
194 />
195 <span v-if="errors.crewSize" class="error-text">{{ errors.crewSize }}</span>
196 </div>
197 </div>
198
199 <div class="form-field">
200 <label for="launch">Date de lancement :</label>
201 <input
202 id="launch"
203 v-model="missionForm.launchDate"
204 type="date"
205 class="form-input"
206 :class="{ error: errors.launchDate }"
207 />
208 <span v-if="errors.launchDate" class="error-text">{{ errors.launchDate }}</span>
209 </div>
210 </div>
211
212 <div class="form-section">
213 <h3>Configuration de la mission</h3>
214
215 <div class="form-field">
216 <label>Type de mission :</label>
217 <div class="radio-group">
218 <label
219 v-for="type in missionTypes"
220 :key="type.value"
221 class="radio-label"
222 >
223 <input
224 v-model="missionForm.missionType"
225 type="radio"
226 :value="type.value"
227 />
228 {{ type.label }}
229 </label>
230 </div>
231 </div>
232
233 <div class="form-field">
234 <label>Équipement :</label>
235 <div class="checkbox-group">
236 <label
237 v-for="item in equipmentOptions"
238 :key="item.id"
239 class="checkbox-label"
240 >
241 <input
242 v-model="missionForm.equipment"
243 type="checkbox"
244 :value="item.id"
245 />
246 {{ item.name }}
247 </label>
248 </div>
249 </div>
250
251 <div class="form-field">
252 <label class="checkbox-label emergency">
253 <input
254 v-model="missionForm.emergencyProtocol"
255 type="checkbox"
256 />
257 Activer le protocole de sauvetage
258 </label>
259 </div>
260 </div>
261
262 <div class="form-section">
263 <h3>Objectifs de la mission</h3>
264
265 <div class="form-field">
266 <label for="objectives">Description détaillée des objectifs :</label>
267 <textarea
268 id="objectives"
269 v-model.trim="missionForm.objectives"
270 placeholder="Décris les principaux objectifs et tâches de la mission (minimum 20 caractères)..."
271 class="form-textarea"
272 :class="{ error: errors.objectives }"
273 rows="6"
274 ></textarea>
275 <div class="char-counter">
276 Caractères : {{ missionForm.objectives.length }}
277 </div>
278 <span v-if="errors.objectives" class="error-text">{{ errors.objectives }}</span>
279 </div>
280 </div>
281
282 <div class="form-actions">
283 <button
284 type="submit"
285 class="submit-btn"
286 :class="{ disabled: !isFormValid }"
287 :disabled="!isFormValid"
288 >
289 Enregistrer la mission
290 </button>
291
292 <button
293 type="button"
294 @click="resetForm"
295 class="cancel-btn"
296 >
297 Annuler
298 </button>
299 </div>
300
301 <div class="form-status" :class="{ valid: isFormValid }">
302 {{ isFormValid ? 'Formulaire prêt à être envoyé' : 'Remplis tous les champs obligatoires' }}
303 </div>
304 </form>
305 </div>
306</template>
307
308<style scoped>
309.mission-control {
310 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
311 color: #00ff88;
312 padding: 30px;
313 border-radius: 15px;
314 border: 2px solid #00b4d8;
315 font-family: 'Courier New', monospace;
316 max-width: 1000px;
317 margin: 0 auto;
318}
319
320.control-header {
321 text-align: center;
322 margin-bottom: 30px;
323 padding-bottom: 20px;
324 border-bottom: 2px solid #00b4d8;
325}
326
327.control-header h2 {
328 color: #00ff88;
329 margin: 0 0 10px 0;
330 font-size: 28px;
331 text-shadow: 0 0 10px #00ff88;
332}
333
334.subtitle {
335 color: #00b4d8;
336 margin: 0;
337 font-size: 14px;
338}
339
340.success-panel {
341 text-align: center;
342}
343
344.success-message {
345 background: rgba(0, 255, 136, 0.1);
346 border: 2px solid #00ff88;
347 border-radius: 12px;
348 padding: 30px;
349 margin-bottom: 30px;
350}
351
352.success-message h3 {
353 color: #00ff88;
354 margin: 0 0 10px 0;
355 font-size: 24px;
356}
357
358.success-message p {
359 color: #00b4d8;
360 margin: 0;
361}
362
363.submitted-data {
364 background: rgba(0, 180, 216, 0.05);
365 border: 1px solid #00b4d8;
366 border-radius: 8px;
367 padding: 20px;
368 margin-bottom: 20px;
369 text-align: left;
370}
371
372.submitted-data h4 {
373 color: #00ff88;
374 margin-top: 0;
375}
376
377.data-grid {
378 display: grid;
379 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
380 gap: 15px;
381 color: #00b4d8;
382}
383
384.data-grid strong {
385 color: #00ff88;
386}
387
388.reset-btn {
389 background: linear-gradient(135deg, #00b4d8 0%, #0077b6 100%);
390 color: white;
391 border: none;
392 padding: 15px 40px;
393 border-radius: 8px;
394 font-size: 16px;
395 font-weight: bold;
396 cursor: pointer;
397 font-family: 'Courier New', monospace;
398}
399
400.mission-form {
401 display: flex;
402 flex-direction: column;
403 gap: 30px;
404}
405
406.form-section {
407 background: rgba(0, 180, 216, 0.05);
408 padding: 25px;
409 border-radius: 12px;
410 border: 1px solid #00b4d8;
411}
412
413.form-section h3 {
414 color: #00ff88;
415 margin: 0 0 20px 0;
416 font-size: 20px;
417}
418
419.form-field {
420 margin-bottom: 20px;
421}
422
423.form-field:last-child {
424 margin-bottom: 0;
425}
426
427.form-field > label {
428 display: block;
429 color: #00b4d8;
430 margin-bottom: 8px;
431 font-size: 14px;
432 font-weight: bold;
433}
434
435.form-input,
436.form-textarea {
437 width: 100%;
438 background: rgba(0, 180, 216, 0.1);
439 border: 2px solid #00b4d8;
440 border-radius: 8px;
441 padding: 12px 15px;
442 color: #00ff88;
443 font-size: 16px;
444 font-family: 'Courier New', monospace;
445 box-sizing: border-box;
446}
447
448.form-input:focus,
449.form-textarea:focus {
450 outline: none;
451 border-color: #00ff88;
452 box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
453}
454
455.form-input.error,
456.form-textarea.error {
457 border-color: #ff6b35;
458}
459
460.form-textarea {
461 resize: vertical;
462}
463
464.form-row {
465 display: grid;
466 grid-template-columns: 1fr 1fr;
467 gap: 15px;
468}
469
470.error-text {
471 display: block;
472 margin-top: 5px;
473 color: #ff6b35;
474 font-size: 12px;
475}
476
477.char-counter {
478 margin-top: 5px;
479 color: #00b4d8;
480 font-size: 12px;
481}
482
483.radio-group,
484.checkbox-group {
485 display: flex;
486 flex-direction: column;
487 gap: 10px;
488}
489
490.radio-label,
491.checkbox-label {
492 display: flex;
493 align-items: center;
494 gap: 10px;
495 padding: 10px 15px;
496 background: rgba(0, 180, 216, 0.05);
497 border: 1px solid #00b4d8;
498 border-radius: 6px;
499 cursor: pointer;
500 color: #00ff88;
501}
502
503.radio-label:hover,
504.checkbox-label:hover {
505 background: rgba(0, 180, 216, 0.15);
506}
507
508.checkbox-label.emergency {
509 background: rgba(255, 107, 53, 0.1);
510 border-color: #ff6b35;
511 color: #ff6b35;
512 font-weight: bold;
513}
514
515.form-actions {
516 display: flex;
517 gap: 15px;
518 justify-content: center;
519}
520
521.submit-btn,
522.cancel-btn {
523 padding: 15px 40px;
524 border: none;
525 border-radius: 8px;
526 font-size: 16px;
527 font-weight: bold;
528 cursor: pointer;
529 font-family: 'Courier New', monospace;
530 transition: all 0.3s ease;
531}
532
533.submit-btn {
534 background: linear-gradient(135deg, #00ff88 0%, #00b368 100%);
535 color: #0a0e27;
536}
537
538.submit-btn:hover:not(.disabled) {
539 transform: translateY(-2px);
540 box-shadow: 0 5px 20px rgba(0, 255, 136, 0.4);
541}
542
543.submit-btn.disabled {
544 opacity: 0.5;
545 cursor: not-allowed;
546}
547
548.cancel-btn {
549 background: linear-gradient(135deg, #ff6b35 0%, #d64545 100%);
550 color: white;
551}
552
553.cancel-btn:hover {
554 transform: translateY(-2px);
555 box-shadow: 0 5px 20px rgba(255, 107, 53, 0.4);
556}
557
558.form-status {
559 text-align: center;
560 padding: 15px;
561 background: rgba(255, 107, 53, 0.1);
562 border: 2px solid #ff6b35;
563 border-radius: 8px;
564 color: #ff6b35;
565 font-weight: bold;
566}
567
568.form-status.valid {
569 background: rgba(0, 255, 136, 0.1);
570 border-color: #00ff88;
571 color: #00ff88;
572}
573</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. Quelle est la meilleure façon de réinitialiser un formulaire dans Vue ?
2. Comment affiche-t-on un champ supplémentaire uniquement quand une case est cochée ?
Voici 2 des 4 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne la syntaxe pour afficher un champ conditionnellement avec v-if :
- Éditeur de code
Construis un formulaire avec fieldset, validation, réinitialisation et envoi.
- Tri horizontal
Ordonne la syntaxe correcte pour un fieldset avec legend :
- Clique dans l'ordre
Ordonne la syntaxe pour un formulaire avec un gestionnaire d'envoi :
- Éditeur de code
Construis un formulaire avec validation au @blur, un bouton de réinitialisation et un envoi désactivé.
- Tri vertical
Ordonne les étapes de création d'un formulaire réactif dans Vue :
- Clique dans l'ordre
Ordonne la définition d'un computed qui vérifie le formulaire :
- Tri horizontal
Ordonne la syntaxe correcte pour une textarea avec v-model :