Cours Vue.js · Module 7: Props et événements
v-model sur les composants
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 :
- Accepter une prop
modelValue - É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. Qu'est-ce que v-model sur un composant personnalisé ?
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.