Cours Vue.js · Module 5: Événements et formulaires
Modificateurs d'événements
Dans cette leçon4
Le premier essai du Simulateur de Contrôle s'est terminé par une panne. Un opérateur a envoyé un formulaire de commande, et le navigateur a rechargé la page en effaçant tout l'état du simulateur. Un instant plus tard, l'opérateur a cliqué sur un bouton placé dans un panneau qui réagit lui aussi aux clics, et deux procédures se sont exécutées en même temps. En JavaScript pur, on soigne cela avec des appels à event.preventDefault() et event.stopPropagation() au début de la fonction. Vue a une écriture plus courte : les modificateurs, un point et un nom ajoutés au nom de l'événement.
Comment un événement voyage dans le DOM
Pour comprendre les modificateurs, il faut savoir comment voyage un événement. Un clic sur un bouton traverse trois phases. Dans la phase de capture, l'événement descend de la fenêtre du navigateur et de document le long de l'arbre, jusqu'à l'élément cliqué. Dans la phase cible (target), il atteint le bouton lui-même. Dans la phase de bouillonnement (bubble), il remonte : du bouton vers le <div> parent, puis vers <body>, <html> et document. Un écouteur branché avec @click fonctionne par défaut dans la phase de bouillonnement, c'est pourquoi un clic sur le bouton lance aussi la procédure du parent.
Modificateurs d'événements
.prevent appelle event.preventDefault(), c'est-à-dire qu'il bloque l'action par défaut du navigateur, par exemple le rechargement de la page après l'envoi d'un formulaire. .stop appelle event.stopPropagation() et arrête le bouillonnement vers les parents. .once ne laisse la procédure fonctionner qu'au premier événement. La première partie du template montre ces trois modificateurs.
1<template>
2 <!-- .prevent - empêche l'action par défaut -->
3 <form @submit.prevent="onSubmit">
4 <button type="submit">Envoyer</button>
5 </form>
6
7 <!-- .stop - arrête la propagation -->
8 <div @click="outer">
9 <button @click.stop="inner">Uniquement l'intérieur</button>
10 </div>
11
12 <!-- .once - une seule fois -->
13 <button @click.once="runOnce">Une seule fois</button>Le formulaire s'envoie sans recharger la page et appelle onSubmit. Un clic sur le bouton à l'intérieur du <div> n'affiche que le message de la procédure intérieure, car .stop ne laisse pas l'événement remonter jusqu'au parent. Le bouton avec .once réagit une fois, et les clics suivants passent sans effet. La seconde partie du template montre les modificateurs liés aux phases de l'événement.
1 <!-- .self - uniquement sur cet élément -->
2 <div @click.self="onlyThis">
3 <button>Un clic sur le bouton ne déclenchera pas onlyThis</button>
4 </div>
5
6 <!-- .capture - mode capture -->
7 <div @click.capture="captureFirst">
8 <button @click="child">Enfant</button>
9 </div>
10
11 <!-- .passive - pour la performance du défilement -->
12 <div @scroll.passive="onScroll">Contenu défilable</div>
13
14 <!-- Combinaisons -->
15 <a @click.stop.prevent="handleLink">Lien</a>
16</template>.self lance la procédure seulement si l'événement s'est produit sur l'élément lui-même, et non sur l'un de ses enfants. .capture branche l'écouteur dans la phase de capture, donc captureFirst s'exécute avant la procédure de l'enfant. .passive indique au navigateur que la procédure ne bloquera pas l'action par défaut, donc le défilement peut démarrer tout de suite, sans l'attendre. On l'ajoute le plus souvent aux événements tactiles et à ceux de la molette. Ne le combine pas avec .prevent, car les deux se contredisent. Les modificateurs peuvent s'enchaîner, comme dans @click.stop.prevent, mais l'ordre compte : @click.prevent.self bloque l'action par défaut aussi pour les clics sur les enfants, alors que @click.self.prevent la bloque seulement pour les clics sur l'élément lui-même. Pour finir, le script avec les fonctions.
1<script setup>
2function onSubmit() {
3 console.log('Formulaire envoyé sans rechargement de la page')
4}
5
6function outer() { console.log('extérieur') }
7function inner() { console.log('intérieur') }
8function runOnce() { console.log('Ceci ne s\'exécute qu\'une seule fois') }
9function onlyThis() { console.log('Clic sur le div lui-même') }
10</script>Les fonctions sont simples, car toute la logique des événements se trouve dans le template, et nous avons laissé de côté les autres procédures, comme captureFirst, pour rester brefs. Je te recommande cette règle : un modificateur plutôt qu'un preventDefault manuel dans la fonction. La procédure reste de la pure logique de mission, et le comportement du navigateur se voit directement dans le template.
Modificateurs de touches
Avec les événements du clavier, keyup et keydown, un modificateur peut désigner une touche précise. Vue propose les alias .enter, .tab, .delete (qui capture Delete et Backspace), .esc, .space et les flèches .up, .down, .left et .right. N'importe quel nom issu de event.key fonctionne aussi, écrit en minuscules avec des tirets, par exemple .page-down.
1<template>
2 <!-- Touches spécifiques -->
3 <input @keyup.enter="submit" placeholder="Entrée pour envoyer" />
4 <input @keyup.esc="cancel" placeholder="Échap pour annuler" />
5
6 <!-- Touches fléchées -->
7 <div tabindex="0" @keyup.up="moveUp" @keyup.down="moveDown"
8 @keyup.left="moveLeft" @keyup.right="moveRight">Panneau de commande</div>
9
10 <!-- Touches fonctionnelles -->
11 <input @keyup.delete="deleteItem" />
12 <input @keyup.space="togglePlay" />
13 <input @keyup.tab="nextField" />Le panneau des flèches a tabindex="0", car un simple <div> ne reçoit pas le focus, et les événements du clavier vont à l'élément qui a le focus. Attention à @keyup.tab : Tab déplace le focus vers le champ suivant, donc l'événement keyup arrive déjà là-bas, et tu captes la sortie d'un champ avec @keydown.tab. Les touches système .ctrl, .alt, .shift et .meta (Cmd sur Mac, la touche Windows sur PC) se combinent avec d'autres en raccourcis.
1 <!-- Combinaisons avec des modificateurs -->
2 <input @keyup.ctrl.enter="submitWithCtrl" />
3 <input @keyup.shift.delete="forceDelete" />
4 <input @keyup.alt.s="saveShortcut" />
5 <input @keyup.meta.s="macSave" /> <!-- Cmd sur Mac -->
6
7 <!-- Modificateur exact -->
8 <input @keyup.ctrl.exact="onlyCtrl" /> <!-- Uniquement Ctrl, pas Ctrl+Shift -->
9</template>@keyup.ctrl.enter se déclenche quand tu relâches Entrée en maintenant Ctrl, car avec keyup la touche système doit être enfoncée au moment de l'événement. .exact exige exactement cet ensemble de touches système : @keyup.ctrl.exact ne réagira pas si tu maintiens Shift en même temps que Ctrl.
Modificateurs de la souris
Les modificateurs .left, .right et .middle limitent la procédure au bouton principal, secondaire et auxiliaire. Les noms supposent une souris pour droitier, mais ils désignent le rôle des boutons, et non leur position physique.
1<template>
2 <!-- Boutons de la souris -->
3 <div @click.left="leftClick">Clic gauche</div>
4 <div @click.right="rightClick">Clic droit</div>
5 <div @click.middle="middleClick">Clic du milieu</div>
6
7 <!-- Menu contextuel -->
8 <div @contextmenu.prevent="showCustomMenu">
9 Clic droit = menu personnalisé
10 </div>
11</template>
12
13<script setup>
14function leftClick() { console.log('Gauche') }
15function rightClick() { console.log('Droit') }
16function middleClick() { console.log('Milieu') }
17function showCustomMenu(e) {
18 console.log('Menu personnalisé à :', e.clientX, e.clientY)
19}
20</script>Les navigateurs n'envoient click que pour le bouton principal, c'est pourquoi Vue compile @click.right en événement contextmenu et @click.middle en mouseup. D'où une conclusion pratique : .right à lui seul ne masquera pas le menu du navigateur. Tu construis ton propre menu contextuel avec @contextmenu.prevent, comme dans le dernier élément de l'exemple, et e.clientX et e.clientY t'indiquent où l'ouvrir. Dans la prochaine leçon, tu passeras aux formulaires et à la directive v-model.
Souviens-toi : un modificateur est un filtre sur la ligne de signal - il ne laisse passer vers la procédure que les bonnes commandes et étouffe celles que le navigateur exécuterait de son propre chef.
Code de cette leçon : App.vue
1<script setup>
2import { ref } from 'vue'
3
4const commandLog = ref([])
5const secretCode = ref('')
6const mouseAction = ref('')
7
8const addLog = (message) => {
9 commandLog.value.unshift({
10 id: Date.now(),
11 message,
12 time: new Date().toLocaleTimeString()
13 })
14 if (commandLog.value.length > 5) {
15 commandLog.value.pop()
16 }
17}
18
19const handleEnter = () => {
20 addLog('⏎ Entrée : Commande exécutée !')
21 if (secretCode.value === 'MARS') {
22 addLog('Code d\'accès accepté !')
23 }
24}
25
26const handleEscape = () => {
27 addLog('⎋ Échap : Opération annulée')
28 secretCode.value = ''
29}
30
31const handleDelete = () => {
32 addLog('⌫ Suppr : Données effacées')
33}
34
35const handleLeftClick = () => {
36 mouseAction.value = 'Bouton gauche : Scan du secteur gauche'
37 addLog('Bouton gauche de la souris')
38}
39
40const handleRightClick = (event) => {
41 event.preventDefault()
42 mouseAction.value = 'Bouton droit : Scan du secteur droit'
43 addLog('Bouton droit de la souris')
44}
45
46const handleMiddleClick = () => {
47 mouseAction.value = 'Bouton du milieu : Scan du secteur central'
48 addLog('Bouton du milieu de la souris')
49}
50</script>
51
52<template>
53 <div class="command-center">
54 <div class="center-header">
55 <h2>⌨Centre de Commande NOVA LAB</h2>
56 <p class="subtitle">Modificateurs de Touches et de Souris</p>
57 </div>
58
59 <div class="input-section">
60 <div class="input-group">
61 <label>Code d'accès (tape MARS et appuie sur Entrée) :</label>
62 <input
63 v-model="secretCode"
64 @keyup.enter="handleEnter"
65 @keyup.esc="handleEscape"
66 @keyup.delete="handleDelete"
67 type="text"
68 placeholder="Saisis le code..."
69 class="command-input"
70 />
71 </div>
72
73 <div class="key-hints">
74 <span class="hint">Entrée - Exécuter</span>
75 <span class="hint">Échap - Annuler</span>
76 <span class="hint">Suppr - Effacer</span>
77 </div>
78 </div>
79
80 <div class="mouse-section">
81 <h3>Contrôle du Scanner à la Souris</h3>
82 <div class="scanner-area">
83 <div
84 class="scan-zone"
85 @click.left="handleLeftClick"
86 @click.right="handleRightClick"
87 @click.middle="handleMiddleClick"
88 >
89 <p>Clique avec différents boutons de la souris</p>
90 <p class="action-display">{{ mouseAction || 'En attente de commande...' }}</p>
91 </div>
92 </div>
93 </div>
94
95 <div class="log-section">
96 <h3>Journal des Commandes</h3>
97 <div class="log-container">
98 <div
99 v-for="log in commandLog"
100 :key="log.id"
101 class="log-entry"
102 >
103 <span class="log-time">{{ log.time }}</span>
104 <span class="log-message">{{ log.message }}</span>
105 </div>
106 <div v-if="commandLog.length === 0" class="log-empty">
107 Aucune activité...
108 </div>
109 </div>
110 </div>
111
112 <div class="modifiers-info">
113 <h3>Modificateurs disponibles :</h3>
114 <div class="modifier-grid">
115 <div class="modifier-item">
116 <strong>Touches :</strong>
117 <code>.enter</code>
118 <code>.esc</code>
119 <code>.delete</code>
120 <code>.tab</code>
121 <code>.space</code>
122 </div>
123 <div class="modifier-item">
124 <strong>Souris :</strong>
125 <code>.left</code>
126 <code>.right</code>
127 <code>.middle</code>
128 </div>
129 </div>
130 </div>
131 </div>
132</template>
133
134<style scoped>
135.command-center {
136 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
137 color: #00ff88;
138 padding: 30px;
139 border-radius: 15px;
140 border: 2px solid #00b4d8;
141 font-family: 'Courier New', monospace;
142 max-width: 900px;
143 margin: 0 auto;
144}
145
146.center-header {
147 text-align: center;
148 margin-bottom: 30px;
149 padding-bottom: 20px;
150 border-bottom: 2px solid #00b4d8;
151}
152
153.center-header h2 {
154 color: #00ff88;
155 margin: 0 0 10px 0;
156 font-size: 28px;
157 text-shadow: 0 0 10px #00ff88;
158}
159
160.subtitle {
161 color: #00b4d8;
162 margin: 0;
163 font-size: 14px;
164}
165
166.input-section {
167 margin-bottom: 30px;
168}
169
170.input-group {
171 margin-bottom: 15px;
172}
173
174.input-group label {
175 display: block;
176 color: #00b4d8;
177 margin-bottom: 10px;
178 font-size: 14px;
179}
180
181.command-input {
182 width: 100%;
183 background: rgba(0, 180, 216, 0.1);
184 border: 2px solid #00b4d8;
185 border-radius: 8px;
186 padding: 15px;
187 color: #00ff88;
188 font-size: 16px;
189 font-family: 'Courier New', monospace;
190 box-sizing: border-box;
191}
192
193.command-input:focus {
194 outline: none;
195 border-color: #00ff88;
196 box-shadow: 0 0 15px rgba(0, 255, 136, 0.3);
197}
198
199.key-hints {
200 display: flex;
201 gap: 10px;
202 flex-wrap: wrap;
203}
204
205.hint {
206 background: rgba(0, 255, 136, 0.1);
207 border: 1px solid #00ff88;
208 padding: 5px 12px;
209 border-radius: 20px;
210 font-size: 12px;
211 color: #00ff88;
212}
213
214.mouse-section {
215 margin-bottom: 30px;
216}
217
218.mouse-section h3 {
219 color: #00ff88;
220 margin-bottom: 15px;
221}
222
223.scanner-area {
224 background: rgba(0, 180, 216, 0.05);
225 padding: 20px;
226 border-radius: 8px;
227 border: 2px dashed #00b4d8;
228}
229
230.scan-zone {
231 background: linear-gradient(135deg, rgba(0, 180, 216, 0.2) 0%, rgba(0, 255, 136, 0.1) 100%);
232 padding: 40px;
233 border-radius: 8px;
234 text-align: center;
235 cursor: pointer;
236 transition: all 0.3s ease;
237}
238
239.scan-zone:hover {
240 background: linear-gradient(135deg, rgba(0, 180, 216, 0.3) 0%, rgba(0, 255, 136, 0.2) 100%);
241 transform: scale(1.02);
242}
243
244.scan-zone p {
245 margin: 0 0 10px 0;
246 color: #00b4d8;
247}
248
249.action-display {
250 color: #00ff88 !important;
251 font-weight: bold;
252 font-size: 18px;
253}
254
255.log-section {
256 margin-bottom: 30px;
257}
258
259.log-section h3 {
260 color: #00ff88;
261 margin-bottom: 15px;
262}
263
264.log-container {
265 background: rgba(0, 0, 0, 0.3);
266 border: 1px solid #00b4d8;
267 border-radius: 8px;
268 padding: 15px;
269 max-height: 200px;
270 overflow-y: auto;
271}
272
273.log-entry {
274 padding: 8px;
275 margin-bottom: 5px;
276 background: rgba(0, 180, 216, 0.1);
277 border-radius: 4px;
278 display: flex;
279 gap: 15px;
280}
281
282.log-time {
283 color: #00b4d8;
284 font-size: 12px;
285}
286
287.log-message {
288 color: #00ff88;
289 flex: 1;
290}
291
292.log-empty {
293 color: #00b4d8;
294 text-align: center;
295 opacity: 0.5;
296}
297
298.modifiers-info {
299 background: rgba(0, 255, 136, 0.05);
300 padding: 20px;
301 border-radius: 8px;
302 border: 1px solid #00ff88;
303}
304
305.modifiers-info h3 {
306 color: #00ff88;
307 margin-top: 0;
308}
309
310.modifier-grid {
311 display: grid;
312 grid-template-columns: 1fr 1fr;
313 gap: 20px;
314}
315
316.modifier-item {
317 color: #00b4d8;
318}
319
320.modifier-item strong {
321 display: block;
322 color: #00ff88;
323 margin-bottom: 10px;
324}
325
326.modifier-item code {
327 background: rgba(0, 255, 136, 0.2);
328 padding: 4px 8px;
329 border-radius: 4px;
330 color: #00ff88;
331 font-size: 12px;
332 margin-right: 5px;
333 display: inline-block;
334 margin-bottom: 5px;
335}
336</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 fait le modificateur .prevent dans @submit.prevent ?
2. Quand utilise-t-on le modificateur .stop ?
Voici 2 des 7 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Crée un formulaire avec @submit.prevent et un bouton avec @click.once.
- Tri horizontal
Ordonne la syntaxe pour un formulaire avec le modificateur .prevent :
- Tri vertical
Ordonne les phases de propagation des événements du DOM dans l'ordre :
- Clique dans l'ordre
Clique dans le bon ordre pour construire @click.stop.prevent :
- Éditeur de code
Crée un input qui répond à @keyup.enter et @keyup.esc.
- Tri vertical
Ordonne l'ordre de propagation de l'événement de bas en haut (bouillonnement) :