Video
Материалы
- Lesson 1
- Lesson 2
- Lesson 3
- Lesson 4
- Lesson 5
- Lesson 6
- Lesson 7
- Lesson 8
- Lesson 9
- Lesson 10
- Lesson 11
The Vue Instance
Throughout this course you will learn the fundamentals of Vue while we build this product page together.
Prerequisites:
This course assumes a foundational knowledge in HTML, CSS and JavaScript.
Our Goal
In this lesson, we’ll show you how to use Vue to display data onto a webpage.
Our Starting Code
We’re going to start with some very simple HTML and JS code, which looks like this:
Problem
We need a way to take the variable product
from our JavaScript and have it show up in the h1
of our HTML.
Our first step is to include Vue in our project, which we’ll do by adding this line at the bottom of our index.html
file.
Next in our main.js we’ll write the following:
And then in our index.html we’ll use our first JavaScript expression:
When we save this, we’ll see “Socks” appear on our webpage.
It worked. Data from our JavaScript is showing up in our HTML. But what did we just do? Let’s break it down:
The Vue Instance
A Vue instance is the root of our application. It is created by passing an options object into it. Just like it sounds, this object has a variety of optional properties that give the instance the ability to store data and perform actions.
Plugging in to an Element
The Vue instance is then plugged into an element of your choosing, forming a relationship between the instance and that portion of the DOM. In other words, we’re activating Vue on the div with the id of app
by setting '``#app``'
as the element ( el
) that our instance is plugged into.
Putting our data in its place
A Vue instance has a place for data, in its data property.
The instance’s data can be accessed from inside the element that the instance is plugged into.
Using an Expression
If we want our product
to appear in our h1
, we can put product
inside these double curly braces.
See? It works. Simple huh?
How does it work? Inside the double curly braces, we’re using a JavaScript expression.
Important Term: Expression
Expressions allow us to utilize existing data values, together with logic, to produce new data values.
When Vue sees the expression {{ product }}
, it recognizes that we are referencing the associated Vue instance’s data, and it replaces that expression with the value of product instead, in this case: “Socks”.
Some other ways expressions can be used:
Introducing Reactivity
The reason Vue is able to display product
‘s value immediately is because Vue is reactive . In other words, the instance’s data is linked to every place that data is being referenced. So not only can Vue make its data appear within the HTML that references it, but that HTML will be updated to display new values any time that referenced data changes.
To prove that, let’s open the console and change the value of our product string. Look what happens.
See how easy that was?
What have we learned?
- How to begin writing a Vue application with a Vue instance, and how to load basic data onto the webpage.
- The Vue instance is the root of every Vue application
- The Vue instance plugs into an element in the DOM
- The Vue instance’s data can be displayed using the mustache-like syntax
{{ }}
called an expression. - Vue is reactive
Learn by doing
Challenge
Add a description
key to our existing data object with the value “A pair of warm, fuzzy socks”. Then display description
using an expression in an p
element, underneath the h1
.
Attribute Binding
In this lesson, we’ll explore ways you can connect data to the attributes of your HTML elements.
Our Goal
We’ll use attribute-binding in order to display an image and attach alt text to it based on values from our instance’s data.
Starting Code
Currently our HTML looks like this:
We’ve created a div
for the product image and the product info, in order to style them with Flexbox.
And our JavaScript looks like this:
Notice we’ve added an image
source to our data.
Problem
We want an image to show up on our page, but we need it to be dynamic. We want to be able to update that image in our data and have the image automatically update on the page. Since our src
attribute is what pulls the image into this element, we’ll need data to be bound to src
so that we can dynamically display an image based on the data at that time.
Important Term: Data Binding
When we talk about data binding in Vue, we mean that the place where it is used or displayed in the template is directly linked, or bound to the source of the data, which is the data object inside the Vue instance.
In other words, the host of the data is linked to the target of the data. In this case, our data is hosted by the data property of our Vue instance. And we want to target that data from our src
.
Solution
To bind the value of image
in our data object to the src
in our img
tag, we’ll use Vue’s v-bind
directive.
This evaluates to:
Voila! Our image appears. If the value of image
were to change, the src
will update to match, and the new image will appear.
Again, this happens because the data that lives in image
is bound to our src
attribute.
Additional Usages
We can use v-bind
again here if we want to bind alt text data to this same img
element.
If we add this in our data:
We can bind that to the alt
attribute like so:
In both of these cases, we’ve used the syntax v-bind
and after the colon :
, we’ve stated which attribute we’re binding the data to, src
and alt
in this case.
Now whenever the image
and altText
data changes, that updated data will remain linked to the src
and alt
attributes.
This is a very commonly used feature in Vue. Because it’s so common, there’s a shorthand for v-bind
, and it’s just a colon :
Simple, clean, and handy.
So what have we learned?
- Data can be bound to HTML attributes.
- Syntax is
v-bind:
or:
for short. - The attribute name that comes after the
:
specifies the attribute we’re binding data to. - Inside the attribute’s quotes, we reference the data we’re binding to.
Learn by doing
Challenge:
Add a link to your data object, and use v-bind
to sync it up with an anchor tag in your HTML. Hint: you’ll be binding to the href
attribute.
Conditional Rendering
In this lesson we’ll be uncovering how to conditionally display elements with Vue.
Our Goal
We want to display text that says if our product is in stock or not, based on our data.
Starting Code
Notice we’ve added a new data property there at the bottom: inStock
.
Problem
Often in a web application, we want elements to appear on the page depending on if a condition is met or not. For instance, if our product is not in stock, our page should display the fact that it’s out of stock.
So how could we conditionally render these elements, depending on whether our product is in stock or not?
Solution
Vue’s solution is simple and straightforward.
Given that our data contains this new property:
We can use the v-if
and v-else
directives to determine which element to render.
If inStock
is truthy, the first paragraph will render. Otherwise, the second paragraph will. In this case, since the value of inStock
is true, the first paragraph will render.
Great. We’ve used conditional rendering to display whether our product is in stock or not. Our feature is done. But let’s explore conditional rendering some more before we move onto our next topic.
Additional Conditional Syntax: v-else-if
We can add a third degree of logic with v-else-if
. To demonstrate, let’s use an example that is a bit more complex.
If our data looked something like this:
We could use expressions, inside the quotes, to make our conditions more specific.
The element that will render is the first element whose expression evaluates to true.
Additional Conditional Syntax: v-show
If your app needs an element to frequently toggle on and off the page, you’ll want to use the v-show
directive. An element with this directive on it will always be present in our DOM, but it will only be visible on the page if its condition is met. It will conditionally add or remove the CSS property display: none
to the element.
This method is more performant than inserting and removing an element over and over with v-if
/ v-else
.
However, in the product app we’re building, using a v-if
and v-else
works just fine, so we’ll keep that as our solution.
What’d we learn
- There are Vue directives to conditionally render elements:
- v-if
- v-else-if
- v-else
- v-show
- If whatever is inside the directive’s quotes is truthy, the element will display.
- You can use expressions inside the directive’s quotes.
- V-show only toggles visibility, it does not insert or remove the element from the DOM.
Learn by doing
Challenge:
Add an onSale
property to the product’s data that is used to conditionally render a span
that says “On Sale!”
List Rendering
In this lesson, we’ll learn how to display lists onto our webpages with Vue.
Our Goal
We want to be able to display a list of our product’s details.
Starting Code
Our project’s code currently looks like this:
What’s new here is our array of details
at the bottom.
Problem
We want our page to display our product’s details
. How can we iterate through this array to display its data?
Solution
Another Vue directive to the rescue. The v-for
directive allows us to loop over an array and render data from within it.
Now we have our details showing up in a list. But how is this working?
The syntax within the quotes of the v-for
directive may look familiar if you have used JavaScript’s for of
or for in
before. The v-for
works like this:
We use a singular noun ( detail
) as an alias for the string in the array we are iterating over. We then say in
and name the collection ( details
) that we are looping through. Inside the double curly braces, we specify what data to be displayed there( {{ detail }}
).
Since our v-for is inside an <li>
, Vue will print out a new <li>
for each detail in our details
array. If our v-for
was inside a <div>
, then a <div>
would have been printed out for each array item along with its content.
You can envision v-for
like an assembly line, where a mechanical arm that takes an element from the collection one at a time in order to construct your list.
Let’s take a look at a more complex example, displaying an object’s data in a div.
Iterating Over Objects
Problem
The product page we’re building needs to be able to show different versions of the same product, based on an array in our data of variants
. How would we iterate through this array of objects to display its data?
Let’s display the color of each variant. We’ll write:
In this case, we just want to display the color from the variant object, so we’re using dot notation to do so. If we wrote {{ variant }}
we’d display the entire object.
Note that it is recommended to use a special key attribute when rendering elements like this so that Vue can keep track of each node’s identity. We’ll add that in now, using our variant’s unique variantId
property.
What’d we learn
- The
v-for
directive allows us to iterate over an array to display data. - We use an alias for the element in the array being iterated on, and specify the name of the array we are looping through. Ex:
v-for="item in items"
- We can loop over an array of objects and use dot notation to display values from the objects.
- When using
v-for
it is recommended to give each rendered element its own unique key.
Learn by doing
Challenge:
Add an array of sizes
to the data and use v-for
to display them in a list.
Event Handling
In this lesson we’ll be learning how to listen for DOM events that we can use to trigger methods.
Goal
We want to have a button that increments the number of items in our cart.
Starting Code
Problem
We need a button to listen for click events on it, then trigger a method when that click happens, in order to increment our cart total.
First, we’ll add a new data property for our cart
.
In our HTML, we’ll create a div
for our cart. We’ll add a p
inside it to display our cart
data’s value.
We’ll also make a button
to add items to our cart
.
As you can see, we’re using Vue’s v-on
directive to increment the value of cart
This works. But how is it working?
Let’s dissect this syntax. We say v-on
, which let’s Vue know we’re listening for events on this button, and after the :
we specify the kind of event we are listening for, in this case: a click. Inside the quotes, we’re using an expression that adds 1 to the value of cart
every time the button is clicked.
This is simple, but not entirely realistic. Rather than using the expression cart += 1
, let’s make the click trigger a method that increments the value of cart
instead, like this:
As you can see, addToCart
is the name of a method that will fire when that click event happens. We haven’t yet defined that method, so let’s do that now, right on our instance.
Just like it does for its data, the Vue instance has an optional property for methods. So we’ll write out our addToCart
method within that option.
Now, when we click our button
, our addToCart
method is triggered, which increments the value of cart
, which is being displayed in our p
tag.
Let’s break this down further.
Our button
is listening for click events with the v-on
directive, which triggers the addToCart
method. That method lives within the methods property of the Vue instance as an anonymous function. The body of that function adds 1 to the value of this.cart
. Because this
refers to the data of the instance we’re currently in, our function is adding 1 to the value of cart
, because this.cart
is the cart
inside our data property.
If we just said cart += 1
here, we’d get an error letting us know that “cart is not defined”, so we use this.cart
to refer to the cart
from this
instance’s data.
You might be thinking, “But wait. We’re only incrementing the number of items in the cart, we’re not actually adding a product to the cart.” And you’re right. We’ll build that out in a future lesson.
Now that we’ve learned the basics of event handling in Vue, let’s look at a more complex example.
First, let’s add a variantImage
to each of our variants.
Now each variant has an image with green and blue socks, respectively.
Problem
We want to be able to hover our mouse over a variant’s color and have its variantImage
show up where our product image currently is.
Solution
We’ll use the v-on
directive again, but this time we’ll use its shorthand @
and listen for a mouseover
event.
Notice that we’re passing variant.variantImage
in as an argument to our updateProduct
method.
Let’s build out that method.
This is very similar to what we did to increment the value of cart
earlier.
But here, we are updating the value of image
, and its updated value is now the variantImage
from the variant that was just hovered on. We passed that variant’s image into the updateProduct
function from the event handler itself:
In other words, the updateProduct
method is ready to fire, with a parameter of variantImage
.
When it’s called, variant.variantImage
is passed in as variantImage
and is used to update the value of this.image
. As we just learned, this.image
is image
. So the value of image
is now dynamically updating based on the variant that was hovered on.
ES6 Syntax
Instead of writing out an anonymous function like updateProduct: function(variantImage)
, we can use the ES6 shorthand and just say updateProduct(variantImage)
. These are equivalent ways of saying the same thing.
What’d we Learn
- The
v-on
directive is used to allow elements to listen for events - The shorthand for
v-on
is@
- You can specify the type of event to listen for:
- click
- mouseover
- any other DOM event
- The
v-on
directive can trigger a method - Triggered methods can take in arguments
-
this
refers to the current Vue instance’s data as well as other methods declared inside the instance
Learn by doing
Challenge:
Create a new button and method to decrement the value of cart
.
Class & Style Binding
In this lesson we’ll be learning how to dynamically style our HTML by binding data to an element’s style attribute, as well as its class.
Goal
Our first goal in this lesson is to use our variant colors to style the background-color
of divs. Since our variant colors are “green” and “blue”, we want a div with a green background-color
and a div with a blue background-color
.
Starting Code
Problem
In the previous lesson, we created an event handler that updates the product’s image based on which p
tag was hovered on. Instead of printing out the variant’s color into a p
tag, we want to use that color to set the style of a div’s background-color
. That way, instead of hovering over text in a p
tag, we can hover over colored squares, which would update the product’s image to match the color that was hovered on.
Solution
First, let’s add a class of color-box
to our div
, which gives it a width, height and margin. Since we’re still printing out “green” and “blue” onto the page, we can make use of those variant color strings and bind them to our style attribute, like so:
We are using an inline style to dynamically set the background-color
of our divs, based on our variant colors ( variant.variantColor
).
Now that our div
s are being styled by the variantColor
, we no longer need to print them out. So we can delete the p
tag and move its @mouseover
into the div
itself.
Now when we hover over the blue box and the blue socks appear, hover over the green box and the green socks appear. Pretty neat!
Now that we’ve learned how to do style binding, let’s explore class binding.
Problem
Currently, we have this in our data:
When this boolean is false
, we shouldn’t allow users to click the “Add to Cart” button, since there is no product in stock to add to the cart. Fortunately, there’s a built-in HTML attribute, disabled
, which will disable the button.
As we learned in our second lesson in this series, we can use attribute binding to add the disabled
attribute whenever inStock
is false, or rather not true : !inStock
.
Now our button is disabled whenever inStock
is false
. But that doesn’t change the appearance ****of the button. In other words, the button still appears clickable, even though it’s not.
Solution
In a similar way to how we just bound inStock
to the button’s disabled
attribute, we can bind a disabledButton
class to our button whenever inStock
is false. That way, our button will also appear disabled.
It works! The button is now grayed out when inStock = false
.
Let’s break this down.
We’re using the v-bind
directive’s shorthand :
to bind to our button’s class
. Inside the brackets we’re determining the presence of the disabled-button
class by the truthiness of the data property inStock
.
In other words, when our product is not in stock ( !inStock
), the disabledButton
class is added. Since the disabled-button
class applies a gray background-color
, the button turns gray.
Great! We’ve combined our new skill class binding with attribute binding to disable our button and turn it gray whenever our product inStock
is false.
What’d we learn
- Data can be bound to an element’s
style
attribute - Data can be bound to an element’s
class
- We can use expressions inside an element’s class binding to evaluate whether a class should appear or not
What else should we know?
- You can bind an entire class object or array of classes to an element
Learn by doing
Challenge:
When inStock
is false, bind a class to the “Out of Stock” p
tag that adds text-decoration: line-through
to that element.
Computed Properties
In this lesson, we’ll be covering Computed Properties. These are properties on the Vue instance that calculate a value rather than store a value.
Goal
Our first goal in this lesson is to display our brand
and our product
as one string.
Starting Code
Notice we’ve added a brand
.
Problem
We want brand
and product
to be combined into one string. In other words, we want to display “Vue Mastery Socks” in our h1
instead of just “Socks. How can we concatenate two values from our data?
Solution
Since computed properties calculate a value rather than store a value, let’s add the computed
option to our instance and create a computed property called title
.
This is pretty straightforward. When title
is called, it will concatenate brand
and product
into a new string and return that string.
Now all we need to do is put title
within the h1
of our page.
So instead of:
We now have:
It works! “Vue Mastery Socks” is appearing in our h1
.
We’ve taken two values from our data and computed them in such a way that we’ve created a new value. If brand
were to update in the future, let’s say to “Vue Craftery”, our computed property would not need to be refactored. It would still return the correct string: “Vue Craftery Socks”. Our computed property title
would still be using brand
, just like before, but now brand
would have a new value.
That was a pretty simple but not entirely practical example, so let’s work through a more complex usage of a computed property.
A More Complex Example
Currently, the way we are updating our image is with the updateProduct
method. We are passing our variantImage
into it, then setting the image to be whichever variant is currently hovered on.
This works fine for now, but if we want to change more than just the image based on which variant is hovered on, then we’ll need to refactor this code. So let’s do that now.
Instead of having image
in our data, let’s replace it with selectedVariant
, which we’ll initialize as 0.
Why 0? Because we’ll be setting this based on the index that we hover on. We can add index to our v-for here, like so.
Now instead of passing in the variantImage, we’ll pass in the index.
In our updateProduct
method, we’ll pass in the index, and instead of updating this.image, we’ll update this.selectedVariant with the index of whichever variant is currently hovered on. Let’s put a console.log in here too, to make sure it’s working.
Now when we refresh and open up the console, we can see that it works. We’re logging 0 and 1 as we hover on either variant.
But notice this warning here in the console:
That’s because we deleted image
and replaced it with selectedVariant
. So let’s turn image
into a computed property.
Inside, we are returning this.variants
, which is our array of variants, and we are using our selectedVariant
, which is either 0 or 1, to target the first or second element in that array, then we’re using dot notation to target its image.
When we refresh, our image is toggling correctly like it was before, but now we’re using a computed property to handle this instead.
Now that we have refactored the updateProduct
method to update the selectedVariant
, we can access other data from the variant, such as the variantQuantity
they both now have.
Just like we did with image
, let’s remove inStock
from our data and turn it into a computed property that uses our variant’s quantities.
This is very similar to our image
computed property, we’re just targeting the variantQuantity
now rather than the variantImage
.
Now when we hover on the blue variant, which has a quantity of zero, inStock will evaluate to false since 0 is “falsey”, so we’ll now see Out of Stock appear.
Notice how our button is still conditionally turning gray whenever inStock
is false, just like before.
Why? Because we’re still using inStock
to bind the disabledButton
class to that button. The only difference is that now inStock
is a computed property rather than a data value.
What’d we learn
- Computed properties calculate a value rather than store a value.
- Computed properties can use data from your app to calculate its values.
What else should we know?
Computed properties are cached, meaning the result is saved until its dependencies change. So when quantity
changes, the cache will be cleared and the **next time you access the value of inStock
, it will return a fresh result, and cache that result.
With that in mind, it’s more efficient to use a computed property rather than a method for an expensive operation that you don’t want to re-run every time you access it.
It is also important to remember that you should not be mutating your data model from within a computed property. You are merely computing values based on other values. Keep these functions pure.
Learn by doing
Challenge:
Add a new boolean data property onSale
and create a computed property that takes brand
, product
and onSale
and prints out a string whenever onSale
is true.
Components
Welcome
In this lesson we’ll be learning about the wonderful world of components. Components are reusable blocks of code that can have both structure and functionality. They help create a more modular and maintainable codebase.
Goal
Throughout the course of this lesson we’ll create our first component and then learn how to share data with it.
Starting Code
Problem
In a Vue application, we don’t want all of our data, methods, computed properties, etc. living on the root instance. Over time, that would become unmanageable. Instead, we’ll want to break up our code into modular pieces so that it is easier and more flexible to work with.
Solution
We’ll start out by taking the bulk of our current code and moving it over into a new component for our product.
We register the component like this:
The first argument is the name we choose for the component, and the second is an options object, similar to how we created our initial Vue instance.
In the Vue instance, we used the el
property to plug into an element in the DOM. For a component, we use the template
property to specify its HTML.
Inside that options object, we’ll add our template.
There are several ways to create a template in Vue, but for now we’ll be using a template literal, with back ticks.
If all of our template code was not nested within one element, such as this div with the class of “product”, we would have gotten this error:
Component template should contain exactly one root element
In other words, a component’s template can only return one element.
So this will work, since it’s only one element:
But this won’t work, since it’s two sibling elements:
So if we have multiple sibling elements, like we have in our product
template, they must be wrapped in an outer container element so that the template has exactly one root element
:
Now that our template is complete with our product HTML, we’ll add our data, methods and computed properties from the root instance into our new component.
As you can see, this component looks nearly identical in structure to our original instance. But did you notice that data
is now a function? Why the change?
Because we often want to reuse components. And if we had multiple product
components, we’d need to ensure a separate instance of our data was created for each component. Since data
is now a function that returns a data object, each component will definitely have its own data. If data
weren’t a function, each product
component would be sharing the same data everywhere it was used, defeating the purpose of it being a reusable component.
Now that we’ve moved our product-related code into its own product
component, our root instance looks like this:
All we need to do now is neatly tuck our product
component within our index.html.
Now our product
is being displayed again.
If we open the Vue dev tools, we’ll see that we have the Root
and then below that, the Product
component.
Just to demonstrate the handy power of components, let’s add two more product
components, to see how easy it is to reuse a component.
We’ll only be using one product
component moving forward, however.
Problem
Often in an application, a component will need to receive data from its parent. In this case, the parent of our product
component is the root instance itself.
Let’s say our root instance has some user data on it, specifying whether the user is a premium account holder. If so, our instance now might look like this:
Let’s also say that if a user is a premium member, then all of their shipping is free.
That means we’ll need our product
component to display different values for shipping based on what the value of premium
is, on our root instance.
So how can we send premium
from the root instance, down into its child, the product
component?
Solution
In Vue, we use props to handle this kind of downward data sharing. Props are essentially variables that are waiting to be filled with the data its parent sends down into it.
We’ll start by specifying what props the product
component is expecting to receive by adding a props object to our component.
Notice that we’re using some built-in props validation, where we’re specifying the data type of the premium
prop as Boolean, and making it required.
Next, in our template, let’s make an element to display our prop to make sure it’s being passed down correctly.
So far so good. Our product
component knows that it will be receiving a required boolean, and it also has a place to display that data.
But we have not yet passed premium
into the product
component. We can do this with a custom attribute, which is essentially a funnel on the component that we can pass premium
through.
So what are we doing here?
We’ve given our product
component a prop, or a custom attribute , called premium
. We are binding that custom attribute :
to the premium
that lives in our instance’s data.
Now our root instance can pass premium
into its child product
component. Since the attribute is bound to premium
in our root instance’s data, the current value of premium
in our instance’s data will always be sent to product
.
If we’ve wired this up correctly, we should see: “User is premium: true”.
Great, it’s working. If we check the Vue dev tools, we can see on the right that product
now has a prop called premium
with the value of true
.
Now that we’re successfully passing data into our component, let’s use that data to affect what we show for shipping. Remember, if premium
is true, that means shipping is free. So let’s use our premium
prop within a computed property called shipping.
Now, we’re using our prop ( this.premium
), and whenever it’s true, shipping
will return “Free”. Otherwise, it’ll return 2.99.
Instead of saying User is premium: {{ premium }}
, let’s use this element to show our shipping cost, by calling our computed property shipping
from here:
And now we see “Shipping: Free”. Why? Because premium
is true, so shipping
returned “Free”.
Awesome. So now we’ve passed data from our parent into its child component, and used that data within a computed property that displays different shipping values based on the value of our prop.
Great work!
Good to Know: You should not mutate props inside your child components.
What’d we learn
- Components are blocks of code, grouped together within a custom element
- Components make applications more manageable by breaking up the whole into resuable parts that have their own structure and behavior
- Data on a component must be a function
- Props are used to pass data from parent to child
- We can specify requirements for the props a component is receiving
- Props are fed into a component through a custom attribute
- Props can be dynamically bound to the parent’s data
- Vue dev tools provide helpful insight about your components
Learn by doing
Challenge:
Create a new component for product-details
with a prop of details
.
Communicating Events
In our previous lesson, we learned how to create components and pass data down into them via props. But what about when we need to pass information back up? In this lesson we’ll learn how to communicate from a child component up to its parent.
Goal
By the end of this lesson, our product
component will be able to tell its parent, the root instance, that an event has occurred, and send data along with that event notification.
Starting Code
Currently, our app looks like this:
<div id="app">
<product :premium="premium"></product>
</div>
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
<div id="product">
<div class="product-image">
<img :src="image" />
</div>
<div class="product-info">
<div class="cart">
<p>Cart({{ cart }})</p>
</div>
<h1>{{ title }}</h1>
<p>Shipping: {{ shipping }}</p>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<h2>Details</h2>
<ul>
<li v-for="detail in details">{{ detail }}</li>
</ul>
<h3>Colors:</h3>
<div v-for="variant in variants" :key="variant.variantId">
<div class="color-box" :style="{ backgroundColor: variant.variantColor }" @mouseover="updateProduct(index)"></div>
</div>
<button :class="{ disabledButton: !inStock }" v-on:click="addToCart" :disabled="!inStock">Add to Cart</button>
</div>
</div>
`,
data() {
return {
product: "Socks",
brand: "Vue Mastery",
selectedVariant: 0,
details: ["80% cotton", "20% polyester", "Gender-neutral"],
variants: [
{
varaintId: 1,
variantQuantity: 15,
variantColor: "green",
variantImage: "./assets/vmSocks-green.jpg"
},
{
variantId: 2,
variantQuantity: 0,
variantColor: "blue",
variantImage: "./assets/vmSocks-blue.jpg"
}
],
cart: 0
}
},
methods: {
addToCart() {
this.cart += 1
},
updateProduct(index) {
this.selectedVariant = index
}
},
computed: {
title() {
return this.brand + ' ' + this.product
},
image() {
return this.variants[this.selectedVariant].variantImage
},
inStock() {
if (this.quantity > 0) {
return true
} else {
return false
}
},
shipping() {
if (this.premium) {
return "Free"
} else {
return 2.99
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true
}
})
Problem
Now that product
is its own component, it doesn’t make sense for our cart to live within product
. It would get very messy if every single product had its own cart that we had to keep track of. Instead, we’ll want the cart to live on the root instance, and have product
communicate up to that cart when its “Add to Cart” button is pressed.
Solution
Let’s move the cart back to our root instance.
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: 0
}
})
And we’ll move our cart’s template into our index.html:
<div id="app">
<div class="cart">
<p>Cart({{ cart }})</p>
</div>
<product :premium="premium"></product>
</div>
As expected, now if we click on the “Add to Cart” button, nothing happens.
What do we want to happen? When the “Add to Cart” button is pressed in product
, the root instance should be notified, which then triggers a method it has to update the cart
.
First, let’s change out the code we have in our component’s addToCart
method.
It was this:
addToCart() {
this.cart += 1
},
Now it’s this:
addToCart() {
this.$emit('add-to-cart')
},
What does this mean?
It means: when addToCart
is run, emit
an event by the name of “add-to-cart”. In other words, when the “Add to Cart” button is clicked, this method fires, announcing that the click event just happened.
But right now, we have nowhere listening for that announcement that was just emitted. So let’s add that listener here:
<product :premium="premium" @add-to-cart="updateCart"></product>
Here we are using @add-to-cart
in a similar way as we are using :premium
. Whereas :premium
is a funnel on product
that data can be passed down into, @add-to-cart
is essentially a radio that can receive the event emission from when the “Add to Cart” button was clicked. Since this radio is on product
, which is nested within our root instance, the radio can blast the announcement that a click happened, which will trigger the updateCart
method, which lives on the root instance.
@add-to-cart="updateCart"
This code essentially translates to: “When you hear that the “Add to Cart” event happened, run the updateCart
method.
That method should look familiar:
methods: {
updateCart() {
this.cart += 1
}
}
It’s the method that used to be on product
. Now it lives on our root instance, and is called whenever the “Add to Cart” button is pressed.
Now when our button is pressed, it triggers addToCart
, which emits an announcement. Our root instance hears the announcement through the radio on its product
component, and the updateCart
method runs, which increments the value of cart
.
So far so good.
But in a real application, it’s not helpful to only know that a product was added to the cart, we’d need to know which product was just added to the cart. So we’ll need to pass data up along with the event announcement.
We can add that in as a second argument when we emit an event:
addToCart() {
this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId)
},
Now, along with the announcement that the click event occurred, the id of the product that was just added to the cart is sent as well. Instead of just incrementing the number of cart, we can now make cart an array:
cart: []
And push the product’s id into our cart array:
methods: {
updateCart(id) {
this.cart.push(id)
}
}
Now our array has one product within it, whose id
is being displayed on the page.
But we don’t need to display the contents of our array here. Instead, we just want to display the amount of products in our array, so we can say this in our template instead:
<p>Cart({{ cart.length }})</p>
Now we’re just displaying the length of the array, or in other words: the number of products in the cart. It looks just like it did before, but instead of only incrementing the value of cart
by 1, now we’re actually sending data about which product was just added to the cart.
Great work!
What’d we learn
- A component can let its parent know that an event has happened with
$emit
- A component can use an event handler with the
v-on
directive (@
for short) to listen for an event emission, which can trigger a method on the parent - A component can
$emit
data along with the announcement that an event has occurred - A parent can use data emitted from its child
Learn by doing
Challenge:
Add a button that removes the product from the cart array by emitting an event with the id of the product to be removed.
Forms
In this lesson we’ll be learning how to work with forms in Vue in order to collect user input, and also learn how to do some custom form validation.
Goal
We’ll be creating a form that allows users to submit a review of a product, but only if they have filled out the required fields.
Starting Code
Our app now looks like this:
index.html
<div id="app">
<div class="cart">
<p>Cart({{ cart.length }})</p>
</div>
<product :premium="premium" @add-to-cart="updateCart"></product>
</div>
main.js
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
<div class="product">
<div class="product-image">
<img :src="image" />
</div>
<div class="product-info">
<h1>{{ product }}</h1>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<p>Shipping: {{ shipping }}</p>
<ul>
<li v-for="detail in details">{{ detail }}</li>
</ul>
<div class="color-box"
v-for="(variant, index) in variants"
:key="variant.variantId"
:style="{ backgroundColor: variant.variantColor }"
@mouseover="updateProduct(index)"
>
</div>
<button v-on:click="addToCart"
:disabled="!inStock"
:class="{ disabledButton: !inStock }"
>
Add to cart
</button>
</div>
</div>
`,
data() {
return {
product: 'Socks',
brand: 'Vue Mastery',
selectedVariant: 0,
details: ['80% cotton', '20% polyester', 'Gender-neutral'],
variants: [
{
variantId: 2234,
variantColor: 'green',
variantImage: './assets/vmSocks-green.jpg',
variantQuantity: 10
},
{
variantId: 2235,
variantColor: 'blue',
variantImage: './assets/vmSocks-blue.jpg',
variantQuantity: 0
}
]
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId)
},
updateProduct(index) {
this.selectedVariant = index
}
},
computed: {
title() {
return this.brand + ' ' + this.product
},
image(){
return this.variants[this.selectedVariant].variantImage
},
inStock(){
return this.variants[this.selectedVariant].variantQuantity
},
shipping() {
if (this.premium) {
return "Free"
}
return 2.99
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: []
},
methods: {
updateCart(id) {
this.cart.push(id)
}
}
})
Problem
We want our users to be able to review our product, but we don’t yet have a way to collect user input. We’ll need to create a form for that.
Solution
We’ll create a new component for our form, which will be called product-review
since it is the component that collects product reviews. product-review
will be nested within our product
component.
Let’s register our new component, start building out its template, and give it some data.
Vue.component('product-review', {
template: `
<input>
`,
data() {
return {
name: null
}
}
})
As you can see, our component has an input element, and name
within its data.
How can we bind what the user types into the input field to our name
data?
Earlier we learned about binding with v-bind
but that was only for one-way binding, from the data to the template. Now, we want whatever the user inputs to be bound to name
in our data. In other words, we want to add a dimension of data binding, from the template to the data .
The v-model directive
Vue’s v-model
directive gives us this two-way binding. That way, whenever something new is entered into the input, the data changes. And whenever the data changes, anywhere using that data will update.
So let’s add v-model
to our input, and bind it to name
in our component’s data.
<input v-model="name">
So far so good. Let’s add a complete form to our template.
<form class="review-form" @submit.prevent="onSubmit">
<p>
<label for="name">Name:</label>
<input id="name" v-model="name" placeholder="name">
</p>
<p>
<label for="review">Review:</label>
<textarea id="review" v-model="review"></textarea>
</p>
<p>
<label for="rating">Rating:</label>
<select id="rating" v-model.number="rating">
<option>5</option>
<option>4</option>
<option>3</option>
<option>2</option>
<option>1</option>
</select>
</p>
<p>
<input type="submit" value="Submit">
</p>
</form>
As you can see, we’ve added v-model
to our input
, textarea
and select
. Note on the select
we’ve used the .number
modifier (more on this below). This ensures that the data will be converted into an integer versus a string.
These elements are now bound to this data:
data() {
return {
name: null,
review: null,
rating: null
}
}
At the top of the form, you can see that our onSubmit
method will be triggered when this form is submitted. We’ll build out the onSubmit
method in a moment. But first, what’s that .prevent
doing?
That is an event modifier , which is used to prevent the submit event from reloading our page. There are several other useful event modifiers, which we won’t cover in this lesson.
We’re now ready to build out our onSubmit
method. We’ll start out with this:
onSubmit() {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.name = null
this.review = null
this.rating = null
}
As you can see, onSubmit
is creating an object of our user-inputted data, stored within a variable called productReview
. We’re also resetting the values of name
, review
and rating
to be null. But we’re not done yet. We need to send this productReview
somewhere. Where do we want to send it?
It makes sense for our product reviews to live within the data of the product
itself. Considering product-review
is nested within product
, that means that product-review
is a child of product
. As we learned in the previous lesson, we can use $emit
to send up data from our child to our parent when an event occurs.
So let’s add $emit
to our onSubmit
method:
onSubmit() {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.$emit('review-submitted', productReview)
this.name = null
this.review = null
this.rating = null
}
We’re now emitting an event announcement by the name of “review-submitted”, and passing along with it the productReview
object we just created.
Now we need to listen for that announcement on product-review
.
<product-review @review-submitted="addReview"></product-review>
This translates to: when the “review-submitted” event happens, run product
's addReview
method.
That method looks like this:
addReview(productReview) {
this.reviews.push(productReview)
}
This function takes in the productReview
object emitted from our onSubmit
method, then pushes that object into the reviews
array on our product
component’s data. We don’t yet have reviews
on our product’s data, so all that’s left is to add that now:
reviews: []
Awesome. Our form elements are bound to the product-review
component’s data, that data is used to create a productReview
object, and that productReview
is being sent up to product
when the form is submitted. Then that productReview
is added to product
's reviews
array.
Displaying the Reviews
Now all that’s left to do is to display our reviews. We’ll do so in our product
component, just above where the product-review
component is nested.
<div>
<h2>Reviews</h2>
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul>
<li v-for="review in reviews">
<p>{{ review.name }}</p>
<p>Rating: {{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
Here, we are creating a list of our reviews with v-for
and printing them out using dot notation, since each review is an object.
In the p tag, we’re checking if the reviews
array has a length (has any productReview
objects in it), and it if does not, we’ll display: “There are no reviews yet.”
Form Validation
Often with forms, we’ll have required fields. For instance, we wouldn’t want our user to be able to submit a review if the field they were supposed to write their review in is empty.
Fortunately, HTML5 provides you with the required
attribute, like so:
<input required >
This will provide an automatic error message when the user tries to submit the form if that field is not filled in.
While it is nice to have form validation handled natively in the browser, instead of in your code, sometimes the way that the native form validation is happening may not be the best for your use-case. You may prefer writing your own custom form validation.
Custom Form Validation
Let’s take a look at how you can build out your own custom form validation with Vue.
In our product-review
component’s data we’ll add an array for errors:
data() {
return {
name: null,
review: null,
rating: null,
errors: []
}
}
We want to add an error into that array whenever one of our fields is empty. So we can say:
if(!this.name) this.errors.push("Name required.")
if(!this.review) this.errors.push("Review required.")
if(!this.rating) this.errors.push("Rating required.")
This translates to: if our name
data is empty, push “Name required.” into our errors
array. The same goes for our review
and rating
data. If either are empty, an error string will be pushed into our errors
array.
But where will we put these lines of code?
Since we only want errors to be pushed if we don’t have our name
, review
or rating
data filled in, we can place this code within some conditional logic in our onSubmit
method.
onSubmit() {
if(this.name && this.review && this.rating) {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.$emit('review-submitted', productReview)
this.name = null
this.review = null
this.rating = null
} else {
if(!this.name) this.errors.push("Name required.")
if(!this.review) this.errors.push("Review required.")
if(!this.rating) this.errors.push("Rating required.")
}
}
Now, we are checking to see if we have data filled in for our name
, review
and rating
. If we do, we create the productReview
object, and send it up to our parent, the product
component. Then we reset the data values to null.
If we don’t have name
, review
and rating
, we’ll push errors into our errors
array, depending on which data is missing.
All that remains is to display these errors, which we can do with this code:
<p v-if="errors.length">
<b>Please correct the following error(s):</b>
<ul>
<li v-for="error in errors">{{ error }}</li>
</ul>
</p>
This uses the v-if
directive to check if there are any errors. In other words, if our errors array is not empty, then this p tag is displayed, which renders out a list with v-for
, using the errors
array in our data.
Great. Now we’ve implemented our own custom form validation.
Using .number
Using the .number
modifier on v-model
is a helpful feature, but please be aware there is a known bug with it. If the value is blank, it will turn back into a string. The Vue.js Cookbook offers the solution to wrap that data in the Number
method, like so:
Number(this.myNumber)
What’d we learn
- We can use the
v-model
directive to create two-way binding on form elements - We can use the
.number
modifier to tell Vue to cast that value as a number, but there is a bug with it - We can use the
.prevent
event modifier to stop the page from reloading when the form is submitted - We can use Vue to do fairly simple custom form validation
Learn by doing
Challenge
Add a question to the form: “Would you recommend this product”. Then take in that response from the user via radio buttons of “yes” or “no” and add it to the productReview
object, with form validation.
Tabs
In this lesson, we’ll learn how to add tabs to our application and implement a simple solution for global event communication.
Goal
We’ll learn how to create tabs to display our reviews and our review form separately.
Starting Code
index.html
<div id="app">
<div class="cart">
<p>Cart({{ cart.length }})</p>
</div>
<product :premium="premium" @add-to-cart="updateCart"></product>
</div>
main.js
Vue.component('product', {
props: {
premium: {
type: Boolean,
required: true
}
},
template: `
<div class="product">
<div class="product-image">
<img :src="image">
</div>
<div class="product-info">
<h1>{{ product }}</h1>
<p v-if="inStock">In Stock</p>
<p v-else>Out of Stock</p>
<p>Shipping: {{ shipping }}</p>
<ul>
<li v-for="(detail, index) in details" :key="index">{{ detail }}</li>
</ul>
<div class="color-box"
v-for="(variant, index) in variants"
:key="variant.variantId"
:style="{ backgroundColor: variant.variantColor }"
@mouseover="updateProduct(index)"
>
</div>
<button @click="addToCart"
:disabled="!inStock"
:class="{ disabledButton: !inStock }"
>
Add to cart
</button>
</div>
<product-review @review-submitted="addReview"></product-review>
</div>
`,
data() {
return {
product: 'Socks',
brand: 'Vue Mastery',
selectedVariant: 0,
details: ['80% cotton', '20% polyester', 'Gender-neutral'],
variants: [
{
variantId: 2234,
variantColor: 'green',
variantImage: './assets/vmSocks-green.jpg',
variantQuantity: 10
},
{
variantId: 2235,
variantColor: 'blue',
variantImage: './assets/vmSocks-blue.jpg',
variantQuantity: 0
}
],
reviews: []
}
},
methods: {
addToCart() {
this.$emit('add-to-cart', this.variants[this.selectedVariant].variantId)
},
updateProduct(index) {
this.selectedVariant = index
},
addReview(productReview) {
this.reviews.push(productReview)
}
},
computed: {
title() {
return this.brand + ' ' + this.product
},
image(){
return this.variants[this.selectedVariant].variantImage
},
inStock(){
return this.variants[this.selectedVariant].variantQuantity
},
shipping() {
if (this.premium) {
return "Free"
}
return 2.99
}
}
})
Vue.component('product-review', {
template: `
<form class="review-form" @submit.prevent="onSubmit">
<p class="error" v-if="errors.length">
<b>Please correct the following error(s):</b>
<ul>
<li v-for="error in errors">{{ error }}</li>
</ul>
</p>
<p>
<label for="name">Name:</label>
<input class="name" v-model="name">
</p>
<p>
<label for="review">Review:</label>
<textarea id="review" v-model="review"></textarea>
</p>
<p>
<label for="rating">Rating:</label>
<select id="rating" v-model.number="rating">
<option>5</option>
<option>4</option>
<option>3</option>
<option>2</option>
<option>1</option>
</select>
</p>
<p>
<input type="submit" value="Submit">
</p>
</form>
`,
data() {
return {
name: null,
review: null,
rating: null,
errors: []
}
},
methods: {
onSubmit() {
if (this.name && this.review && this.rating) {
let productReview = {
name: this.name,
review: this.review,
rating: this.rating
}
this.$emit('review-submitted', productReview)
this.name = null
this.review = null
this.rating = null
} else {
if (!this.name) this.errors.push("Name required.")
if (!this.review) this.errors.push("Review required.")
if (!this.rating) this.errors.push("Rating required.")
}
}
}
})
var app = new Vue({
el: '#app',
data: {
premium: true,
cart: []
},
methods: {
updateCart(id) {
this.cart.push(id)
}
}
})
Problem
Currently in our project, we’re displaying our reviews and the form that is used to submit a review on top of each other. This works for now, but if our page needs to display more and more content, we’ll want the option to conditionally display content, based on user behavior.
Solution
We can implement tabs so when we click on the Reviews tab, our reviews are shown, and when we click on the Add a Review tab, our form is shown.
Creating a Tabs Component
We’ll start by creating a product-tabs
component, which will be nested at the bottom of our product
component.
Vue.component('product-tabs', {
template: `
<div>
<span class="tab" v-for="(tab, index) in tabs" :key="index">{{ tab }}</span>
</div>
`,
data() {
return {
tabs: ['Reviews', 'Make a Review']
}
}
})
We’ll add to this component soon, but so far this is what’s happening:
In our data, we have a tabs
array with strings that we’re using as the titles for each tab. In the template, we’re using v-for
to create a span
for each string from our tabs
array.
We want to know which tab is currently selected, so in our data we’ll add selectedTab
and dynamically set that value with an event handler, setting it equal to the tab that was just clicked, with:
@click="selectedTab = tab"
So if we click the “Reviews” tab, then selectedTab
will be “Reviews”. If we click the “Make a Review” tab, selectedTab
will be “Make a Review”.
Vue.component('product-tabs', {
template: `
<div>
<ul>
<span class="tab"
v-for="(tab, index) in tabs"
@click="selectedTab = tab" // sets value of selectedTab in data
>{{ tab }}</span>
</ul>
</div>
`,
data() {
return {
tabs: ['Reviews', 'Make a Review'],
selectedTab: 'Reviews' // set from @click
}
}
})
Class Binding for Active Tab
We should give the user some visual feedback so they know which tab is selected.
We can do this quickly by adding this class binding to our span:
:class="{ activeTab: selectedTab === tab }"
This translates to: apply our activeTab
class whenever it is true that selectedTab
is equal to tab
. Because selectedTab
is always equal to whichever tab
was just clicked, then this class will be applied to the tab the user clicked on.
In other words, when the first tab is clicked, selectedTab
will be “Reviews” and the tab
will be “Reviews”. So the activeTab
class will be applied since they are equivalent.
Great! It works.
Adding to our Template
Now that we’re able to know which tab was selected, we can build out our template and add the content we want to display when either tab is clicked.
What do we want to show up if we click on “Reviews”? Our reviews. So we’ll move that code from where it lives on our product
component and paste it into the template of our product-tabs
component, below our unordered list.
template: `
<div>
<div>
<span class="tab"
v-for="(tab, index) in tabs"
@click="selectedTab = tab"
>{{ tab }}</span>
</div>
<div> // moved here from where it was on product component
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul v-else>
<li v-for="(review, index) in reviews" :key="index">
<p>{{ review.name }}</p>
<p>Rating:{{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
</div>
`
Notice, we’ve deleted the h2
since we no longer need to say “Reviews” since that is the title of our tab.
But since reviews
lives on our product
component, we’ll need to send that data into our product-tabs
component via props.
So we’ll add the prop we expect to receive on our product-tabs
component:
props: {
reviews: {
type: Array,
required: false
}
}
And pass in reviews
on our product-tabs
component itself, so it is always receiving the latest reviews
.
<product-tabs :reviews="reviews"></product-tabs>
Now, what do we want to show when we click on the “Make a Review” tab? The review form.
So we’ll move the product-review
component from where it lives within the product
component and place it in the template of our tabs
component, below the div for reviews that we just added.
<div>
<product-review @review-submitted="addReview"></product-review>
</div>
Conditionally Displaying with Tabs
Now that we have our template set up, we want to conditionally display either the reviews div or the review form div, depending on which tab is clicked.
Since we are already storing the selectedTab
, we can use that with v-show
to conditionally display either tab. So whichever tab is selected, we’ll show the content for that tab.
We can add v-show="selectedTab === 'Reviews'"
to our reviews div, and that div will display whenever we click the first tab. Similarly we can say v-show="selectedTab === 'Make a Review'"
to display the second tab.
Now our template looks like this:
template: `
<div>
<div>
<span class="tab"
v-for="(tab, index) in tabs"
@click="selectedTab = index"
>{{ tab }}</span>
</div>
<div v-show="selectedTab === 'Review'"> // displays when "Reviews" is clicked
<p v-if="!reviews.length">There are no reviews yet.</p>
<ul>
<li v-for="review in reviews">
<p>{{ review.name }}</p>
<p>Rating:{{ review.rating }}</p>
<p>{{ review.review }}</p>
</li>
</ul>
</div>
<div v-show="selectedTab === 'Make a Review'"> // displays when "Make a Review" is clicked
<product-review @review-submitted="addReview"></product-review>
</div>
</div>
`
The form can’t trigger addReview
We are now able to click on a tab and display the content we want, and even dynamically style the tab that is currently selected. But if you look in the console, you’ll find this warning:
So what’s going on with our addReview
method?
This is a method that lives on our product
component. And it’s supposed to be triggered when our product-review
component (which is a child of product
) emits the review-submitted
event.
<product-review @review-submitted="addReview"></product-review>
But now our product-review
component is a child of our tabs
component, which is a child of the product
component. In other words, product-review
is now a grandchild of product
.
Our code is currently designed to have our product-review
component communicate with its parent, but it’s parent is no longer the product
component. So we need to refactor to make this event communication happen successfully.
Refactoring Our Code
A common solution for communicating from a grandchild up to a grandparent, or for communicating between components, is to use what’s called a global event bus.
This is essentially a channel through which you can send information amongst your components, and it’s just a Vue instance, without any options passed into it. Let’s create our event bus now.
var eventBus = new Vue()
If it helps, just imagine this as a literal bus, and its passengers are whatever you’re sending at the time. In our case, we want to send an event emission. We’ll use this bus to communicate from our product-review
component and let our product
component know the form was submitted, and pass the form’s data up to product
.
In our product-review
component, instead of saying:
this.$emit('review-submitted', productReview)
We’ll instead use the eventBus
to emit the event, along with its payload: productReview
.
eventBus.$emit('review-submitted', productReview)
Now, we no longer need to listen for the review-submitted
event on our product-review component, so we’ll remove that.
<product-review></product-review>
Now, in our product
component, we can delete our addReview
method and instead we’ll listen for that event using this code:
eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
This essentially says: when the eventBus
emits the review-submitted
event, take its payload (the productReview
) and push it into product
's reviews
array. This is very similar to what we were doing before with our addReview
method.
Why the ES6 Syntax?
We’re using the ES6 arrow function syntax here because an arrow function is bound to its parent’s context. In other words, when we say this
inside the function, it refers to this
component/instance. You can write this code without an arrow function, you’ll just have to manually bind the component’s this
to that function, like so:
eventBus.$on('review-submitted', function (productReview) {
this.reviews.push(productReview)
}.bind(this))
Final Step
We’re almost done. All that’s left to do is to put this code somewhere, like in mounted
.
mounted() {
eventBus.$on('review-submitted', productReview => {
this.reviews.push(productReview)
})
}
What’s mounted
? That’s a lifecycle hook, which is a function that is called once the component has mounted to the DOM. Now, once product
has mounted, it will be listening for the review-submitted
event. And once it hears it, it’ll add the new productReview
to its data.
We’ll learn more about lifecycle hooks in our Real World Vue course.
A Better Solution
Using an event bus is a common solution and you may see it in others’ Vue code, but please be aware this isn’t the best solution for communicating amongst components within your app.
As your app grows, you’ll want to implement Vue’s own state management solution: Vuex. This is a state-management pattern and library. You’ll also learn all about Vuex in our Mastering Vuexcourse, where we’ll teach you how to build a production-level apps that can scale, from setting up your project using the Vue CLI all the way to deployment.
Learn by doing
Challenge:
Create tabs for “Shipping” and “Details” that display the shipping cost and product details, respectively.