Vue.js course Β· Module 5: Events & Forms
Event Handling - Control Buttons
In this lesson5
Welcome to the NOVA LAB Control Simulator! Until now the station's panels only displayed data: they calculated, filtered and reported. The crew, however, has to operate them, and a button that does not react to a click is nothing more than decoration. In plain JavaScript every button would require finding the element, attaching a listener with addEventListener and remembering to remove it when the panel disappears. Vue does this for you with a single directive.
What you will learn
- event handling - how to hook a procedure up to a button, a key or a mouse movement.
- event modifiers - how to block a page reload, stop propagation and react to specific keys.
- v-model - how to bind form fields to variables in both directions.
- v-model modifiers - how to clean up the signal from fields with
.lazy,.numberand.trim. - advanced forms - selecting multiple options, options generated from an array, and validation.
- forms in practice and the project - a complete module registration and a mission registration panel.
Basic Event Handling
The v-on directive listens for a DOM event and runs code when it happens. The full form is v-on:click, and the shorthand used every day is @click. The value can be the name of a function, a method handler, or a short piece of JavaScript, an inline handler. Vue tells them apart by itself: a name like handleClick or a path like panel.start is a method, and anything else, for example count++ or initiateSystem('OXYGEN'), is inline code. First, the panel template with five button variants.
1<template>
2 <div class="control-panel" style="background: #0a0e27; color: #00ff88;">
3 <!-- Full syntax -->
4 <button v-on:click="handleClick" style="background: #00b4d8; color: #0a0e27;">Activate</button>
5
6 <!-- Shorthand syntax (recommended) -->
7 <button @click="handleClick" style="background: #00b4d8; color: #0a0e27;">Activate</button>
8
9 <!-- Inline handler -->
10 <button @click="powerLevel++" style="background: #00b4d8; color: #0a0e27;">Power: {{ powerLevel }}%</button>
11
12 <!-- With argument -->
13 <button @click="initiateSystem('OXYGEN')" style="background: #00b4d8; color: #0a0e27;">Start O2</button>
14
15 <!-- With event object -->
16 <button @click="logEvent($event)" style="background: #00b4d8; color: #0a0e27;">Log Command</button>
17 </div>
18</template>The first two buttons do exactly the same thing and differ only in notation. The third one increments powerLevel directly in the template. The fourth calls a function with an argument, and the fifth passes it the special $event variable, which Vue fills with the native event object. Now the script with the functions these buttons call.
1<script setup>
2import { ref } from 'vue'
3
4const powerLevel = ref(0)
5
6function handleClick() {
7 console.log('System activated!')
8}
9
10function initiateSystem(systemName) {
11 console.log(`Initializing ${systemName} system...`)
12}
13
14function logEvent(event) {
15 console.log('Event:', event.type, event.target)
16}
17</script>Look at handleClick: in the template we gave just the name, without parentheses, so Vue calls the function and passes it the event object as the first argument. We do not use it here, but it is available. What did not change compared to plain JavaScript: these are still ordinary functions and ordinary browser events. Vue only adds attaching the listener and removing it automatically when the component leaves the page.
The event and your own argument
Sometimes a procedure needs both the event object and a piece of information of its own, such as the name of a system. In an inline handler you then pass $event next to the argument. An equivalent form is an arrow function that receives the event as a parameter.
1<template>
2 <button @click="logAction($event, 'oxygen')">Log O2</button>
3 <button @click="(event) => logAction(event, 'water')">Log H2O</button>
4</template>
5
6<script setup>
7function logAction(event, system) {
8 console.log(system, event.type)
9}
10</script>Both buttons call the same function and print the system name and the event type, which is click. You choose the order of the arguments yourself, as long as it matches the function definition. Remember that in an inline handler only $event means the event - a plain event or e is an unknown variable to the template.
Common Events
After @ you can type the name of any DOM event. The template below is a catalog of the most common ones: mouse, keyboard and form events, plus scrolling and image loading. We left out the handler functions, because only the event names matter here.
1<template>
2 <!-- Mouse events -->
3 <div @click="onClick" @dblclick="onDoubleClick">Click</div>
4 <div @mouseenter="onEnter" @mouseleave="onLeave">Hover</div>
5 <div @mousedown="onDown" @mouseup="onUp">Press</div>
6
7 <!-- Keyboard events -->
8 <input @keydown="onKeyDown" @keyup="onKeyUp" @keypress="onKeyPress" />
9
10 <!-- Form events -->
11 <form @submit="onSubmit">
12 <input @input="onInput" @change="onChange" @focus="onFocus" @blur="onBlur" />
13 </form>
14
15 <!-- Other -->
16 <div @scroll="onScroll">Scrollable content</div>
17 <img @load="onLoad" @error="onError" :src="imageUrl" />
18</template>mouseenter and mouseleave report the cursor entering and leaving an element, and unlike mouseover they do not fire again when the cursor moves between the elements inside. input arrives after every change of a field's content, and change after the change is committed. focus and blur mean entering and leaving a field. Watch out for keypress: according to MDN this event is deprecated, so use keydown in new code. Vue does not invent events of its own here - they are all native browser events.
Accessing the Event Object
A method handler receives the event object automatically as its first parameter. The most useful properties are: target, the element on which the event happened, clientX and clientY with the cursor position in the window, buttons with a mask of the pressed mouse buttons, and for the keyboard key, code and the ctrlKey and shiftKey flags.
1<template>
2 <button @click="handleClick">Click</button>
3 <input @keyup="handleKey" />
4</template>
5
6<script setup>
7function handleClick(event) {
8 console.log('Target:', event.target)
9 console.log('Position:', event.clientX, event.clientY)
10 console.log('Buttons:', event.buttons)
11}
12
13function handleKey(event) {
14 console.log('Key:', event.key)
15 console.log('Code:', event.code)
16 console.log('Ctrl:', event.ctrlKey)
17 console.log('Shift:', event.shiftKey)
18}
19</script>key gives the value of the key, for example "Enter" or "a", and code its physical place on the keyboard, for example "KeyA", regardless of the layout. For shortcuts that respond to a character I recommend key, because the key modifiers from the next lesson are built on it. Choose code for controls that depend on key positions, such as WASD. With buttons, keep in mind that during a click event the button may already be released, and the button that triggered the event is reported by the button property.
Remember: @ is the cable between a button on the panel and a procedure in the onboard computer - you give it the name of a function or a short command, and Vue plugs the cable in and unplugs it by itself.
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const signalsSent = ref(0)
5const lastSignalType = ref('')
6const emergencyActive = ref(false)
7
8const sendSignal = (type) => {
9 signalsSent.value++
10 lastSignalType.value = type
11}
12
13const activateEmergency = (event) => {
14 event.preventDefault()
15 emergencyActive.value = true
16 setTimeout(() => {
17 emergencyActive.value = false
18 }, 3000)
19}
20
21const sendOnce = () => {
22 console.log('One-time signal sent!')
23}
24</script>
25
26<template>
27 <div class="communication-panel">
28 <div class="panel-header">
29 <h2>NOVA LAB Communication Panel</h2>
30 <p class="subtitle">Communication Zone - Event Handling</p>
31 </div>
32
33 <div class="stats">
34 <div class="stat-item">
35 <span class="label">Signals sent:</span>
36 <span class="value">{{ signalsSent }}</span>
37 </div>
38 <div class="stat-item">
39 <span class="label">Last type:</span>
40 <span class="value">{{ lastSignalType || 'None' }}</span>
41 </div>
42 </div>
43
44 <div class="signal-buttons">
45 <button @click="sendSignal('Routine')" class="signal-btn routine">
46 Routine Signal
47 </button>
48
49 <button @click="sendSignal('Urgent')" class="signal-btn urgent">
50 Urgent Signal
51 </button>
52
53 <button @click.once="sendOnce" class="signal-btn once">
54 One-time (.once)
55 </button>
56 </div>
57
58 <div class="emergency-section">
59 <a href="#" @click.prevent="activateEmergency" class="emergency-link">
60 Activate Alarm (.prevent)
61 </a>
62
63 <div v-if="emergencyActive" class="emergency-alert">
64 ALARM ACTIVE!
65 </div>
66 </div>
67
68 <div class="event-info">
69 <h3>Event modifiers:</h3>
70 <ul>
71 <li><code>.prevent</code> - prevents the default action</li>
72 <li><code>.stop</code> - stops event propagation</li>
73 <li><code>.once</code> - the event fires only once</li>
74 </ul>
75 </div>
76 </div>
77</template>
78
79<style scoped>
80.communication-panel {
81 background: linear-gradient(135deg, #0a0e27 0%, #1a1e3f 100%);
82 color: #00ff88;
83 padding: 30px;
84 border-radius: 15px;
85 border: 2px solid #00b4d8;
86 font-family: 'Courier New', monospace;
87 max-width: 800px;
88 margin: 0 auto;
89}
90
91.panel-header {
92 text-align: center;
93 margin-bottom: 30px;
94 padding-bottom: 20px;
95 border-bottom: 2px solid #00b4d8;
96}
97
98.panel-header h2 {
99 color: #00ff88;
100 margin: 0 0 10px 0;
101 font-size: 28px;
102 text-shadow: 0 0 10px #00ff88;
103}
104
105.subtitle {
106 color: #00b4d8;
107 margin: 0;
108 font-size: 14px;
109}
110
111.stats {
112 display: grid;
113 grid-template-columns: 1fr 1fr;
114 gap: 15px;
115 margin-bottom: 30px;
116}
117
118.stat-item {
119 background: rgba(0, 180, 216, 0.1);
120 padding: 15px;
121 border-radius: 8px;
122 border: 1px solid #00b4d8;
123 display: flex;
124 justify-content: space-between;
125 align-items: center;
126}
127
128.label {
129 color: #00b4d8;
130 font-size: 14px;
131}
132
133.value {
134 color: #00ff88;
135 font-size: 20px;
136 font-weight: bold;
137}
138
139.signal-buttons {
140 display: grid;
141 gap: 15px;
142 margin-bottom: 30px;
143}
144
145.signal-btn {
146 background: linear-gradient(135deg, #00b4d8 0%, #0077b6 100%);
147 color: white;
148 border: none;
149 padding: 15px 30px;
150 border-radius: 8px;
151 font-size: 16px;
152 font-weight: bold;
153 cursor: pointer;
154 transition: all 0.3s ease;
155 font-family: 'Courier New', monospace;
156}
157
158.signal-btn:hover {
159 transform: translateY(-2px);
160 box-shadow: 0 5px 15px rgba(0, 180, 216, 0.4);
161}
162
163.signal-btn.urgent {
164 background: linear-gradient(135deg, #ff6b35 0%, #d64545 100%);
165}
166
167.signal-btn.once {
168 background: linear-gradient(135deg, #9d4edd 0%, #7209b7 100%);
169}
170
171.emergency-section {
172 text-align: center;
173 margin-bottom: 30px;
174 padding: 20px;
175 background: rgba(255, 107, 53, 0.1);
176 border-radius: 8px;
177 border: 1px solid #ff6b35;
178}
179
180.emergency-link {
181 color: #ff6b35;
182 font-size: 18px;
183 text-decoration: none;
184 font-weight: bold;
185 padding: 10px 20px;
186 display: inline-block;
187 border: 2px solid #ff6b35;
188 border-radius: 8px;
189 transition: all 0.3s ease;
190}
191
192.emergency-link:hover {
193 background: rgba(255, 107, 53, 0.2);
194 transform: scale(1.05);
195}
196
197.emergency-alert {
198 margin-top: 15px;
199 padding: 15px;
200 background: #ff6b35;
201 color: white;
202 border-radius: 8px;
203 font-weight: bold;
204 animation: pulse 1s infinite;
205}
206
207@keyframes pulse {
208 0%, 100% { opacity: 1; }
209 50% { opacity: 0.7; }
210}
211
212.event-info {
213 background: rgba(0, 255, 136, 0.05);
214 padding: 20px;
215 border-radius: 8px;
216 border: 1px solid #00ff88;
217}
218
219.event-info h3 {
220 color: #00ff88;
221 margin-top: 0;
222 font-size: 18px;
223}
224
225.event-info ul {
226 margin: 0;
227 padding-left: 20px;
228}
229
230.event-info li {
231 color: #00b4d8;
232 margin: 8px 0;
233}
234
235.event-info code {
236 background: rgba(0, 255, 136, 0.2);
237 padding: 2px 6px;
238 border-radius: 4px;
239 color: #00ff88;
240 font-weight: bold;
241}
242</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. How do you add a click event listener in Vue.js?
2. What symbol is a shorthand for the v-on directive?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Add a @click handler that increments a counter and display the value.
- Click in order
Arrange the syntax for adding a click handler in Vue:
- Code editor
Add @mouseenter, @mouseleave, and @dblclick handlers to a div element.
- Horizontal ordering
Arrange the correct syntax for a handler passing event and argument: