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

Les formulaires en pratique - Enregistrement d'un module

7 min de lecture

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. 1. Quelle est la meilleure façon de réinitialiser un formulaire dans Vue ?

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