Vue.js course Β· Module 7: Props & Events
Custom Events
In this lesson4
At the Communication Station, signals don't travel in just one direction! When Mars receives data, it sends an acknowledgment back to Earth. In Vue, a child communicates with its parent through custom events - signals emitted up the component tree.
Props flow only downwards, so a button in the transmitter module cannot change the state of mission control by itself. The operator clicks "Send" - and then what? The button does not own the communication log; the parent does. You need a return channel: the child announces that something happened, and the parent decides what to do about it.
defineEmits
In <script setup> you declare the events a component emits with the defineEmits macro. Like defineProps, it needs no import. It returns an emit function: its first argument is the event name, and the following ones are the data you want to send, the payload:
1<!-- SignalButton.vue (child) -->
2<script setup>
3const emit = defineEmits(['signal-sent', 'error'])
4
5const sendSignal = () => {
6 emit('signal-sent', { message: 'OK', time: Date.now() })
7}
8
9const reportError = (code) => {
10 emit('error', code)
11}
12</script>
13
14<template>
15 <button @click="sendSignal">Send</button>
16</template>The list in defineEmits works like the module's channel register: you can see at a glance which signals the component is able to send. If you emit an event that is not on the list, Vue warns you in the console in development mode. Calling emit changes nothing in the child itself - it only broadcasts the message.
Listening in the Parent
The parent listens to events with @event-name or v-on:event-name, exactly as it listens to clicks on ordinary HTML elements:
1<!-- Parent.vue -->
2<template>
3 <SignalButton
4 @signal-sent="handleSignal"
5 @error="handleError"
6 />
7</template>
8
9<script setup>
10const handleSignal = (data) => {
11 console.log('Signal received:', data.message)
12}
13
14const handleError = (code) => {
15 console.error('Error:', code)
16}
17</script>The handler receives the payload as its arguments, which is why handleSignal can read data.message. Vue also matches the name casing: an event emitted as signalSent can be caught with @signal-sent. The documentation recommends camelCase in emit and kebab-case in the parent's template, but kebab-case on both sides, as in this example, works too. The most important difference from DOM events: component events do not travel up the tree. Only the direct parent hears them, and a component two levels higher will not receive the signal unless the parent passes it on.
Event Payload
Emit can pass any data as the payload - a single value, several arguments or a whole object:
1<script setup>
2const emit = defineEmits(['submit'])
3
4const onSubmit = () => {
5 // Single argument
6 emit('submit', 'data')
7
8 // Multiple arguments
9 emit('submit', name, email, role)
10
11 // Object
12 emit('submit', { name, email, role })
13}
14</script>All arguments after the event name reach the handler in the same order. I recommend the third form, a single object: it is easier to add a new field to it, and the parent does not have to remember which argument was which.
Event Validation
The object syntax of defineEmits lets you check the payload before it is sent. The key is the event name, and the value is either null (no validation) or a function that receives the emit arguments and returns true or false:
1<script setup>
2const emit = defineEmits({
3 // No validation
4 click: null,
5
6 // With validation
7 submit: (payload) => {
8 if (!payload.email) return false
9 if (!payload.email.includes('@')) return false
10 return true
11 }
12})
13</script>Just like with props, a failed validation only prints a console warning in development mode - the event is sent anyway, so the parent should still be ready for bad data. This example also hides a trap: the line click: null declares the name of a native event. From then on, a @click listener in the parent reacts only to events emitted by the component, not to ordinary clicks on its root element.
My advice: declare every event, even when you don't need validation. The channel register is the shortest documentation a module can have, and warnings about undeclared signals will catch typos in names. In the editor below two buttons send the same signal, and the parent collects them in one log. Props going down and events going up form a complete data loop together - in the next lesson you will see how Vue merges these two channels into a single v-model directive.
Remember: props are an order from Earth, and emit is the acknowledgment from Mars - two channels, each going its own way.
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3import SignalButton from './SignalButton.vue'
4
5const messages = ref([])
6
7const handleSignal = (data) => {
8 messages.value.push(data)
9}
10</script>
11
12<template>
13 <div class="nova-lab">
14 <h1>Custom Events Demo</h1>
15
16 <SignalButton
17 label="Send Signal Alpha"
18 @signal-sent="handleSignal"
19 />
20 <SignalButton
21 label="Send Signal Beta"
22 @signal-sent="handleSignal"
23 />
24
25 <div class="log">
26 <h3>Received Signals:</h3>
27 <p v-for="(msg, i) in messages" :key="i">
28 {{ msg.timestamp }} - {{ msg.label }}: {{ msg.message }}
29 </p>
30 <p v-if="!messages.length" class="empty">No signals received yet...</p>
31 </div>
32 </div>
33</template>
34
35<style scoped>
36.nova-lab {
37 background: #0a0e27;
38 color: #00ff88;
39 min-height: 100vh;
40 padding: 20px;
41 font-family: monospace;
42}
43h1 { color: #00ff88; margin-bottom: 20px; }
44.log {
45 margin-top: 20px;
46 padding: 15px;
47 background: rgba(0, 180, 216, 0.1);
48 border: 2px solid #00b4d8;
49}
50.log h3 { color: #00b4d8; margin-bottom: 10px; }
51.empty { color: #666; font-style: italic; }
52</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 are custom events in Vue?
2. How do you emit an event in <script setup>?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Create a component that emits a 'signal-received' event with a payload when a button is clicked.
- Vertical ordering
Arrange the steps of the custom event flow:
- Click in order
Arrange the code for emitting an event with data:
- Code editor
Create a component with defineEmits that validates the payload (object syntax).