May 12, 2019
The Vue.js documentation does a great job of explaining custom events in this section. This article further clarifies the role of this
in Vue's pub/sub ("publish" / "subscriber") mechanism.
Events are an important part of application design. They decouple the app components so that that details and dependencies from one component don't leak out into another, causing side effects. Additionally, events support asynchronous programming which is important to keep the app responsive.
In Vue.js, custom events are published using the $emit function of a component, including the root app instance. Components receive events by subscribing to them. Often, this registration is done in a created()
lifecycle hook using the $on function.
This screenshot shows the demo program. The screen is divided into two sections: Local Event Component and Global Event Component.
The "Local Event Component" section is a component posting an event to itself. That is, the event scope is limited to the issuing component. When the user enters text in a textfield and presses the Post Event button, an event is posted and received by the same component. Upon receiving the event, the component appends its payload to an array data field. A list item is bound to this with a v-for and the UI updates. In the preceding screenshot, several my-local-events have been posted and received. These have been displayed in the <ul> under "Local Events Emitted". They are also recorded in the Vue devtool. Notice the "$emit by <MyLocalEventComponent>.
This is the source code for LocalEventComponent. The text input is bound to a data field "mydata". When the Post Event button is clicked, the postEvent() function is called. postEvent uses this
which is the LocalEventComponent. That means "my-local-event" will only be captured by a handler also on the this
for LocalEventComponent. LocalEventComponent's created() lifecycle hook receives for the event and provides an arrow function to add to the emittedEvents array. Through binding, the items are added to the unordered list.
const LocalEventComponent = {
template: `
<div class="col-sm">
<h2>Local Event Component</h2>
<input type="text" v-model="mydata">
<button @click="postEvent">Post Event</button>
<hr />
<h3>Local Events Emitted</h3>
<ul>
<li v-for="event in emittedEvents">{{ event }}</li>
</ul>
</div>
`,
data() {
return {
mydata: null,
emittedEvents: []
}
},
methods: {
postEvent() {
this.$emit("my-local-event", this.mydata);
}
},
created() {
this.$on("my-local-event", data => this.emittedEvents.push(data));
}
};
"Local" in this example, is referring to the this
pointer of a component instance.
While there isn't much value in posting an event within a component -- direct function calls would clearer -- this "local event" technique will be used in parent / child communication presented later.
Global Events are those events posted to a globally-accessible object. Often, this is a dedicated object but can also be the main Vue.js app instance itself. With all components knowing about these global objects, any publisher can $emit events to the object and any receiver can $on the events.
I've seen a few cases where this.$on
is mistakenly used for this.$root.$on
. Check the Vue devtools to make sure the publisher and subscriber match up.
This screenshot shows a similar demo to the Local Events example. The internal structure is very different however. A component "GlobalEventPoster", emits an event to the app instance or root. A component "GlobalEventReceiver", registers for the event with the app instance. A third component, "GlobalEventComponent" is a container for the GlobalEventPoster and receiver components.
Notice that the event emitted is from the Root component. The communication between the poster component and the receiver component is brokered through this widely known component. The syntax used to access the Root component is $root
.
One variation of this global technique you might find is an EventBus. This is a secondary app instance that is also globally known, but the event processing can be factored out of the main instance. This makes a more cohesive design as the EventBus can contain all of the dispatching and dependencies.
In this listing, you'll see the three components that make up the Global Event Components demo.
const GlobalEventPoster = {
template: `
<div>
<input type="text" v-model="mydata">
<button @click="postEvent">Post Event</button>
</div>`,
data() {
return {
mydata: null
}
},
methods: {
postEvent() {
this.$root.$emit("my-global-event", this.mydata);
}
}
};
const GlobalEventReceiver = {
template: `
<div>
<h3>Global Events Received</h3>
<ul>
<li v-for="event in emittedEvents">{{ event }}</li>
</ul>
</div>`,
data() {
return {
emittedEvents: []
}
},
created() {
this.$root.$on("my-global-event", data => this.emittedEvents.push(data));
}
};
const GlobalEventComponent = {
template: `
<div class="col-sm">
<h2>Global Event Components</h2>
<my-global-event-poster />
<hr />
<my-global-event-receiver />
</div>
`,
components: {
"my-global-event-poster": GlobalEventPoster,
"my-global-event-receiver": GlobalEventReceiver
}
};
The app instance for both parts of the demo follows.
const app = new Vue({
el: "#app",
template: `
<div class="container-fluid">
<div class="row">
<div class="col-sm">
<h1>Events Demo</h1>
</div>
</div>
<div class="row">
<my-local-event-component />
<my-global-event-component />
</div>
</div>
`,
components: {
"my-local-event-component": LocalEventComponent,
"my-global-event-component": GlobalEventComponent
}
});
The following UML recaps the program structure. If you're not familiar with UML, the line ending in the diamond is composition as in "GlobalEventComponent" is composed of a "GlobalPosterComponent" and a "GlobalReceiverComponent". Each component is modeled as a UML class. The class shows the data fields as UML attributes and the methods as UML operations. GlobalPosterComponent has a "mydata" data field and a method "postEvent". GlobalReceiverComponent has a emittedEntries array and overrides the lifecycle hook "created".
Posting an event to oneself -- rather than making a simple call -- isn't a great benefit. However, posting non-global, instance events is very important in the Vue.js framework. Unlike the double-binding of v-model
, you should only use one-way binding when nesting components. The "one-way" is props. You may have though a child component that needs to share data back with the caller. This is accomplished by emitting a child event that is associated with a parent callback handler.
This screenshot shows a new demo program. The structure of this, presented in UML later, is an app instance including a parent component ("ParentComponent") which itself is partially composed of a child component ("ChildComponent"). The program is functionally similar; an event is fired that adds to a list when caught. Notice that it's the ChildComponent which is emitting the event rather than the root app instance object.
The ChildComponent responds to a button click. An event "my-child-event" is posted.
const ChildComponent = {
template: `
<div class="col-sm">
<h2>Child Component</h2>
<input type="text" v-model="mydata">
<button @click="postEvent">Post Event</button>
</div>
`,
data() {
return {
mydata: null,
emittedEvents: []
}
},
methods: {
postEvent() {
this.$emit("my-child-event", this.mydata);
}
}
};
The ParentComponent associates a parent function, addEventToList(), with my-child-event.
const ParentComponent = {
template: `
<div>
<child-component @my-child-event="addEventToList" />
<hr />
<h3>Events Received</h3>
<ul>
<li v-for="event in emittedEvents">{{ event }}</li>
</ul>
</div>`,
data() {
return {
emittedEvents: []
}
},
components: {
"child-component": ChildComponent
},
methods: {
addEventToList(e) {
this.emittedEvents.push(e);
}
}
};
This is the code listing for the app instance associated with the Parent / Child.
const app = new Vue({
el: "#app",
template: `
<div class="container-fluid">
<div class="row">
<div class="col-sm">
<h1>Local Events Revisitied</h1>
<my-parent-component />
</div>
</div>
</div>
`,
components: {
"my-parent-component": ParentComponent
}
});
This UML diagram shows the new app instance, parent component, and child component.
Custom Events are an important part of many architectures, not just Vue.js or even web programming. They reduce side effects by hiding the details of the component from callers who might misuse the component. Consider a component that is a form. That component can internally process the form fields and post a final "save-event" in the limited contact it has with the outside of the world.
By Carl Walker
President and Principal Consultant of Bekwam, Inc