Cours Vue.js · Module 5: Événements et formulaires

Modificateurs d'événements

6 min de lecture
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. 1. Que fait le modificateur .prevent dans @submit.prevent ?

  2. 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) :