[VueMastery] Intro to Vue.js (Materials)

Video

Материалы

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.