Vue.js course Β· Module 5: Events & Forms

Event Handling - Control Buttons

6 min read
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, .number and .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. 1. How do you add a click event listener in Vue.js?

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

Useful articles