Cours Vue.js · Module 7: Props et événements
Projet : système de communication du NOVA LAB
Dans cette leçon4
Le projet final de la station de communication ! Il est temps d'appliquer tous les motifs de communication entre composants dans un seul projet cohérent. Tu vas construire un système de gestion des modules de la station NOVA LAB qui réunit toutes les techniques apprises.
Jusqu'ici, tu as pratiqué chaque canal séparément, sur deux composants. Un vrai panneau de station, ce sont plusieurs composants à la fois, et avec eux de vraies questions : qui est propriétaire de la liste des modules, qui peut changer le statut du générateur, et comment la barre d'état peut-elle savoir que le filtre à eau est passé hors ligne ? Dans ce projet, tu y répondras en lisant un système qui fonctionne.
Résumé du module
Dans ce module, tu as découvert l'ensemble complet des outils de communication entre composants Vue :
- Props - transmission de données à sens unique du parent vers l'enfant, avec typage, validation et valeurs par défaut. Définies via
defineProps() - Events (Emit) - communication enfant-parent par l'émission d'événements avec une charge utile optionnelle. Définis via
defineEmits()avec validation des arguments - v-model sur les composants - liaison de données bidirectionnelle avec un composant, basée en interne sur la prop
modelValueet l'émissionupdate:modelValue, écrite plus brièvement avecdefineModel()depuis Vue 3.4. Prise en charge de plusieurs v-model avec des liaisons nommées - Provide/Inject - transmission de données à travers plusieurs niveaux de l'arbre de composants sans « prop drilling ». Motif readonly + actions pour une modification sûre de l'état
- Attributs fallthrough - héritage automatique des attributs HTML, contrôlé via
inheritAttrset$attrs - Motifs avancés - combinaison des props, des événements, de provide/inject et de v-model dans des architectures complexes, plus
defineExposecomme port de maintenance d'un composant
Architecture du projet
Avant d'ouvrir les fichiers, regarde la carte des connexions. Chaque entrée entre parenthèses indique par quel canal un composant reçoit ou envoie des données :
1App (provide: modules, actions)
2 ├── StatusBar (inject: modules → computed online count)
3 └── ControlPanel (inject: modules)
4 └── ModuleCard[] (props: module, emit: status-change)App est le seul propriétaire de l'état. StatusBar et ControlPanel se trouvent côte à côte comme enfants de App et injectent tous les deux la liste des modules, tandis que ModuleCard reçoit son module via les props de ControlPanel.
Tâche
Analyse le code dans l'éditeur et comprends comment tous les motifs de communication fonctionnent ensemble :
- App.vue - Fournisseur avec l'état central des modules dans un
refet des actions (provide readonly state + action functions) - StatusBar.vue - Inject de l'état des modules + un computed qui compte les modules en ligne par rapport au total
- ControlPanel.vue - Inject des modules + rendu des cartes avec v-for, transmission des données via les props
- ModuleCard.vue - Reçoit les données via les props, émet les changements de statut vers le parent, utilise inject pour appeler les actions
Commence par l'émetteur. App expose la liste des modules en lecture seule, et à côté les actions qui sont les seuls moyens de modifier l'état :
1provide('modules', readonly(modules))
2provide('selectedId', selectedId)
3provide('selectModule', (id) => { selectedId.value = id })
4provide('updateStatus', (id, status) => {
5 const m = modules.value.find(m => m.id === id)
6 if (m) m.status = status
7})L'action updateStatus modifie le statut à l'intérieur de App, donc StatusBar recalcule son computed tout seul, sans aucun signal supplémentaire. Remarque une exception à la règle : selectedId arrive chez les descendants sans readonly.
Passons au récepteur, à l'autre bout de la liaison. ModuleCard combine deux canaux dans une seule fonction :
1const toggle = () => {
2 const newStatus = props.module.status === 'online' ? 'offline' : 'online'
3 updateStatus(props.module.id, newStatus)
4 emit('status-change', props.module.id, newStatus)
5}La carte n'affecte rien à props.module - elle demande une modification à App via l'action injectée, et envoie une notification à son propre parent, ControlPanel, par un événement. Dans le template, le bouton a le modificateur @click.stop pour que le changement de statut ne sélectionne pas en même temps toute la carte.
Observations clés :
- L'état est fourni par App et injecté dans StatusBar et ControlPanel - aucun des deux ne reçoit la liste via les props
- Les actions
selectModuleetupdateStatusvont directement de App à ModuleCard - elles sautent le niveau ControlPanel - ControlPanel transmet les modules via les props à ModuleCard - motif parent-enfant classique
- ModuleCard utilise inject pour les actions et emit pour les notifications - deux canaux de communication différents
- Le motif readonly + actions protège la liste des modules contre la modification directe - seul App peut modifier les données
Développe le système
Trois dernières tâches. D'abord, enveloppe selectedId dans readonly et vérifie que la sélection des cartes fonctionne toujours, puisqu'elle passe par l'action selectModule. Ensuite, le gestionnaire @status-change de ControlPanel est vide - ajoute un journal des changements affiché sous les cartes. Enfin, ajoute à ModuleCard un champ de note du technicien géré avec v-model, ou une validation de la prop module. Fais-les dans l'ordre, et à chaque étape demande-toi quel composant est propriétaire des données modifiées. À la prochaine station, le Module Storage, tu y ajouteras les slots et les composants dynamiques.
Retiens : dans une station bien conçue, chaque donnée a un seul propriétaire, et les autres modules écoutent ses signaux et lui demandent les modifications.
Code de cette leçon : App.vue
1<script setup>
2import { ref, provide, readonly } from 'vue'
3import ControlPanel from './ControlPanel.vue'
4import StatusBar from './StatusBar.vue'
5
6// État central de la station
7const modules = ref([
8 { id: 1, name: 'Oxygen Gen', status: 'online', temp: -12 },
9 { id: 2, name: 'Water Recycler', status: 'online', temp: 22 },
10 { id: 3, name: 'Solar Array', status: 'maintenance', temp: 85 }
11])
12const selectedId = ref(null)
13
14// Provide : état global
15provide('modules', readonly(modules))
16provide('selectedId', selectedId)
17provide('selectModule', (id) => { selectedId.value = id })
18provide('updateStatus', (id, status) => {
19 const m = modules.value.find(m => m.id === id)
20 if (m) m.status = status
21})
22</script>
23
24<template>
25 <div class="nova-lab">
26 <h1>NOVA LAB Communication System</h1>
27 <StatusBar />
28 <ControlPanel />
29 </div>
30</template>
31
32<style scoped>
33.nova-lab {
34 background: #0a0e27;
35 color: #00ff88;
36 min-height: 100vh;
37 padding: 20px;
38 font-family: monospace;
39}
40h1 { color: #00ff88; margin-bottom: 20px; }
41</style>Tu as repéré une erreur dans cette leçon ?