In this article, we are going to be chatting about making custom events in Vue. We will talk about basic events in Vue before moving on to making our own custom events. We will be working in a CodeSandbox and you can find the final code here.
Vue's component system is powerful and allows us to build any application type. We can create global components using
Vue.component('login-form', { ... })
or we can use Vue's Single File Components when we are building out more complex projects. Every application needs to listen for user interactions. Some examples of common events in the HTML DOM that we often listen for are:
Once we are done creating our Vue templates and our Vue styles, the next step in making our apps complete is to handle events.
Handling Basic Events in Vue
In Vue, we can listen for the HTML DOM events using the
directive. This adds an event listener to listen for the built-in DOM events and will look like this:v-on
<template> <button v-on:click="blastoff">Blast Off!</button> </template> <script> export default { methods: { blastoff(event) { alert('going to space!') } } } </script>
We are creating a button here that listens for the event click. We are using the
blastoff()
method to handle that event.“Vue's component system is powerful and allows us to build any application type!”
Tweet This
Shorthand for v-on
We can also use the v-on shorthand,
@
, to simplify our templates further.<button @click="blastoff">Blast Off!</button>
Handling DOM events in Vue is quick, thanks to the
v-on
directive. When we start building out our custom components, we may need to handle some more complicated events.How to Create a Custom Vue Event
We can create a custom event from our Vue components by using the $emit method. Here we have a button that emits a
sandwich-chosen
event with a value of grilled-cheese
:<button @click="$emit('sandwich-chosen', 'grilled cheese')"> Choose Grilled Cheese </button>
For more complex scenarios, we can pass in an object as the second parameter. This is useful if we need to provide our parent components with more information like a user object:
<button @click="$emit('user-chosen', { name: 'chris', username: 'chrisoncode' })"> Choose User </button>
That's the quick version of how to create a custom Vue event. Let's dive deeper and talk about:
- Creating custom Vue events
- Naming custom Vue events
- Emitting data through Vue events
- Syncing data between parent and child components
“Have you ever created a custom event in Vue?”
Tweet This
How Vue Handles Events
Before we go any further, we need to talk about Vue's methodology when it comes to components and custom events.
Vue takes an approach similar to the HTML DOM events. An element will "emit" an event and the data from that event.
Just like we can "listen" for when an element emits an event, we can create custom events that come from our components. For instance, we can have an HTML button that emits a
click
event. We can also have a custom component called <ColorPicker />
that emits a color-chosen
event:<!-- listening on a button for a click event --> <button @click="doSomething">Click Me</button> <!-- listening on a color-picker for a custom event called color-chosen --> <ColorPicker @color-chosen="doSomething" />
This is a robust way to handle custom events because we know that a child component has inputs (props) and outputs (custom events). We can reuse this same component in many places and always know that it has the same API.
Note: It's important to read through the Vue style guide when thinking about naming components, especially if we are switching between global components and single file components projects.
Creating Custom Vue Events
Let's expand on our
<ColorPicker />
example from above. We can create a Vue example where our main app background will change depending on if a user clicks one of three HTML colors: - peachpuff
- skyblue
- salmon
Let's start by creating our main
App.vue
component:<template> <div :style="{ background: backgroundColor }"> <ColorPicker /> </div> </template> <script> import ColorPicker from './ColorPicker'; export default { components: { ColorPicker }, data() { return { backgroundColor: 'powderblue' } } } </script>
In our main
App
component, we have created a main <div>
that has a style of background
bound to our backgroundColor
variable that starts as powderblue
.
Right now, this Vue application does nothing and will probably throw an error since we haven't defined our <ColorPicker />
component. Let's do that now in ColorPicker.vue
:<template> <div> <button>Peach Puff</button> <button>Sky Blue</button> <button>Salmon</button> </div> </template> <script> export default {}; </script>
The next step is for this component to emit a custom event. We'll call this event
color-chosen
. In ColorPicker.vue
, add:<template> <div> <button @click="$emit('color-chosen')">Peach Puff</button> <button @click="$emit('color-chosen')">Sky Blue</button> <button @click="$emit('color-chosen')">Salmon</button> </div> </template> <script> export default {}; </script>
Our component is now emitting an event! We can listen for this event from our parent component now. Back in
App.vue
, update as follows:<template> <div :style="{ background: backgroundColor }"> <ColorPicker @color-chosen="updateBackgroundColor" /> </div> </template> <script> import ColorPicker from "./ColorPicker"; export default { components: { ColorPicker }, data() { return { backgroundColor: "powderblue" }; }, methods: { updateBackgroundColor() { } } }; </script>
We've added an
updateBackgroundColor
method to listen to this component for that color-chosen
event. The next step is to get our child component to pass the color chosen to our parent component.Naming conventions for Vue custom events
Naming conventions from the Vue docs say to always name events with kebab-case. The reason for this is that when Vue compiles its HTML, HTML does not differentiate between upper or lowercase letters. That means that in our browsers' eyes, there is no difference between
colorChosen
and colorchosen
. For this reason, it's best always to use the color-chosen
version.Emitting Data Through Vue Events
To pass data through an emitted event, we can pass the data as the second parameter. In
ColorPicker.vue
: <template> <div> <button @click="$emit('color-chosen', 'peachpuff')">PeachPuff</button> <button @click="$emit('color-chosen', 'skyblue')">Sky Blue</button> <button @click="$emit('color-chosen', 'salmon')">Salmon</button> </div> </template>
Now we can listen for the color chosen from our parent component!
methods: { updateBackgroundColor(color) { this.backgroundColor = color; } } }
That will be all the code required to update our background color since we bound that color variable using the
:style
directive. Refactoring to be a cleaner component
Instead of calling
$emit
three times within our template, we can move that logic into our JavaScript to create a cleaner component. In ColorPicker.vue
:<template> <div> <button @click="chooseColor('peachpuff')">PeachPuff</button> <button @click="chooseColor('skyblue')">Sky Blue</button> <button @click="chooseColor('salmon')">Salmon</button> </div> </template> <script> export default { methods: { chooseColor(color) { this.$emit("color-chosen", color); } } }; </script>
When we use
$emit
from within our <script>
, we have to use this.$emit
. Passing Objects Through Event Data
Often, our components need to pass more information than a single string or number. We can pass objects and arrays through as data and access them in our parent component:
<template> <UserPicker @user-chosen="updateUser" /> </template> <script> import UserPicker from './UserPicker'; export default { components: { UserPicker }, methods: { updateUser(user) { console.log(user.name, user.username); } } } </script>
Then in our child component:
<template> <button @click="$emit('user-chosen', { name: 'chris', username: 'chrisoncode' }"> Choose Chris </button> <button @click="$emit('user-chosen', { name: 'kapehe', username: 'kapehe_ok' }"> Choose Kapehe </button> </template> <script> export default {} </script>
The Final Product!
All the code can be found in this CodeSandbox! I have added a little more CSS so that the color fills the page! Feel free to fork and play around with this code!
Aside: Authenticate a Vue App
Using Auth0, you can protect your applications so that only authenticated users can access them. Let's explore how to authenticate a Vue application.
If you would like to have a more in-depth explanation of protecting a Vue application, you can follow this fantastic article: Beginner Vue.js Tutorial with User Login.
Setting up Auth0
To begin, you will need an Auth0 account. You can sign up for a free Auth0 account here. Once you are logged in, follow these steps to set up an Auth0 application.
- Go to your Auth0 Dashboard and click the "+ CREATE APPLICATION" button.
- Name your new app and select "Single Page Web Applications". Hit "Create".
- In the Settings for your new Auth0 application, add
to the Allowed Callback URLs, Allowed Logout URLs, Allowed Web Origins. Hit "Save Changes" at the bottom of the page.http://localhost:8080
Vue application
You will need to install the Auth0
SDK. To do so, run the following command:auth0-spa-js
npm install @auth0/auth0-spa-js
Next, within your
src/
folder, create an auth
folder. Within the auth
folder, create a file named index.js
. You should now have a path that is src/auth/index.js
.Within that newly created file, paste in the following code:
// src/auth/index.js import Vue from 'vue'; import createAuth0Client from '@auth0/auth0-spa-js'; /** Define a default action to perform after authentication */ const DEFAULT_REDIRECT_CALLBACK = () => window.history.replaceState({}, document.title, window.location.pathname); let instance; /** Returns the current instance of the SDK */ export const getInstance = () => instance; /** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */ export const useAuth0 = ({ onRedirectCallback = DEFAULT_REDIRECT_CALLBACK, redirectUri = window.location.origin, ...options }) => { if (instance) return instance; // The 'instance' is simply a Vue object instance = new Vue({ data() { return { loading: true, isAuthenticated: false, user: {}, auth0Client: null, popupOpen: false, error: null, }; }, methods: { /** Authenticates the user using a popup window */ async loginWithPopup(o) { this.popupOpen = true; try { await this.auth0Client.loginWithPopup(o); } catch (e) { // eslint-disable-next-line console.error(e); } finally { this.popupOpen = false; } this.user = await this.auth0Client.getUser(); this.isAuthenticated = true; }, /** Handles the callback when logging in using a redirect */ async handleRedirectCallback() { this.loading = true; try { await this.auth0Client.handleRedirectCallback(); this.user = await this.auth0Client.getUser(); this.isAuthenticated = true; } catch (e) { this.error = e; } finally { this.loading = false; } }, /** Authenticates the user using the redirect method */ loginWithRedirect(o) { return this.auth0Client.loginWithRedirect(o); }, /** Returns all the claims present in the ID token */ getIdTokenClaims(o) { return this.auth0Client.getIdTokenClaims(o); }, /** Returns the access token. If the token is invalid or missing, a new one is retrieved */ getTokenSilently(o) { return this.auth0Client.getTokenSilently(o); }, /** Gets the access token using a popup window */ getTokenWithPopup(o) { return this.auth0Client.getTokenWithPopup(o); }, /** Logs the user out and removes their session on the authorization server */ logout(o) { return this.auth0Client.logout(o); }, }, /** Use this lifecycle method to instantiate the SDK client */ async created() { // Create a new instance of the SDK client using members of the given options object this.auth0Client = await createAuth0Client({ domain: options.domain, client_id: options.clientId, audience: options.audience, redirect_uri: redirectUri, }); try { // If the user is returning to the app after authentication... if ( window.location.search.includes('code=') && window.location.search.includes('state=') ) { // handle the redirect and retrieve tokens const { appState } = await this.auth0Client.handleRedirectCallback(); // Notify subscribers that the redirect callback has happened, passing the appState // (useful for retrieving any pre-authentication state) onRedirectCallback(appState); } } catch (e) { this.error = e; } finally { // Initialize our internal authentication state this.isAuthenticated = await this.auth0Client.isAuthenticated(); this.user = await this.auth0Client.getUser(); this.loading = false; } }, }); return instance; }; // Create a simple Vue plugin to expose the wrapper object throughout the application export const Auth0Plugin = { install(Vue, options) { Vue.prototype.$auth = useAuth0(options); }, };
The comments in this file go over what each section does. To find more details about this file, please visit this blog post section.
Connecting Auth0 and the Vue application
To connect your Auth0 app and your Vue app, you will need to bring over some data from your Auth0 app that you set up earlier. You will want those values protected. To do so, create a file named
auth_config.json
in the root of your Vue application. Then in the .gitignore
, you will want to put that newly created file in there.In that file, put the following values:
// auth_config.json { "domain": "your-domain.auth0.com", "clientId": "yourclientid" }
Back in your Auth0 dashboard, click on the Settings tab of your Auth0 application. You will find the values "Domain" and "Client ID". Copy and paste those values into this file.
Using authentication globally in Vue
To use this authentication globally within the Vue app, you need to update the
src/main.js
file. Delete everything in the file and replace with the following code:// src/main.js import Vue from 'vue'; import App from './App.vue'; import router from './router'; // Import the Auth0 configuration import { domain, clientId } from '../auth_config.json'; // Import the plugin here import { Auth0Plugin } from './auth'; // Install the authentication plugin here Vue.use(Auth0Plugin, { domain, clientId, onRedirectCallback: (appState) => { router.push( appState && appState.targetUrl ? appState.targetUrl : window.location.pathname, ); }, }); Vue.config.productionTip = false; new Vue({ router, render: (h) => h(App), }).$mount('#app');
Log in and log out buttons
In order to use all this, you will want to add "Log In" and "Log Out" buttons. To do that, wherever you would like your buttons to be in your application, add this code within the
<template>
section of that file:<div v-if="!$auth.loading"> <!-- show login when not authenticated --> <a v-if="!$auth.isAuthenticated" @click="login">Log in</a> <!-- show logout when authenticated --> <a v-if="$auth.isAuthenticated" @click="logout">Log out</a> </div>
In that same file within the
<script>
tag, add in these methods:<script> export default { name: 'App', methods: { // Log the user in login() { this.$auth.loginWithRedirect(); }, // Log the user out logout() { this.$auth.logout({ returnTo: window.location.origin, }); }, }, }; </script>
You now have the necessary code to authenticate your Vue.js application!
More resources
Conclusion
Vue events can make our components more versatile and reusable. By using
$emit
, we can pass data out of a component and up to a parent component. The parent component then has the ability to utilize that information; however it sees fit.This model lets us create components that are compartmentalized from the other parts of our app, a great value when creating complex applications.