Vue.js course · Module 5: Events & Forms
Event Modifiers
In this lesson4
The first test of the Control Simulator ended in a failure. An operator submitted a form with a command, and the browser reloaded the page and wiped the entire simulator state. A moment later the operator clicked a button inside a panel that also reacts to clicks itself, and two procedures ran at once. In plain JavaScript you cure this with calls to event.preventDefault() and event.stopPropagation() at the start of the function. Vue has a shorter notation: modifiers, a dot and a name appended to the event name.
How an event travels through the DOM
To understand modifiers, you need to know how an event travels. A button click goes through three phases. In the capture phase the event descends from the browser window and document down the tree to the clicked element. In the target phase it reaches the button itself. In the bubble phase it goes back up: from the button to the parent <div>, then to <body>, <html> and document. A listener attached with @click works in the bubble phase by default, which is why clicking the button also runs the parent's procedure.
Event Modifiers
.prevent calls event.preventDefault(), which blocks the browser's default action, for example the page reload after a form is submitted. .stop calls event.stopPropagation() and stops the bubbling towards the parents. .once lets the procedure work only on the first event. The first part of the template shows these three modifiers.
1<template>
2 <!-- .prevent - prevents default action -->
3 <form @submit.prevent="onSubmit">
4 <button type="submit">Submit</button>
5 </form>
6
7 <!-- .stop - stops propagation -->
8 <div @click="outer">
9 <button @click.stop="inner">Only inner</button>
10 </div>
11
12 <!-- .once - only once -->
13 <button @click.once="runOnce">Only once</button>The form submits without reloading the page and calls onSubmit. Clicking the button inside the <div> prints only inner, because .stop does not let the event out to the parent. The button with .once reacts once, and further clicks pass without a trace. The second part of the template shows the modifiers related to event phases.
1 <!-- .self - only on this element -->
2 <div @click.self="onlyThis">
3 <button>Click on button won't trigger onlyThis</button>
4 </div>
5
6 <!-- .capture - capture mode -->
7 <div @click.capture="captureFirst">
8 <button @click="child">Child</button>
9 </div>
10
11 <!-- .passive - for scroll performance -->
12 <div @scroll.passive="onScroll">Scroll content</div>
13
14 <!-- Combinations -->
15 <a @click.stop.prevent="handleLink">Link</a>
16</template>.self runs the procedure only when the event happened on the element itself, not on one of its children. .capture attaches the listener in the capture phase, so captureFirst runs before the child's procedure. .passive tells the browser that the procedure will not block the default action, so scrolling can start right away, without waiting for it. It is most often added to touch and mouse wheel events. Do not combine it with .prevent, because the two contradict each other. Modifiers can be chained, as in @click.stop.prevent, but the order matters: @click.prevent.self blocks the default action for clicks on the children too, while @click.self.prevent blocks it only for clicks on the element itself. Finally, the script with the functions.
1<script setup>
2function onSubmit() {
3 console.log('Form submitted without page reload')
4}
5
6function outer() { console.log('outer') }
7function inner() { console.log('inner') }
8function runOnce() { console.log('This runs only once') }
9function onlyThis() { console.log('Clicked on div itself') }
10</script>The functions are simple, because all the event logic lives in the template, and we left out the remaining procedures, such as captureFirst, for brevity. I recommend this rule: a modifier instead of a manual preventDefault in the function. The procedure stays pure mission logic, and the browser behavior is visible right in the template.
Key Modifiers
With keyboard events, keyup and keydown, a modifier can point to a specific key. Vue has the aliases .enter, .tab, .delete (captures both Delete and Backspace), .esc, .space and the arrows .up, .down, .left and .right. Any name from event.key also works when written in lowercase with dashes, for example .page-down.
1<template>
2 <!-- Specific keys -->
3 <input @keyup.enter="submit" placeholder="Enter to submit" />
4 <input @keyup.esc="cancel" placeholder="Esc to cancel" />
5
6 <!-- Arrow keys -->
7 <div tabindex="0" @keyup.up="moveUp" @keyup.down="moveDown"
8 @keyup.left="moveLeft" @keyup.right="moveRight">Control panel</div>
9
10 <!-- Function keys -->
11 <input @keyup.delete="deleteItem" />
12 <input @keyup.space="togglePlay" />
13 <input @keyup.tab="nextField" />The arrow panel has tabindex="0", because a plain <div> does not take focus, and keyboard events go to the focused element. Watch out for @keyup.tab: Tab moves the focus to the next field, so the keyup event already lands there, and you catch leaving a field with @keydown.tab. The system keys .ctrl, .alt, .shift and .meta (Cmd on a Mac, the Windows key on a PC) combine with others into shortcuts.
1 <!-- Combinations with modifiers -->
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 on Mac -->
6
7 <!-- Exact modifier -->
8 <input @keyup.ctrl.exact="onlyCtrl" /> <!-- Only Ctrl, not Ctrl+Shift -->
9</template>@keyup.ctrl.enter fires when you release Enter while holding Ctrl, because with keyup the system key has to be pressed at the moment of the event. .exact requires exactly this set of system keys: @keyup.ctrl.exact will not react when you hold Shift together with Ctrl.
Mouse Modifiers
The .left, .right and .middle modifiers restrict the procedure to the main, secondary and auxiliary button. The names assume a right-handed mouse, but they stand for the roles of the buttons, not their physical position.
1<template>
2 <!-- Mouse buttons -->
3 <div @click.left="leftClick">Left click</div>
4 <div @click.right="rightClick">Right click</div>
5 <div @click.middle="middleClick">Middle click</div>
6
7 <!-- Context menu -->
8 <div @contextmenu.prevent="showCustomMenu">
9 Right click = custom menu
10 </div>
11</template>
12
13<script setup>
14function leftClick() { console.log('Left') }
15function rightClick() { console.log('Right') }
16function middleClick() { console.log('Middle') }
17function showCustomMenu(e) {
18 console.log('Custom menu at:', e.clientX, e.clientY)
19}
20</script>Browsers send click only for the main button, which is why Vue compiles @click.right to the contextmenu event and @click.middle to mouseup. Hence a practical conclusion: .right on its own will not hide the browser menu. You build your own context menu with @contextmenu.prevent, as in the last element of the example, and e.clientX and e.clientY tell you where to open it. In the next lesson you will move on to forms and the v-model directive.
Remember: a modifier is a filter on the signal line - it lets only the right commands through to the procedure and suppresses the ones the browser would carry out on its own.
Code for this lesson: 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('⏎ Enter: Command executed!')
21 if (secretCode.value === 'MARS') {
22 addLog('Access code accepted!')
23 }
24}
25
26const handleEscape = () => {
27 addLog('⎋ Escape: Operation cancelled')
28 secretCode.value = ''
29}
30
31const handleDelete = () => {
32 addLog('⌫ Delete: Data removed')
33}
34
35const handleLeftClick = () => {
36 mouseAction.value = 'Left button: Scanning the left sector'
37 addLog('Left mouse button')
38}
39
40const handleRightClick = (event) => {
41 event.preventDefault()
42 mouseAction.value = 'Right button: Scanning the right sector'
43 addLog('Right mouse button')
44}
45
46const handleMiddleClick = () => {
47 mouseAction.value = 'Middle button: Scanning central sector'
48 addLog('Middle mouse button')
49}
50</script>
51
52<template>
53 <div class="command-center">
54 <div class="center-header">
55 <h2>⌨NOVA LAB Command Center</h2>
56 <p class="subtitle">Key and Mouse Modifiers</p>
57 </div>
58
59 <div class="input-section">
60 <div class="input-group">
61 <label>Access code (type MARS and press Enter):</label>
62 <input
63 v-model="secretCode"
64 @keyup.enter="handleEnter"
65 @keyup.esc="handleEscape"
66 @keyup.delete="handleDelete"
67 type="text"
68 placeholder="Enter code..."
69 class="command-input"
70 />
71 </div>
72
73 <div class="key-hints">
74 <span class="hint">Enter - Execute</span>
75 <span class="hint">Esc - Cancel</span>
76 <span class="hint">Delete - Remove</span>
77 </div>
78 </div>
79
80 <div class="mouse-section">
81 <h3>Mouse Scanner Control</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>Click with different mouse buttons</p>
90 <p class="action-display">{{ mouseAction || 'Waiting for command...' }}</p>
91 </div>
92 </div>
93 </div>
94
95 <div class="log-section">
96 <h3>Command Log</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 No activity...
108 </div>
109 </div>
110 </div>
111
112 <div class="modifiers-info">
113 <h3>Available modifiers:</h3>
114 <div class="modifier-grid">
115 <div class="modifier-item">
116 <strong>Keys:</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>Mouse:</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>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the .prevent modifier do in @submit.prevent?
2. When do we use the .stop modifier?
These are 2 of 7 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a form with @submit.prevent and a button with @click.once.
- Horizontal ordering
Arrange the syntax for a form with the .prevent modifier:
- Vertical ordering
Arrange the DOM event propagation phases in order:
- Click in order
Click in the correct order to build @click.stop.prevent:
- Code editor
Create an input that responds to @keyup.enter and @keyup.esc.
- Vertical ordering
Arrange the order of event propagation from bottom to top (bubbling):