Cours Vue.js · Module 7: Props et événements
Événements personnalisés
Dans cette leçon4
À la station de communication, le signal ne circule pas que dans un sens ! Quand Mars reçoit des données, elle renvoie un accusé de réception vers la Terre. Dans Vue, un enfant communique avec son parent grâce aux événements personnalisés (custom events) - des signaux émis vers le haut de l'arbre de composants.
Les props ne circulent que vers le bas, donc un bouton du module d'émission ne peut pas modifier lui-même l'état du centre de contrôle. L'opérateur clique sur « Envoyer », et ensuite ? Le bouton n'est pas propriétaire du journal de communication, c'est le parent qui le détient. Il faut un canal de retour : l'enfant annonce qu'il s'est passé quelque chose, et le parent décide quoi en faire.
defineEmits
Dans <script setup>, tu déclares les événements émis avec la macro defineEmits. Comme defineProps, elle ne nécessite aucun import. Elle renvoie une fonction emit : son premier argument est le nom de l'événement, et les suivants sont les données que tu veux envoyer, la charge utile (payload) :
1<!-- SignalButton.vue (enfant) -->
2<script setup>
3const emit = defineEmits(['signal-sent', 'error'])
4
5const sendSignal = () => {
6 emit('signal-sent', { message: 'OK', time: Date.now() })
7}
8
9const reportError = (code) => {
10 emit('error', code)
11}
12</script>
13
14<template>
15 <button @click="sendSignal">Send</button>
16</template>La liste de defineEmits fonctionne comme le registre des canaux du module : on voit tout de suite quels signaux le composant sait émettre. Si tu émets un événement absent de la liste, Vue t'avertit dans la console en mode développement. L'appel à emit ne change rien dans l'enfant lui-même - il diffuse seulement le message.
Écouter dans le parent
Le parent écoute les événements avec @event-name ou v-on:event-name, exactement comme les clics sur des éléments HTML ordinaires :
1<!-- Parent.vue -->
2<template>
3 <SignalButton
4 @signal-sent="handleSignal"
5 @error="handleError"
6 />
7</template>
8
9<script setup>
10const handleSignal = (data) => {
11 console.log('Signal received:', data.message)
12}
13
14const handleError = (code) => {
15 console.error('Error:', code)
16}
17</script>Le gestionnaire reçoit la charge utile comme arguments, c'est pourquoi handleSignal peut lire data.message. Vue fait aussi correspondre la casse des noms : un événement émis sous le nom signalSent s'attrape avec @signal-sent. La documentation recommande le camelCase dans emit et le kebab-case dans le template du parent, mais le kebab-case des deux côtés, comme dans cet exemple, fonctionne aussi. La différence essentielle avec les événements du DOM : les événements de composants ne remontent pas l'arbre. Seul le parent direct les entend, et un composant situé deux niveaux plus haut ne recevra pas le signal, sauf si le parent le relaie.
Charge utile de l'événement
Emit peut transmettre n'importe quelles données comme charge utile - une valeur unique, plusieurs arguments ou tout un objet :
1<script setup>
2const emit = defineEmits(['submit'])
3
4const onSubmit = () => {
5 // Un seul argument
6 emit('submit', 'data')
7
8 // Plusieurs arguments
9 emit('submit', name, email, role)
10
11 // Objet
12 emit('submit', { name, email, role })
13}
14</script>Tous les arguments placés après le nom de l'événement arrivent au gestionnaire dans le même ordre. Je te recommande la troisième forme, un seul objet : il est plus facile d'y ajouter un nouveau champ, et le parent n'a pas à se souvenir de l'ordre des arguments.
Validation des événements
La syntaxe objet de defineEmits permet de vérifier la charge utile avant l'envoi. La clé est le nom de l'événement, et la valeur est soit null (aucune validation), soit une fonction qui reçoit les arguments de emit et renvoie true ou false :
1<script setup>
2const emit = defineEmits({
3 // Aucune validation
4 click: null,
5
6 // Avec validation
7 submit: (payload) => {
8 if (!payload.email) return false
9 if (!payload.email.includes('@')) return false
10 return true
11 }
12})
13</script>Comme pour les props, une validation échouée ne fait qu'afficher un avertissement dans la console en mode développement - l'événement est envoyé malgré tout, donc le parent doit rester prêt à recevoir de mauvaises données. Cet exemple cache aussi un piège : la ligne click: null déclare le nom d'un événement natif. Dès lors, un écouteur @click dans le parent ne réagit plus qu'aux événements émis par le composant, et non aux clics ordinaires sur son élément racine.
Mon conseil : déclare chaque événement, même quand tu n'as pas besoin de validation. Le registre des canaux est la documentation la plus courte d'un module, et les avertissements sur les signaux non déclarés repéreront les fautes de frappe dans les noms. Dans l'éditeur ci-dessous, deux boutons émettent le même signal, et le parent les rassemble dans un seul journal. Les props vers le bas et les événements vers le haut forment ensemble une boucle de données complète - dans la prochaine leçon, tu verras comment Vue fusionne ces deux canaux en une seule directive v-model.
Retiens : les props sont un ordre venu de la Terre, et emit est l'accusé de réception de Mars - deux canaux, chacun dans son sens.
Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3import SignalButton from './SignalButton.vue'
4
5const messages = ref([])
6
7const handleSignal = (data) => {
8 messages.value.push(data)
9}
10</script>
11
12<template>
13 <div class="nova-lab">
14 <h1>Custom Events Demo</h1>
15
16 <SignalButton
17 label="Send Signal Alpha"
18 @signal-sent="handleSignal"
19 />
20 <SignalButton
21 label="Send Signal Beta"
22 @signal-sent="handleSignal"
23 />
24
25 <div class="log">
26 <h3>Received Signals:</h3>
27 <p v-for="(msg, i) in messages" :key="i">
28 {{ msg.timestamp }} - {{ msg.label }}: {{ msg.message }}
29 </p>
30 <p v-if="!messages.length" class="empty">No signals received yet...</p>
31 </div>
32 </div>
33</template>
34
35<style scoped>
36.nova-lab {
37 background: #0a0e27;
38 color: #00ff88;
39 min-height: 100vh;
40 padding: 20px;
41 font-family: monospace;
42}
43h1 { color: #00ff88; margin-bottom: 20px; }
44.log {
45 margin-top: 20px;
46 padding: 15px;
47 background: rgba(0, 180, 216, 0.1);
48 border: 2px solid #00b4d8;
49}
50.log h3 { color: #00b4d8; margin-bottom: 10px; }
51.empty { color: #666; font-style: italic; }
52</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. Que sont les événements personnalisés dans Vue ?
2. Comment émettre un événement dans <script setup> ?
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 composant qui émet un événement 'signal-received' avec une charge utile lorsqu'un bouton est cliqué.
- Tri vertical
Ordonne les étapes du flux d'un événement personnalisé :
- Clique dans l'ordre
Ordonne le code pour émettre un événement avec des données :
- Éditeur de code
Crée un composant avec defineEmits qui valide la charge utile (syntaxe objet).