Vue Mastery - Intro to Vue.js

 

The Vue Instance

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
    <div id="app">
        <h1>Product goes here</h1>
    </div>

    <script src="main.js"></script>
</body>
</html>

image-20200909191253217

Data to show in the html in main.js:

var product = 'Scoks'

Include the vuejs to the html first:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
    <div id="app">
        <h1>Product goes here</h1>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="main.js"></script>
</body>
</html>

make new instance in js:

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks'
    }
})

Use the data in html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <h1></h1>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200909191850940

image-20200909191941918

image-20200909192006230

var app = new Vue(). creates a new Vue instance (the root)

image-20200909192044050

image-20200909192054089

is an expression.

image-20200909192116090

2020-09-09 19.28.13

Attribute Binding

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img src="" alt="">
        </div>

        <div class="product-info">
            <h1></h1>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

img

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:

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image:"https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg"
    }
})

img

Notice we’ve added an image source to our data.

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img v-bind:src="image" alt="">
        </div>
        <div class="product-info">
            <h1></h1>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200909194126054

ethat results very big image on top of the name of the product.

Voila! Our image appears. If the value of image were to change, the src will update to match, and the new image will appear.

We can use v-bind again here if we want to bind alt text data to this same img element.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image:"https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText:"A pair of Socks"
    }
})
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img v-bind:src="image" v-bind:alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

2020-09-09 19.43.11

image-20200909194624587

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 :

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

img

Summary:

  • Data can be bound to the 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.

Conditional Rendering

In this lesson we’ll be uncovering how to conditionally display elements with Vue.

We want to display text that says if our product is in stock or not, based on our data.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inStock: true
    }
})

img

Notice we’ve added a new data property there at the bottom: inStock.

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?

Vue’s solution is simple and straightforward.

Given that our data contains this new property:

img

We can use the v-if and v-else directives to determine which element to render.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock">In Stock</p>
            <p v-else>Out of Stock</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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.

image-20200909195354902

2020-09-09 19.54.11

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:

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true
    }
})

We could use expressions, inside the quotes, to make our conditions more specific.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inventory > 10 ">In Stock</p>
            <p v-else-if="inventory <=10 && inventory > 0">Almost sold out!</p>
            <p v-else>Out of Stock</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

The element that will render is the first element whose expression evaluates to true.

2020-09-09 19.58.51

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inventory > 10 ">In Stock</p>
            <p v-else-if="inventory <=10 && inventory > 0">Almost sold out!</p>
            <p v-else>Out of Stock</p>
            <p v-show="inStock">You can buy</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

2020-09-09 20.01.31

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.

List Rendering

In this lesson, we’ll learn how to display lists onto our webpages with Vue.

We want to be able to display a list of our product’s details.

img

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ]
    }
})

What’s new here is our array of details at the bottom.

img

We want our page to display our product’s details. How can we iterate through this array to display its data?

Another Vue directive to the rescue. The v-for directive allows us to loop over an array and render data from within it.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200909201025163

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( `` ).

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.

img

Let’s take a look at a more complex example, displaying an object’s data in a div.

Iterating Over Objects

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?

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants:[
            {
                variantID:2234,
                variantColor:"green"
            },
            {
                variantID:2235,
                variantColor:"blue"
            }
        ]
    }
})

Let’s display the color of each variant. We’ll write:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants">
                <p> </p>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200909201605141

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 `` 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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p> </p>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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.

Event Handling

In this lesson we’ll be learning how to listen for DOM events that we can use to trigger methods.

We want to have a button that increments the number of items in our cart.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p> </p>
            </div>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants:[
            {
                variantID:2234,
                variantColor:"green"
            },
            {
                variantID:2235,
                variantColor:"blue"
            }
        ]
    }
})

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green"
            },
            {
                variantID: 2235,
                variantColor: "blue"
            }
        ],
        cart: 0
    }
})

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p> </p>
            </div>
        </div>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

We’ll also make a button to add items to our cart.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p> </p>
            </div>
        </div>
        <button v-on:click="cart+=1">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

As you can see, we’re using Vue’s v-on directive to increment the value of cart

2020-09-09 20.23.21

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:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p> </p>
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green"
            },
            {
                variantID: 2235,
                variantColor: "blue"
            }
        ],
        cart: 0
    },
    methods:{
        addToCart(){
            this.cart +=1
        }
    }
})

image-20200909202723434

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        }
    }
})

Now each variant has an image with green and blue socks, respectively.

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.

We’ll use the v-on directive again, but this time we’ll use its shorthand @ and listen for a mouseover event.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p @mouseover="updateProduct(variant.variantImage)"> </p>
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

Notice that we’re passing variant.variantImage in as an argument to our updateProduct method.

Let’s build out that method.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    }
})

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:

img

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.

2020-09-09 20.36.18

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

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.

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText" width="200px">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div v-for="variant in variants" :key="variant.variantID">
                <p @mouseover="updateProduct(variant.variantImage)"> </p>
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    }
})

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.

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:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText" width="200px">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="variant in variants"
                 :key="variant.variantID"
                 style="{backgroundColor:variant.variantColor}"
            >
                <p @mouseover="updateProduct(variant.variantImage)"> </p>
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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 divs 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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText" width="200px">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="variant in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(variant.variantImage)"
            >
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

img

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!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText" width="200px">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="variant in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(variant.variantImage)"
            >
            </div>
        </div>
        <button v-on:click="addToCart">Add to cart</button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
body {
    font-family: tahoma;
    color:#282828;
    margin: 0px;
}

.nav-bar {
    background: linear-gradient(-90deg, #84CF6A, #16C0B0);
    height: 60px;
    margin-bottom: 15px;
}

.product {
    display: flex;
    flex-flow: wrap;
    padding: 1rem;
}

img {
    border: 1px solid #d8d8d8;
    width: 70%;
    margin: 40px;
    box-shadow: 0px .5px 1px #d8d8d8;
}

.product-image {
    width: 80%;
}

.product-image,
.product-info {
    margin-top: 10px;
    width: 50%;
}

.color-box {
    width: 40px;
    height: 40px;
    margin-top: 5px;
}

.cart {
    margin-right: 25px;
    float: right;
    border: 1px solid #d8d8d8;
    padding: 5px 20px;
}

button {
    margin-top: 30px;
    border: none;
    background-color: #1E95EA;
    color: white;
    height: 40px;
    width: 100px;
    font-size: 14px;
}

.disabledButton {
    background-color: #d8d8d8;
}

.review-form {
    width: 400px;
    padding: 20px;
    margin: 40px;
    border: 1px solid #d8d8d8;
}

input {
    width: 100%;
    height: 25px;
    margin-bottom: 20px;
}

textarea {
    width: 100%;
    height: 60px;
}

.tab {
    margin-left: 20px;
    cursor: pointer;
}

.activeTab {
    color: #16C0B0;
    text-decoration: underline;
}
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    }
})

2020-09-10 11.31.22

Now that we’ve learned how to do style binding, let’s explore class binding.

Currently, we have this in our data:

img

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="variant in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(variant.variantImage)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disableButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

It works! The button is now grayed out when inStock = false.

Let’s break this down.

img

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.

2020-09-10 11.44.03

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

img

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.

Our first goal in this lesson is to display our brand and our product as one string.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    }
})

img

Notice we’ve added a brand.

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?

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    },
    computed:{
        title(){
            return this.brand + "" + this.product
        }
    }
})

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="variant in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(variant.variantImage)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200910115554970

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.

img

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        selectedVariant: 0,
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(variantImage) {
            this.image = variantImage
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        }
    }
})

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.

img

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(variant.variantImage)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

Now instead of passing in the variantImage, we’ll pass in the index.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

img

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        selectedVariant: 0,
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        }
    }
})

img

2020-09-10 12.04.03

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.

So let’s turn image into a computed property.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        image: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
        altText: "A pair of Socks",
        inventory: 100,
        inStock: true,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg"
            }
        ],
        selectedVariant: 0,
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        }
    }
})

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.

2020-09-10 12.07.22

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.

img

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.

var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        altText: "A pair of Socks",
        inventory: 100,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                variantQuantity: 10

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                variantQuantity: 0
            }
        ],
        selectedVariant: 0,
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }
})

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.

2020-09-10 12.14.10

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.

Components

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.

Throughout the course of this lesson we’ll create our first component and then learn how to share data with it.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        product: 'Socks',
        brand: "AzatAI",
        altText: "A pair of Socks",
        inventory: 100,
        details: [
            "80% cotton",
            "20% polyester",
            "Gender-neutral"
        ],
        variants: [
            {
                variantID: 2234,
                variantColor: "green",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                variantQuantity: 10

            },
            {
                variantID: 2235,
                variantColor: "blue",
                variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                variantQuantity: 0
            }
        ],
        selectedVariant: 0,
        cart: 0
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }
})

img

img

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.

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:

Vue.component('product',{
    template:`
    `
})

img

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.

img

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:

img

But this won’t work, since it’s two sibling elements:

img

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.

img

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:

img

All we need to do now is neatly tuck our product component within our index.html.

img

Now our product is being displayed again.

img

Vue.component("product", {
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
})
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <product></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200910123404994

If we open the Vue dev tools, we’ll see that we have the Root and then below that, the Product component.

image-20200910123453932

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.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <product></product>
    <product></product>
    <product></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

2020-09-10 12.35.34

We’ll only be using one product component moving forward, however.

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:

Vue.component("product", {
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

img

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.

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

img

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.

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>User is premium: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(index)
        }
    },
    computed: {
        title() {
            return this.brand + " " + this.product
        },
        image() {
            return this.variants[this.selectedVariant].variantImage
        },
        inStock() {
            return this.variants[this.selectedVariant].variantQuantity
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

img

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.

img

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <product :premium="premium"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

2020-09-10 12.43.51

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”.

image-20200910124518477

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.

image-20200910124611404

image-20200910124629015

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.

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>User is premium: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            }
            else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

img

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: , let’s use this element to show our shipping cost, by calling our computed property shipping from here:

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            }
            else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

img

image-20200910124915675

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 reusuable 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

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.

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.

Currently, our app looks like this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <product :premium="premium"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
        <div class="cart">
            <p>Cart()</p>
        </div>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            }
            else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true
    }
})

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.

Let’s move the cart back to our root instance.

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.cart += 1
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: 0
    }
})
    var app = new Vue({
      el: '#app',
      data: {
        premium: true,
        cart: 0
      }
    })

And we’ll move our cart’s template into our index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div class="cart">
    <p>Cart()</p>
</div>
<div id="app">
    <product :premium="premium"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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.

from :

addToCart() {
            this.cart += 1
        },

to :

addToCart() {
            this.$emit("add-to-cart")
        },
Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart")
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: 0
    }
})

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>    
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div class="cart">
    <p>Cart()</p>
</div>
<div id="app">
    <product :premium="premium" @add-to-cart="updateCart"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

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.

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart")
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: 0
    },
    methods: {
        updateCart() {
            this.cart += 1
        }
    }
})
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="cart">
        <p>Cart (  )</p>
    </div>
    <product :premium="premium" @add-to-cart="updateCart"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

image-20200910150241152

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)
        },
Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart", this.variants[this.selectedVariant].variantID)
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: 0
    },
    methods: {
        updateCart() {
            this.cart += 1
        }
    }
})

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)
        }
      }
Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart", this.variants[this.selectedVariant].variantID)
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

})
var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: []
    },
    methods: {
        updateCart(id) {
            this.cart.push(id)
        }
    }
})

Now our array has one product within it, whose id is being displayed on the page.

img

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()</p>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Product App</title>
    <link rel="stylesheet" href="main.css">
</head>
<body>
<div id="app">
    <div class="cart">
        <p>Cart (  )</p>
    </div>
    <product :premium="premium" @add-to-cart="updateCart"></product>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="main.js"></script>
</body>
</html>

2020-09-10 15.07.01

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

Forms & v-model

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.

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.

Our app now looks like this:

<div id="app">
        <div class="cart">
          <p>Cart()</p>
        </div>
        <product :premium="premium" @add-to-cart="updateCart"></product>    
      </div>
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></h1>
                <p v-if="inStock">In Stock</p>
                <p v-else>Out of Stock</p>
                <p>Shipping: </p>
      
                <ul>
                  <li v-for="detail in details"></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)
            }
          }
      })

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.

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">
Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart", this.variants[this.selectedVariant].variantID)
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

});

Vue.component('prodoct-review', {
    template: `
    <input v-model="name">
    `,
    data() {
        return {
            name: null
        };
    }
});


var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: []
    },
    methods: {
        updateCart(id) {
            this.cart.push(id)
        }
    }
})

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:

Vue.component("product", {
    props: {
        premium: {
            type: Boolean,
            required: true
        }
    },
    template: `
    <div class="product">
        <div class="product-image">
            <img :src="image" :alt="altText">
        </div>
        <div class="product-info">
            <h1></h1>
            <p>Shipping: </p>
            <p v-if="inStock > 10 ">In Stock</p>
            <p v-else>Out of Stock</p>
            <ul>
                <li v-for="detail in details"></li>
            </ul>
            <div class="color-box"
                 v-for="(variant, index) in variants"
                 :key="variant.variantID"
                 :style="{backgroundColor:variant.variantColor}"
                 @mouseover="updateProduct(index)"
            >
            </div>
        </div>
        <button v-on:click="addToCart"
                :disabled="!inStock"
                :class="{disabledButton:!inStock}"
        >Add to cart
        </button>
    </div>
    `,
    data() {
        return {
            product: 'Socks',
            brand: "AzatAI",
            altText: "A pair of Socks",
            inventory: 100,
            details: [
                "80% cotton",
                "20% polyester",
                "Gender-neutral"
            ],
            variants: [
                {
                    variantID: 2234,
                    variantColor: "green",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg",
                    variantQuantity: 10

                },
                {
                    variantID: 2235,
                    variantColor: "blue",
                    variantImage: "https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.jpg",
                    variantQuantity: 0
                }
            ],
            selectedVariant: 0,
            cart: 0
        }
    },
    methods: {
        addToCart() {
            this.$emit("add-to-cart", this.variants[this.selectedVariant].variantID)
        },
        updateProduct(index) {
            this.selectedVariant = index
            console.log(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"
            } else {
                return 2.99
            }
        }
    }

});

Vue.component('prodoct-review', {
    template: `
    <form class="review-form" @submit.prevent="onSubmit">
        <p>
        <label for="name">Name:</label>
        <input type="text" id="name" v-model="name" placeholder="name">
        </p>
        <p>
        <label for="review">Review:</label>
        <textarea name="review" id="review" cols="30" rows="10" v-model="review"></textarea>
</p>
    <p>
    <label for="rating">Rating:</label>
    <select name="rating" id="rating" v-model.number="rating">
    <option value="">5</option>
    <option value="">4</option>
    <option value="">3</option>
    <option value="">2</option>
    <option value="">1</option>
</select>
</p>
<p>
<input type="submit" value="Submit">
</p>
    </form>
    `,
    data() {
        return {
            name: null,
            review: null,
            rating: null
        };
    },
    methods: {
        obSubmit() {
            let productReview = {
                name: this.name,
                review: this.review,
                rating: this.rating
            }
            this.name = null;
            this.review = null;
            this.rating = null;
        }
    }
});


var app = new Vue({ // initializing the Vue instance and passing some property
    el: '#app', // bind to the id=app in html
    data: {
        premium: true,
        cart: []
    },
    methods: {
        updateCart(id) {
            this.cart.push(id)
        }
    }
})
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:

methods: {
        obSubmit() {
            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></p>
          <p>Rating: </p>
          <p></p>
          </li>
        </ul>
       </div>
//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.

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></h1>
            <p v-if="inStock">In Stock</p>
            <p v-else>Out of Stock</p>
            <p>Shipping: </p>
  
            <ul>
              <li v-for="detail in details"></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>
              <p v-if="!reviews.length">There are no reviews yet.</p>
              <ul v-else>
                  <li v-for="(review, index) in reviews" :key="index">
                    <p></p>
                    <p>Rating:</p>
                    <p></p>
                  </li>
              </ul>
          </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: 'https://www.vuemastery.com/images/challenges/vmSocks-green-onWhite.jpg',
                    variantQuantity: 10
                },
                {
                    variantId: 2235,
                    variantColor: 'blue',
                    variantImage: 'https://www.vuemastery.com/images/challenges/vmSocks-blue-onWhite.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"></li>
          </ul>
        </p>

        <p>
          <label for="name">Name:</label>
          <input id="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>Would you recommend this product?</p>
        <label>
          Yes
          <input type="radio" value="Yes" v-model="recommend"/>
        </label>
        <label>
          No
          <input type="radio" value="No" v-model="recommend"/>
        </label>
            
        <p>
          <input type="submit" value="Submit">  
        </p>    
      
    </form>
    `,
    data() {
        return {
            name: null,
            review: null,
            rating: null,
            recommend: null,
            errors: []
        }
    },
    methods: {
        onSubmit() {
            this.errors = []
            if(this.name && this.review && this.rating && this.recommend) {
                let productReview = {
                    name: this.name,
                    review: this.review,
                    rating: this.rating,
                    recommend: this.recommend
                }
                this.$emit('review-submitted', productReview)
                this.name = null
                this.review = null
                this.rating = null
                this.recommend = 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.")
                if(!this.recommend) this.errors.push("Recommendation required.")
            }
        }
    }
})

var app = new Vue({
    el: '#app',
    data: {
        premium: true,
        cart: []
    },
    methods: {
        updateCart(id) {
            this.cart.push(id)
        }
    }
})

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.

n 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"></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

Tabs

In this lesson, we’ll learn how to add tabs to our application and implement a simple solution for global event communication.

We’ll learn how to create tabs to display our reviews and our review form separately.

<div id="app">
      <div class="cart">
        <p>Cart()</p>
      </div>
      <product :premium="premium" @add-to-cart="updateCart"></product>
    </div> 
    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></h1>
                <p v-if="inStock">In Stock</p>
                <p v-else>Out of Stock</p>
                <p>Shipping: </p>
      
                <ul>
                  <li v-for="(detail, index) in details" :key="index"></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"></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)
            }
          }
      })

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.

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.

We’ll add to this component soon, but so far this is what’s happening:

the template, we’re using v-for to create a span for each string from our tabs array.

img

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
            ></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.

img

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"
              ></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></p>
                      <p>Rating:</p>
                      <p></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.

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"
              ></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></p>
                      <p>Rating:</p>
                      <p></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>
    `

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.

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.