Custom events
We're making great progress. We now have a component that can accept input, be registered globally or locally, has scoped styles, validation, and more. Now we need to give it the ability to fire events back to its parent component to communicate whenever theFancyButtonbutton is clicked, this is done by editing the code for the$emitevent:
<
template
>
<
button
@click.prevent="clicked"
>
{{buttonText}}
<
/button
>
<
/template
>
<
script
>
export default {
props: {
buttonText: {
type: String,
default: () =
>
{
return "Fancy Button!"
},
required: true,
validator: value =
>
value.length
>
3
}
},
methods: {
clicked() {
this.$emit('buttonClicked');
}
}
}
<
/script
>
In our example, we've attached theclickedfunction to the click event of the button, meaning that whenever it is selected we're emitting thebuttonClickedevent. We can then listen for this event within ourApp.vuefile, where we add our element to the DOM:
<
template
>
<
fancy-button
@buttonClicked="eventListener()"
button-text="Click
me!"
>
<
/fancy-button
>
<
/template
>
<
script
>
import FancyButton from './components/FancyButton.vue';
export default {
components: {
'fancy-button': FancyButton
},
methods: {
eventListener() {
console.log("The button was clicked from the child component!");
}
}
}
<
/script
>
<
style
>
<
/style
>
Notice how at this point we're using@buttonClicked="eventListener()". This uses thev-onevent to call theeventListener()function any time the event is emitted, subsequently logging the message to the console. We've now demonstrated the ability to send and receive events between two components.
Sending event values
To make the event system even more powerful, we can also pass values along to our other component. Let's add an input box to ourFancyButtoncomponent (perhaps we need to rename it or think about separating the input into its own component!):
<
template
>
<
div
>
<
input type="text" v-model="message"
>
<
button
@click.prevent="clicked()"
>
{{buttonText}}
<
/button
>
<
/div
>
<
/template
>
<
script
>
export default {
data() {
return {
message: ''
};
},
// Omitted
}
The next thing to do is pass along the message value with our$emitcall. We can do this inside of theclickedmethod like so:
methods: {
clicked() {
this.$emit('buttonClicked', this.message);
}
}
At this point, we can then capture the event as an argument to theeventListenerfunction like so:
<
template
>
<
fancy-button @buttonClicked="eventListener($event)" button-text="Click me!"
>
<
/fancy-button
>
<
/template
>
The final thing to do at this point is also match up the expected parameters for the function:
eventListener(message) {
console.log(`The button was clicked from the child component with this message: ${message}`);
}
We should then get the following in the console:
We've now got the ability to truly send events between a parent and child component, along with any data we may want to send along with it.
Event Bus
When we're looking to create an application wide events system (that is, without strictly parent to child component), we can create what's known as an Event Bus. This allows us to "pipe" all of our events through a singular Vue instance, essentially allowing for communication past just parent and child components. As well as this, it's useful for those not looking to use third-party libraries such asVuex, or smaller projects that are not handling many actions. Let's make a new playground project to demonstrate it:
# Create a new Vue project
$ vue init webpack-simple vue-event-bus
# Navigate to directory
$ cd vue-event-bus
# Install dependencies
$ npm install
# Run application
$ npm run dev
Start off by creating anEventsBus.jsinside thesrcfolder. From here we can export a new Vue instance that we can use to emit events like before with$emit:
import Vue from 'vue';
export default new Vue();
Next, we can create our two components,ShoppingInputandShoppingList. This will allow us to both input a new item as well as display a list of inputted items on our shopping list starting with ourShoppingInputcomponent:
<
template
>
<
div
>
<
input v-model="itemName"
>
<
button @click="addShoppingItem()"
>
Add Shopping Item
<
/button
>
<
/div
>
<
/template
>
<
script
>
import EventBus from '../EventBus';
export default {
data() {
return {
itemName: ''
}
},
methods: {
addShoppingItem() {
if(this.itemName.length
>
0) {
EventBus.$emit('addShoppingItem', this.itemName)
this.itemName = "";
}
}
},
}
<
/script
>
The key take away from this component is that we're now importingEventBusand using$emitinstead of using this, changing our application's event system from being component-based to application-based. We can then watch for changes (and the subsequent values) from any component we want using$on. Let's look at this with our next component,ShoppingList:
<
template
>
<
div
>
<
ul
>
<
li v-for="item in shoppingList" :key="item"
>
{{item}}
<
/li
>
<
/ul
>
<
/div
>
<
/template
>
<
script
>
import EventBus from '../EventBus';
export default {
props: ['shoppingList'],
created() {
EventBus.$on('addShoppingItem', (item) =
>
{
console.log(`There was an item added! ${item}`);
})
}
}
<
/script
>
Looking at ourShoppingListcomponent we can see the use of$on, this allows us to listen for the event namedaddShoppingItem(the same event name as we emitted, or any other event you're looking to listen for). This returns the item, which we're then able to log out to the console or do anything else at this point.
We can put this all together inside of ourApp.vue:
<
template
>
<
div
>
<
shopping-input/
>
<
shopping-list :shoppingList="shoppingList"/
>
<
/div
>
<
/template
>
<
script
>
import ShoppingInput from './components/ShoppingInput';
import ShoppingList from './components/ShoppingList';
import EventBus from './EventBus';
export default {
components: {
ShoppingInput,
ShoppingList
},
data() {
return {
shoppingList: []
}
},
created() {
EventBus.$on('addShoppingItem', (itemName) =
>
{
this.shoppingList.push(itemName);
})
},
}
We're defining both of our components, and listening for theaddShoppingItemevent inside of our created lifecycle hook. Just as before, we get theitemName, which we can then add to our array. We can pass the array through to another component as a prop, such as theShoppingListto be rendered on screen.
Finally, if we wanted to stop listening to events (either entirely or per event) we can use$off. Inside ofApp.vue, let's make a new button that shows this further:
<
button @click="stopListening()"
>
Stop listening
<
/button
>
Then we can create thestopListeningmethod like so:
methods: {
stopListening() {
EventBus.$off('addShoppingItem')
}
},
If we wanted to stop listening to all events, we could simply use:
EventBus.$off();
At this point, we've now created an events system that would allow us to communicate with any of our components regardless of the parent/child relationship. We're able to send events and listen to them via theEventBus, giving us a lot more flexibility with our component data.
Slots
When we're composing our components, we should consider how they'll be used by ourselves and our team. Using slots allows us to dynamically add elements to the component with varying behavior. Let's see this in action by making a new playground project:
# Create a new Vue project
$ vue init webpack-simple vue-slots
# Navigate to directory
$ cd vue-slots
# Install dependencies
$ npm install
# Run application
$ npm run dev
We can then go ahead and create a new component namedMessage(src/components/Message.vue). We can then add something specific to this component (such as the followingh1) as well as aslottag that we can use to inject content from elsewhere:
<
template
>
<
div
>
<
h1
>
I'm part of the Message component!
<
/h1
>
<
slot
>
<
/slot
>
<
/div
>
<
/template
>
<
script
>
export default {}
<
/script
>
If we then registered our component inside ofApp.vueand placed it inside of our template, we'd be able to add content inside of thecomponenttag like so:
<
template
>
<
div id="app"
>
<
message
>
<
h2
>
What are you doing today?
<
/h2
>
<
/message
>
<
message
>
<
h2
>
Learning about Slots in Vue.
<
/h2
>
<
/message
>
<
/div
>
<
/template
>
<
script
>
import Message from './components/Message';
export default {
components: {
Message
}
}
<
/script
>
At this point, everything inside themessagetag is being placed inside of theslotwithin ourMessagecomponent:
Notice how we're seeingI'm part of the Message component!with each declaration of theMessagecomponent, this shows that even though we're injecting content into this space, we can still show template information specific to the component each time.
Defaults
Whilst we're able to add content into the slots, we may want to add default content that shows when we don't add anything ourselves. This means we don't have to add content every time, and if we want to, we can override it in that circumstance.
How do we add default behavior to our slots? That's quite simple! All we need to do is add our element(s) in between theslottag like this:
<
template
>
<
div
>
<
h1
>
I'm part of the Message component!
<
/h1
>
<
slot
>
<
h2
>
I'm a default heading that appears
<
em
>
only
<
/em
>
when no slots
have been passed into this component
<
/h2
>
<
/slot
>
<
/div
>
<
/template
>
If we therefore add anothermessageelement, but this time without any markup inside, we'd get the following:
<
template
>
<
div id="app"
>
<
message
>
<
h2
>
What are you doing today?
<
/h2
>
<
/message
>
<
message
>
<
h2
>
Learning about Slots in Vue.
<
/h2
>
<
/message
>
<
message
>
<
/message
>
<
/div
>
<
/template
>
Now if we head to our browser we can see that our messages display as expected like so:
Named slots
We can also take this a step further with named slots. Let's say ourmessagecomponent wanted both adateandmessageTextinput, one of which is a slot and the other a property of the component. Our use case for this would be that perhaps we want to display the date differently, add varying bits of information, or not even show it at all.
Our message component becomes:
<
template
>
<
div
>
<
slot name="date"
>
<
/slot
>
<
h1
>
{{messageText}}
<
/h1
>
<
/div
>
<
/template
>
<
script
>
export default {
props: ['messageText']
}
<
/script
>
Take note of thename="date"attribute on ourslottag. This allows us to dynamically place our content at runtime in the correct locations. We can then build out a small chat system to show this in action, let's ensure we havemomentinstalled in our project prior to continuing:
$ npm install moment --save
You may remember usingmomentinChapter 4,Vue.js Directives, we'll also be reusing theDatepipe that we created earlier. Let's upgrade ourApp.vueto contain the following:
<
template
>
<
div id="app"
>
<
input type="text" v-model="message"
>
<
button @click="sendMessage()"
>
+
<
/button
>
<
message v-for="message in messageList" :message-text="message.text" :key="message"
>
<
h2 slot="date"
>
{{ message.date | date }}
<
/h2
>
<
/message
>
<
/div
>
<
/template
>
<
script
>
import moment from 'moment';
import Message from './components/Message';
const convertDateToString = value =
>
moment(String(value)).format('MM/DD/YYYY');
export default {
data() {
return {
message: '',
messageList: []
}
},
methods: {
sendMessage() {
if ( this.message.length
>
0 ) {
this.messageList.push({ date: new Date(), text: this.message });
this.message = ""
}
}
},
components: {
Message
},
filters: {
date: convertDateToString
}
}
<
/script
>
What's happening here? Inside of our template we're iterating over ourmessageListand creating a new message component each time a new message is added. Inside of the component tag we're expecting themessageTextto appear (as we're passing it as a prop and the markup is defined inside the Message component), but we're also dynamically adding the date using aslot:
What happens if we removeslot="date"from our h2? Does the date still show? Nope. This is because when we only use named slots, there are no other places for the slot to be added. It would only appear if we changed ourMessagecomponent to take in an unnamed slot like so:
<
template
>
<
div
>
<
slot name="date"
>
<
/slot
>
<
slot
>
<
/slot
>
<
h1
>
{{messageText}}
<
/h1
>
<
/div
>
<
/template
>
Summary
This chapter has given us the power to create reusable components that can communicate with one another. We've looked at how we can register components globally throughout the project, or locally to a specific instance, giving us flexibility and appropriate separation of concerns. We've seen just how powerful this can be with examples that range from the addition of simple properties to complex validations and defaults.
In the next chapter, we're going to be investigating how we can createbetter UI.We'll be looking more at directives such asv-modelin the context of forms, animations, and validation.