Cours Vue.js · Module 7: Props et événements

v-model sur les composants

5 min de lecture
Dans cette leçon5

Le formulaire d'émission de la station comporte un champ « Contenu du message ». Le parent garde le contenu dans un ref, tandis que le champ lui-même se trouve dans un composant MarsInput. Les props transmettront la valeur vers le bas et un événement renverra la modification vers le haut - mais écrire cette paire pour chaque champ du formulaire devient vite fastidieux. Heureusement, v-model fonctionne aussi sur les composants personnalisés - c'est comme une synchronisation bidirectionnelle des données entre la Terre et Mars !

Sur les éléments HTML natifs, v-model est un raccourci qui dépend de l'élément : un <input> texte et un <textarea> associent value à l'événement input, les cases à cocher et les boutons radio associent checked à l'événement change, et un <select> associe value à l'événement change. Sur un composant, il n'y a qu'une seule règle, et c'est toi qui la remplis.

Comment fonctionne v-model sur un composant ?

Le compilateur de Vue développe v-model sur un composant en une prop modelValue et un écouteur de l'événement update:modelValue. Les deux écritures ci-dessous font exactement la même chose :

1<!-- Le parent utilise v-model -->
2<MarsInput v-model="message" />
3
4<!-- Est équivalent à : -->
5<MarsInput
6  :modelValue="message"
7  @update:modelValue="message = $event"
8/>

Dans la deuxième version, $event est le premier argument envoyé par l'enfant, c'est-à-dire le nouveau contenu du champ. Le parent l'affecte à message, donc c'est toujours le parent qui reste propriétaire des données.

Implémentation dans le composant

Le composant doit :

  1. Accepter une prop modelValue
  2. Émettre un événement update:modelValue

En pratique, ces deux points tiennent en quelques lignes :

1<!-- MarsInput.vue -->
2<script setup>
3defineProps(['modelValue'])
4const emit = defineEmits(['update:modelValue'])
5</script>
6
7<template>
8  <input
9    :value="modelValue"
10    @input="emit('update:modelValue', $event.target.value)"
11  />
12</template>

Le parent n'a pas changé d'un seul caractère - il écrit toujours <MarsInput v-model="message" />. C'est l'enfant qui fait tout le travail : il affiche modelValue et, à chaque caractère saisi, envoie un événement avec le nouveau contenu. Il n'écrase jamais la prop lui-même, conformément à la règle du flux à sens unique - il demande seulement une modification au parent.

defineModel - une écriture plus courte (Vue 3.4+)

Depuis Vue 3.4, tu peux écrire le même composant plus brièvement grâce à la macro defineModel. Elle renvoie un ref : la lecture donne la valeur de la prop modelValue, et l'écriture émet update:modelValue. C'est pourquoi un simple v-model sur le champ <input> suffit à l'intérieur :

1<!-- MarsInput.vue -->
2<script setup>
3const model = defineModel()
4</script>
5
6<template>
7  <input v-model="model" />
8</template>

Sous le capot, c'est le même mécanisme - une prop et un événement - donc le parent ne voit aucune différence. La documentation de Vue recommande defineModel pour le nouveau code, et la version manuelle reste utile à connaître, car tu la rencontreras dans des projets et des bibliothèques plus anciens.

v-model multiples

Vue 3 permet plusieurs v-model sur un même composant. L'argument après les deux-points change le nom de la paire : v-model:first-name se connecte à la prop firstName et à l'événement update:firstName :

1<!-- Parent -->
2<template>
3  <CrewForm
4    v-model:first-name="firstName"
5    v-model:last-name="lastName"
6  />
7</template>
8
9<!-- CrewForm.vue -->
10<script setup>
11defineProps(['firstName', 'lastName'])
12const emit = defineEmits(['update:firstName', 'update:lastName'])
13</script>
14
15<template>
16  <input
17    :value="firstName"
18    @input="emit('update:firstName', $event.target.value)"
19  />
20  <input
21    :value="lastName"
22    @input="emit('update:lastName', $event.target.value)"
23  />
24</template>

Dans le template du parent, tu peux écrire l'argument en kebab-case ou en camelCase, sous la forme v-model:firstName - les deux fonctionnent dans les fichiers .vue. Avec defineModel, il suffit d'indiquer le nom : const firstName = defineModel('firstName').

Modificateurs de v-model

Tu peux créer tes propres modificateurs. Utilisés sur v-model, ils arrivent chez l'enfant dans la prop modelModifiers sous forme d'objet, par exemple { uppercase: true } :

1<!-- Parent -->
2<MarsInput v-model.uppercase="text" />
3
4<!-- MarsInput.vue -->
5<script setup>
6const props = defineProps({
7  modelValue: String,
8  modelModifiers: { default: () => ({}) }
9})
10const emit = defineEmits(['update:modelValue'])
11
12function onInput(e) {
13  let value = e.target.value
14  if (props.modelModifiers.uppercase) {
15    value = value.toUpperCase()
16  }
17  emit('update:modelValue', value)
18}
19</script>
20
21<template>
22  <input :value="modelValue" @input="onInput" />
23</template>

Le composant vérifie l'indicateur et transforme le texte avant de l'envoyer, donc le parent reçoit déjà des majuscules. Pour un v-model:first-name nommé, la prop des modificateurs s'appelle firstNameModifiers. Avec defineModel, tu lis les modificateurs comme deuxième élément de la paire renvoyée, et tu places la transformation dans l'option set :

1<script setup>
2const [model, modifiers] = defineModel({
3  set(value) {
4    return modifiers.uppercase ? value.toUpperCase() : value
5  }
6})
7</script>

La fonction set reçoit la valeur écrite dans le modèle et renvoie ce qui doit parvenir au parent. Le parent ne change rien - il écrit toujours <MarsInput v-model.uppercase="text" />.

Mon conseil : dans les nouveaux composants, utilise defineModel, car cela fait moins de code et moins d'endroits où glisser une faute de frappe dans le nom d'un événement. Dans l'éditeur ci-dessous, MarsInput et CrewForm utilisent la version manuelle - essaie de les réécrire avec defineModel. Souviens-toi seulement que v-model relie deux niveaux voisins ; dans la prochaine leçon, tu enverras des données cinq étages plus bas sans les faire passer par chaque étage.

Retiens : v-model sur un composant, c'est une prop et un événement réunis en un seul canal duplex.

Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsInput from './MarsInput.vue'
4import CrewForm from './CrewForm.vue'
5
6const message = ref('Hello from Mars!')
7const firstName = ref('Nova')
8const lastName = ref('Commander')
9</script>
10
11<template>
12  <div class="nova-lab">
13    <h1>v-model on Components</h1>
14
15    <h2>Single v-model:</h2>
16    <MarsInput v-model="message" />
17    <p>Parent value: {{ message }}</p>
18
19    <h2>Multiple v-model:</h2>
20    <CrewForm
21      v-model:first-name="firstName"
22      v-model:last-name="lastName"
23    />
24    <p>Name: {{ firstName }} {{ lastName }}</p>
25  </div>
26</template>
27
28<style scoped>
29.nova-lab {
30  background: #0a0e27;
31  color: #00ff88;
32  min-height: 100vh;
33  padding: 20px;
34  font-family: monospace;
35}
36h1 { color: #00ff88; margin-bottom: 20px; }
37h2 { color: #00b4d8; margin: 15px 0 10px; }
38p { margin: 8px 0; }
39</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 sur un composant personnalisé ?

  2. 2. Que doit faire un composant pour supporter v-model ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un MarsInput avec un support complet de v-model (modelValue + update:modelValue).

  • Tri vertical

    Ordonne les étapes pour implémenter v-model dans un composant :

  • Éditeur de code

    Crée un composant avec deux liaisons v-model : v-model:firstName et v-model:lastName.