Vue.js 3 Component Events Tutorial

In this Vue tutorial we learn how to handle events and send data from one component to another up the tree.

We cover how to define and emit events, listen for emitted events, send data and validation.

Lesson Video

If you prefer to learn visually, you can watch this lesson in video format.

Lesson Project

If you want to follow along with the examples, you will need an app generated by the Vue CLI as well as the following extra components.

  • src/components/MainMenu.vue

The project should look similar to the following.

Example: project
project-folder/
├── src/
|   ├── components/
|   |   ├── MainMenu.vue
|   └── App.vue

We want to nest MainMenu inside the root App component underneath a button that shows the MainMenu when clicked.

Example: src/App.vue
<template>
  <button @click="isMenuOpen = true">Open Menu</button>
  <main-menu v-show="isMenuOpen" />
</template>

<script>
  import MainMenu from './components/MainMenu.vue'

  export default {
    components: { MainMenu },
    data() {
      return {
        isMenuOpen: false
      }
    }
  }
</script>

In MainMenu , we’ll have a paragraph with some identifying text and a button below it.

Example: src/components/MainMenu.vue
<template>
  <p>Main Menu Component</p>
  <button>Close Menu</button>
</template>

We’ll implement the closing functionality in the lesson.

What are events

Earlier in the course we learned that we can communicate from a parent component to a child component with props . We also learned how to make data available down through a nested component tree with the Provide and Inject API's .

Vue uses events to communicate from a child component to a parent component.

How to emit an event from a child component to a parent

To emit an event from a child component up to its parent, we use a simple two-step process.

  1. Step 1: Define the event in the child component.
  2. Step 2: Emit the defined event from the child component.

We will use MainMenu as the child and the root App component as the parent.

Step 1: Define the event

We define an event in the child component by specifying it in an array in the emits option of the component we want to send the event from.

Syntax: define event
<script>
export default {
  emits: ['eventName']
}
</script>

As an example, we’ll define an event in MainMenu that will close it when a user clicks the button.

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <button>Close Menu</button>
  </div>
</template>

<script>
export default {
  emits: ['closeMenu']
}
</script>

Step 2: Emit the event

To emit the event, we use the special $emit instance method with the event name as its argument. The event is emitted when an event like a button click is fired.

Syntax: emit event
<template>
  <button @click="$emit('eventName')">Click</button>
</template>

To demonstrate, we’ll add the click event to our MainMenu ’s close button that will emit the event we defined in the emits array.

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <button @click="$emit('closeMenu')">Close Menu</button>
  </div>
</template>

<script>
export default {
  emits: ['closeMenu']
}
</script>

Now our event will be emitted to any components up in the hierarchy when the user clicks the button.

How to listen for emitted events on a parent component

Events aren’t auto-captured in a parent component. The parent has to explicitly listen for it.

To listen to an event, we bind the emitted event with event binding on the child component. As its value, we can specify any functionality that we want to execute when the event triggers.

Syntax: listen for event
<template>
  <component @eventName="functionality/function" />
</template>

To demonstrate, we’ll listen for the closeMenu event in our root App component on the MainMenu instance. To keep things simple, we’ll use an inline expression that closes the menu.

Example: src/App.vue
<template>
  <button @click="isMenuOpen = true">Open Menu</button>
  <main-menu v-show="isMenuOpen" @closeMenu="isMenuOpen = false" />
</template>

<script>
import MainMenu from './components/MainMenu.vue'

export default {
  components: { MainMenu },
  data() {
    return {
      isMenuOpen: false
    }
  }
}
</script>

If we run the example in the browser, the menu closes when we click the Close Menu button.

When we click the button, it fires the $emit and sends the closeMenu event to the root App component. The root App component is listening for the closeMenu event and once it receives it, executes the code that sets isMenuOpen to false.

How to send data with an event

We can send data from the child up to the parent by specifying that data as the second argument in $emit .

Syntax: send data with emit
<template>
  <button @click="$emit('eventName', data-to-send)">Click</button>
</template>

To receive the data in the parent component, we have to create a method. The method automatically receives the data as a parameter.

Syntax: receive data with method
<script>
export default {
  methods: {
    eventFunction(data) {
      // use data from emitted event
    }
  }
}
</script>

To demonstrate, we’ll send the name “John Doe” with the closeMenu event.

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <button @click="$emit('closeMenu', 'John Doe')">Close Menu</button>
  </div>
</template>

<script>
export default {
  emits: ['closeMenu']
}
</script>

In the root App component, we’ll move the logic that closes the menu to a closeMenu method. We’ll also capture the data that was emitted and store it in a data property that we can output in the template.

Example: src/App.vue
<template>
  <button @click="isMenuOpen = true">Open Menu</button>
  <main-menu v-show="isMenuOpen" @closeMenu="closeMenu" />
  <p>{{ name }}</p>
</template>

<script>
import MainMenu from './components/MainMenu.vue'

export default {
  components: { MainMenu },
  data() {
    return {
      name: '',
      isMenuOpen: false
    }
  },
  methods: {
    closeMenu(data) {
      this.isMenuOpen = false
      this.name = data
    }
  }
}
</script>

When we click on the Close Menu button in the browser, the event will emit, send the data and be stored in the name data property that shows underneath the button.

note This is not the only way to send data from a child to a parent. We can also use Slot Props, which we cover in the Slots lesson .

How to validate an emitted event

Similar to validating props passed from a parent component, it is possible to validate custom events that are emitted from the child component.

In the previous example we sent a hardcoded name through the emitted event when the user clicks on the Close Menu button.

Let’s adjust our example and add a text input field that takes the name value from the user with v-model .

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <p><input type="text" v-model="name"></p>

    <button @click="$emit('closeMenu', name)">Close Menu</button>
  </div>
</template>

<script>
export default {
  emits: ['closeMenu'],
  data() {
    return { name: '' }
  }
}
</script>

If we open the menu, enter a name and close the menu again, the name we entered will show below the button. So everything works as expected.

Now let’s add a validation when emitting this event.

We’ll start by changing the emits option from an array to an object. Vue expects the key to be the custom event name, and the value to be the validation function.

We’ll use the ES6 shorthand syntax and define the key as a function. This function receives the argument we specified when emitting the event, in this case the name data property.

If this function returns false, Vue will display a validation warning in the console. So in the function body we can do a simple check to see if the input, and thus the name , is empty.

Example: src/components/MainMenu.vue
<template>
  <div>
    <p>Main Menu Component</p>
    <p><input type="text" v-model="name"></p>

    <button @click="$emit('closeMenu', name)">Close Menu</button>
  </div>
</template>

<script>
export default {
  emits: {
    closeMenu(name) {
      if (!name)
        return false
      else
        return true
    }
  },
  data() {
    return { name: '' }
  }
}
</script>

If we go to the browser and close the menu without entering a name, Vue will raise the following warning in the console.

Output:
[Vue warn]: Invalid event arguments: event validation failed for event "closeMenu".

Even though the validation only provides a warning in the console, it’s useful when you’re working in a team and developing components that will be used by other developers.

Custom components and v-model

We’ll often create form components to be used throughout the application.

For example, we may want to style a text input component and use it throughout the application for all text inputs, like a search bar or inputs in a contact form.

But the problem is that the v-model directive doesn’t know how to behave with a custom component. So we will need to add some logic to ensure it works.

As an example, we’ll create a new component called TextInput that only contains a text field.

Example: src/components/TextInput.vue
<template>
  <input class="theme-input" type="text">
</template>

And we’ll import and use TextInput in the root App component.

Example: src/App.vue
<template>
  <p>Name: {{ name }}</p>
  <text-input v-model="name" />
</template>

<script>
import TextInput from './components/TextInput.vue'

export default {
  components: { TextInput },
  data() {
    return { name: '' }
  }
}
</script>

If we go to the browser and type a name into the field, it doesn’t show in the paragraph above. The value wasn’t stored in the data property.

It is our responsibility to define how the v-model directive behaves with a custom component.

When we use v-model on a custom component, it automatically receives a prop called modelValue .

We need to specify this prop with a type in the props option of that component’s config object. Then we need to bind the prop to the value attribute of the input field with the v-bind directive.

Example: src/components/TextInput.vue
<template>
  <input class="theme-input" type="text" :value="modelValue">
</template>

<script>
export default {
  props: {
    modelValue: String
  }
}
</script>

This takes care of the value from the field, but we still need to handle the input from the user.

The v-model directive will automatically listen for an event called update:modelValue . We have to emit that event with the input value to the parent.

So, we bind to the input event on the element and use $emit to emit the input to the parent.

For the first argument, we specify the update:modelValue event that Vue automatically gives us. As the second argument, we send the data that the user entered by accessing the Javascript event object with $event.target.value .

Example: src/components/TextInput.vue
<template>
  <input
    class="theme-input"
    type="text"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

<script>
export default {
  props: {
    modelValue: String
  }
}
</script>

If we go back to the browser and enter a name, the value will display in the paragraph, indicating that the value is being stored in the name data property.