Introduction to Vue (part 2 of 2)
From part 1, you have learned the basics of Vue.js around components. Now let’s take it a step further and finish building the application!
You can find the demo application here, and you can find the Github code here.
In part 2, you will learn:
- How to handle events and transmit data
- How to use computed property
- (Advanced) How to use Vuex to manage states of an application
use events to transmit data
Now that we know how to create a method that does some action, we want to implement our like, dislike, and skip methods. In this step let’s wire up those actions to influence our like count.
We will make like increment the count by 1, dislike decrement the count by 1 and skip will not do anything to the count. So our actions
component will cause an action that changes the value of likes
, and our header
component will have to know when likes
and dislikes
change and render that value.
Looking at the structure of the app, we see that actions
and header
are siblings, and children of the tvinder-app
component. We know that we can use props to pass data from parent to child, but how can we pass information from child to parent? Every component has an event interface. This allows the component to listen to an event, and trigger an event.
We saw the first of this interface in the previous step where we used v-on
to listen for a click event. Here we will want to do both, trigger an event for one of our three actions (i.e. like, dislike, skip), and listen for that event on the parent component to register that something has happened.
Let’s add some necessary HTML markup so that we can use click events to fire off our methods. We will use some icons for our likes
and dislikes
buttons. Here is the markup that will render those icons and also listen for click events that will trigger our three methods.
src/components/actions.js
...
<div class="actions">
<div class="controls">
<div class="icon-button" v-on:click="decrement()">
<svg class="icon icon-cross"><use xlink:href="#icon-cross"></use></svg>
</div>
<a id="skip" href="#" v-on:click.prevent="skip" class="icon-buttdon">Skip</a>
<div class="icon-button" v-on:click="increment()">
<svg class="icon icon-heart"><use xlink:href="#icon-heart"></use></svg>
</div>
</div>
</div>
...
Now that we have some markup to work with, let’s actually create our methods.
src/components/actions.js
...
methods: {
skip() {
const self = this
self.$emit('handleSkip')
}, increment() {
const self = this
self.$emit('handleLikes', 1)
}, decrement() {
const self = this
self.$emit('handleLikes', -1)
}
}
...
Here we are defining the three actions we want to perform. increment
and decrement
are equivalent to like
and dislike
. So on click of the HTML element, each does the corresponding action.
The only thing that they do is $emit
something. $emit()
is part of the components event interface. Inside the parentheses, we must specify an eventName
and an optional argument for any data we want to pass along. So increment()
will emit an event with the name of handleLikes
along with the value of 1
. These events move upwards in the component tree. So the parent component of actions
will be able to listen for an event with this name. 3. Now that we are emitting named events, we can listen for them on the parent component. We can capture these events that will trigger some action in the parent component.
src/components/app.js
...
<div>
<app-header></app-header>
<movies :image_url="image_url"></movies>
<actions @handleLikes="handleLikes" @handleSkip="handleSkip" </actions>
</div>
...
The way you listen for these events is with a directive on the HTML element. We declare this directive with an @
symbol and the event name we are listening for. So when we $emit('handleLikes')
from actions.js
@handleLikes
will respond to that event. The second part to this directive is the method to call. This is the ="handleLikes"
part.
All this mean is that the app fires off the handleLikes
method on the component as the response to whatever event we have captured. Next we need to implement the methods that will fire when the app receives those events.
app.js
...
methods: {
handleLikes(vote) {
const self = this
self.likes += vote
self.incrementImage()
}, handleDislikes(vote) {
const self = this
this.dislikes += vote
self.incrementImage()
} handleSkip() {
const self = this self.incrementImage()
}, incrementImage() {
// TODO
}
}
...
The last step is to add likes
and disikes
to our data function in app.js
so that we can update that value, and pass it as a prop to our header
component.
src/components/app.js
...
<app-header :likes="likes" :dislikes="dislikes"></app-header>
...
src/components/header.js
...
props: {
likes: {
type: Number,
required: true,
}, dislikes: {
type: Number,
required: true,
},
}
...
That’s it! Now we are updating our like count from our actions component.
use computed properties to render movie data
Here you will learn about computed properties! Now that we have most of the functionality built out, we will need to have some actual data to interact with. We have a json
file with a list of movie objects with a movie name and a url that points to a poster of that movie. Up till now our templates have been very simple.
We have some data or some prop with a name and a value, and we simply bind that data in the template. But what if you have some data that is not a simple key value pairing? For instance, what if you had someone's name, but in the template you wanted to display their name backwards for some odd reason.
...
<div>
{{ name.split('').reverse().join('') }}
</div>
...
That looks kind of ugly, and you can imagine there could be much more complex pieces of logic for information you would want to render in the template. This is where computed properties come in handy. They can all this complex logic and allow us to simply use the name of that property in the template.
...
<div>
{{ reversedName }}
</div>
... computed: {
reversedName() {
return this.name.split('').reverse().join('')
}
}
...
This cleans up our templates and allows us to render our data more efficiently! So much the same as methods, computed is a custom attribute Vue implements. Inside the computed attribute, we declare functions for all of our computed properties. Any time a piece of data is updated, its computed property will also be updated.
With this in mind, let’s think about how we will interact with our data. We have a list of movie objects, and each has a key value pair of name and image url. We have a movies
component that can render the name and image of one movie at a time. So, we should pass a movie object as a prop to our movies
component.
But how will we select which movie to display from the list? One way would be to just randomly select a movie from the movies list and pass that in as a prop. But what if order matters or we don't want to potentially see the same movie twice? We can keep track of an index in the list to select a movie from, and pass the movie at that index to our movies
component.
First let’s capture our movie-data and set up the initial index of that list we want to render.
src/components/app.js
...
data() {
return {
likes: 0,
dislikes: 0,
imageIndex: 0,
movieData: window.movieDataJson.posters
}
}
...
We have already set movieData on the window so that we can easily use this JSON. Now that we have some data to work with, let’s pass that movie object to our movies
component.
src/components/app.js
...
<div>
<app-header :likes="likes"></app-header>
<movies :movie="movie"></movies>
<actions @handleLikes="handleLikes" @handleSkip="handleSkip"></actions>
</div> ... computed: {
movie() {
const self = this
return self.movieData[self.imageIndex]
}
},
...
Here we are using a computed property to select which movie we will send as a prop to our movies
component. We could also accomplish this directly in the template with something like <movies :movie="movieData[imageIndex]"></movies>
, but using a computed property is a cleaner approach.
Now that we are passing the movie object, we need to update our movies
component to use this object. This means more computed properties! Instead of taking the image url directly from a prop, we are now getting it from our movie object.
src/components/movies.js
...
const html = `
<div class="movies">
<div class="movie-poster-container">
<img class="movie-poster" v-bind:src="extractImageUrl" v-bind:key="extractImageUrl">
<div class="movie-name">{{ extractImageName }}</div>
</div>
</div>
`
...
Vue.component("movies", {
template: html,
props: {
movie: {
type: Object,
required: true,
}
},
...
computed: {
extractImageUrl() {
const self = this return
self.movie.url
}, extractImageName() {
const self = this
return self.movie.name
}
}, )
...
Here we update our props to take in a movie object, and we define two computed properties to extract the information we want to bind to the template.
The last step is to implement our incrementImage()
method on our app
component so that when we like or dislike a movie, we cycle to the next movie.
src/components/app.js
...
incrementImage() {
const self = this self.imageIndex += 1 if(self.imageIndex > (self.movieData.length-1)) {
self.imageIndex = 0 }
}
...
set up Vuex
In this step we will introduce Vuex state management system for our app. For a small app like this, Vuex might not be necessary, but it is a good starting point to learn the core concepts of Vuex.
So what is Vuex? It is a centralized store for every component in the app. It ensures that its state can only be mutated in a predictable way. Why would we want to use Vuex? There can be many answers to this question — but two good cases for implementing Vuex in your app are when:
- multiple components depend on the same piece of state
- actions from different components have to mutate the same piece of state.
At it’s basic level a Vuex Store looks like this:
const Store = new Vuex.Store({
modules: { ... },
state: { ... },
getters: { ... },
mutations: { ... },
actions: { ... },
})
There are 5 core concepts to Vuex. They are state, actions, mutations, getters, and modules.
Let’s start by set up Vuex store, and we know that we want to keep track of like
and dislike
, also specifically what movies are being liked or disliked. At the bottom we add window.store = Store so that the application has the store object.
src/store/index.js
((() => { var Vuex = window.Vuex const Store = new Vuex.Store({
state: {
likes: 0,
disLikes: 0,
likedMovies: [],
disLikedMovies: []
},
)} window.store = Store}))()
Next we can define mutations that can change the states in the store. What is mutation? According to Vuex documentation:
The only way to actually change state in a Vuex store is by committing a mutation. Vuex mutations are very similar to events: each mutation has a string type and a handler. The handler function is where we perform actual state modifications, and it will receive the state as the first argument.
Let’s add mutation methods for each state:
src/store/index.js
((() => { var Vuex = window.Vuex const Store = new Vuex.Store({
state: {
likes: 0,
disLikes: 0,
likedMovies: [],
disLikedMovies: []
}, mutations: {
addToLikeCount(state, like) {
state.likes += like
}, addToDislikeCount(state, disLike) {
state.disLikes += disLike
}, addToDislikeArray(state, disLikedMovie) {
state.disLikedMovies.push(disLikedMovie)
}, addToLikeArray(state, likedMovie) {
state.likedMovies.push(likedMovie)
}
},
)} window.store = Store}))()
Now that we have mutations to change states within the store, we now build actions for other components to use. What is action? According to Vuex documentation:
Actions are similar to mutations, the differences being that:
1. Instead of mutating the state, actions commit mutations.
2. Actions can contain arbitrary asynchronous operations.
Even though this project doesn’t use all the features actions can provide, such as asynchronous operations, having actions being called outside the store and keep mutations private within store is a good practice.
src/store/index.js
((() => {var Vuex = window.Vuexconst Store = new Vuex.Store({
state: {...},
mutations: {...}, actions: {
updateLikeCount(context, like) {
context.commit("addToLikeCount", like)
}, updateDisLikeCount(context, dislike) {
context.commit("addToDislikeCount", dislike)
}, updateDisLikedMovies(context, disLikedMovie) {
context.commit("addToDislikeArray", disLikedMovie)
}, updateLikedMovies(context, likedMovie) {
context.commit("addToLikeArray", likedMovie)
}
},
)} window.store = Store}))()
wire up Vuex
Now that we have Vuex setup, we can use it in our app! We have app.js as the source of truth for data rendered in the rest of views, so we can modify getters and setters within app.js
to talk to our Vuex store!
Let’s start with getters, we will get data from our store using computed property.
src/components/app.js
...
<div>
<app-header :likes="likes"></app-header>
<movies :movie="movie"></movies>
<actions @handleLikes="handleLikes" @handleSkip="handleSkip"></actions>
</div>...computed: {
movie() {
const self = this
return self.movieData[self.imageIndex]
}, likes() {
const self = this
return self.$store.state.likes
}, disLikes() {
const self = this
return self.$store.state.disLikes
}},
...
next, we add setters to handle “like”, “dislike” or “skip” actions by using Vuex store. Whenever we want to trigger an action, we need to use store.dispatch method.
src/components/app.js
...computed: {},methods: {
handleLikes(vote) {
const self = this
self.$store.dispatch("updateLikeCount", vote)
self.$store.dispatch("updateLikedMovies", self.movie)
self.incrementImage()
}, handleDisLikes(vote) {
const self = this
self.$store.dispatch("updateDisLikeCount", vote)
self.$store.dispatch("updateDisLikedMovies", self.movie)
self.incrementImage()
}, handleSkip() {
const self = this
self.incrementImage()
},
}
...
toggle modals using conditionals
What if when we click on “votes” tab, we want to see a modal that shows details information of what we like or dislike? What if when we are done viewing that information, we want to close the modal and go back to where we were? This is where we can use v-show
conditionals for toggling.
First we need to create a component lists.js
to capture the modal, and we get likedMovies
and dislikedMovies
from Vuex store. In addition, we use v-for
to loop through the data collection.
src/components/lists.js
((() => {
const html = `
<div class="modal">
<div class="modal-content">
<span class="close">×</span>
<p>Likes</p>
<div v-for="movie in likedMovies" id="likes">
{{ movie.name }}
</div>
<p>Dislikes</p>
<div v-for="movie in disLikedMovies" id="dislikes">
{{ movie.name }}
</div>
</div>
</div>
`
Vue.component("lists", {
template: html, computed: {
likedMovies() {
const self = this
return self.$store.state.likedMovies
}, disLikedMovies() {
const self = this
return self.$store.state.disLikedMovies
}
},
}
})}))()
Next we use v-show to toggle whether the modal should display or not. Essentially whenever v-show
has a value true, Vue appends display:none CSS on the targeted element.
Who decides when to show the modal? We delegate that responsibility to the parent component v-show
, and we pass a boolean prop showlists
down to the child component. This pattern is called “data down”.
We first add the prop to the child component:
src/components/lists.js
((() => {
const html = `
<div class="modal" v-show="showLists">
<div class="modal-content">
<span class="close">×</span>
<p>Likes</p>
<div v-for="movie in likedMovies" id="likes">
{{ movie.name }}
</div>
<p>Dislikes</p>
<div v-for="movie in disLikedMovies" id="dislikes">
{{ movie.name }}
</div>
</div>
</div>
`
Vue.component("lists", {
template: html, props: {
showLists: {
type: Boolean,
required: true,
}
}, computed: {
...
}, methods: {
closeModal() {
const self = this self.$emit("closeModal")
}
}
})}))()
Now what happens if we click the close button on lists.js
? Rather than having the child component directly change data, it calls$emit
function to broadcast an event to its parent component, then the parent component knows what to do upon receiving that event. This pattern is called “event up”.
src/components/lists.js
((() => {
const html = `
<div class="modal" v-show="showLists">
<div class="modal-content">
<span class="close" v-on:click="closeModal()">×</span>
<p>Likes</p>
<div v-for="movie in likedMovies" id="likes">
{{ movie.name }}
</div>
<p>Dislikes</p>
<div v-for="movie in disLikedMovies" id="dislikes">
{{ movie.name }}
</div>
</div>
</div>
`
Vue.component("lists", {
template: html, props: {...}, computed: {...}, methods: {
closeModal() {
const self = this self.$emit("closeModal")
}
}
})}))()
src/components/app.js
((() => {
const html = `
<div>
...
<lists :showLists="showLists" @closeModal="toggleShowLists"></lists>
</div>
`Vue.component("tvinder-app", {
template: html,
data(){ return {
...
showLists: false,
}
}, computed: {...}, methods: {
... toggleShowLists() {
const self = this self.showLists = !self.showLists
}
}
})
}))()
Conclusion
That’s it! You’ve now successfully built a web application using Vue, congratulations! To recap, in this tutorial we have covered:
- Core functionalities of Vue components
- State management using Vuex
- How to handle communication across different Vue components
You can find part 1 of this tutorial here, the demo of the app is here, and the Github code is here.
Let’s take baby steps to learn tech!