In the previous chapter, we saw the power of the v-model directive and how we can use it to bind inputs to our application. We constructed a checkout page that displayed all the input forms we needed to gather from the user. To display this page, we used a conditional statement.
In chapter 3, we created a checkout button bound to a click event method. This method toggles a property called showProduct. In our template, we used the v-if directive and the v-else directive. If showProduct was true, the product page was displayed; if showProduct was false, the checkout page was displayed. By clicking the checkout button, users can easily switch between these pages. In later chapters, we’ll look at refactoring this code to use components and routes, but for now this will work.
To expand our app, we’ll look at other types of conditionals. For example, we need to add a new feature that displays messages to the user based on available inventory levels. In addition, we need to add more products to our product page. We’ll look at that more closely in section 5.2.
Every time an additional item is added to our shopping cart, the cartItemCount computed property is updated. What if we want to let the user know how many are available? Let’s display a few messages when the available inventory is almost out. We’ll use the v-if, v-else-if, and v-else directives to make this possible.
Before we begin, let’s add more inventory. This will make it easier to display a message to the user as they put more items into their cart. To add inventory, we can update our product property in the data object. Edit the availableInventory product property in index.html. Let’s change it from 5 to 10, as shown in listing 5.1. That should be enough for now.
If you’ve been following along from previous chapters, you should have an index.html file. If not, you can always download the completed chapter 4 index.html file that’s included with this book as a starting point, along with any code snippets and CSS. As always, each listing is broken into its own file. Make sure to add each snippet into index.html as you continue.
product: { id: 1001, title: "Cat Food, 25lb bag", description: "A 25 pound bag of <em>irresistible</em>, organic goodness for your cat.", price: 2000, image: "assets/images/product-fullsize.png", availableInventory: 10 1 },
Now that the inventory has been updated, let’s add a conditional to the template when the available inventory is low. We’ll display a message showing the remaining inventory that the user can add to their cart. In listing 5.2 we can see the new span tag with the v-if directive. A class called inventory-message was also added to this span. This CSS makes the message stand out better and positions it correctly. I’ve added basic formatting to make our message look a little nicer. The v-if directive is flexible. You’ll notice that we aren’t using a specific property the way we did in chapter 3 with showProduct. Instead, we’re using an expression. This is a nice touch that Vue.js allows us to do.
When the Add to cart button is clicked, the checkout number at the top increments. When the inventory reaches fewer than 5 (product.availableInventory – cartItemCount), a message appears that displays the amount of remaining inventory. This count continues to decrement as the button is pressed until the inventory reaches zero.
Locate the addToCart button in our template. Add a new span tag with our v-if directive in the index.html file, as shown here.
<button class="btn btn-primary btn-lg" v-on:click="addToCart" v-if="canAddToCart">Add to cart</button> <button disabled="true" class="btn btn-primary btn-lg" v-else >Add to cart</button> <span class="inventory-message" 1 v-if="product.availableInventory - cartItemCount < 5"> 2 Only {{product.availableInventory - cartItemCount}} left! </span>
Keep in mind the source code, including this app.css file, is available to download for this chapter and all other chapters from Manning at www.manning.com/books/vue-js-in-action.
We could have used a computed property in this v-if directive but for the sake of simplicity, using an expression like this will work. Keep in mind that if your expressions are getting too long inside your template, you’re probably better off using a computed property.
The v-show directive is a close relative of the v-if directive. The use of both is similar: <span v-show="product.availableInventory - cartItemCount < 5">Message</span>. The only real difference is that the v-show directive will always be rendered in the DOM. Vue.js uses a simple toggle in CSS to display the element. If you’re confused about which one to use, use v-if if it’s followed by v-else or v-else-if. Use v-show if it’s more likely to be shown/rendered in most cases or if it’s likely that the visibility of the element will be changed more than once during the lifetime of the page. Otherwise, use v-if.
Let’s look at what we have so far (figure 5.1).
We have a slight problem. When the inventory reaches 0, the message displays “Only 0 left!” Obviously, that doesn’t make sense, so let’s update the code so it outputs a better message when the inventory reaches 0. Add a message that encourages the user to buy now. This time we’ll introduce the v-else-if and v-else directives! Let’s break down what we want to do and how we’ll do it. The v-if directive will display if the inventory count, minus the cart item count, equals zero. If we add all the items to the cart, the product inventory is all gone.
In figure 5.2, we see the completed functionality that shows a message (All Out) when the inventory is sold out.
If the product isn’t sold out, we continue to the v-else-if directive. If the available inventory is close to selling out and fewer than five are left, we’ll display a message, as you can see in figure 5.3.
Figure 5.3 shows the Buy Now! message. When you click the Add to cart button, you should see the message change. When the inventory is fewer than 5, you’ll see figure 5.1. After the inventory runs out, you’ll see figure 5.2.
The last else triggers only if both the v-else and v-else-if are false. The v-else directive is a catch-all when everything else falls through. If this occurs, we want a Buy Now message to appear next to the cart button. Update the span tag we added with the following code in the index.html file.
<button class="btn btn-primary btn-lg" v-on:click="addToCart" v-if="canAddToCart">Add to cart</button> <button disabled="true" class="btn btn-primary btn-lg" v-else >Add to cart</button> <span class="inventory-message" v-if="product.availableInventory - cartItemCount === 0">All Out! 1 </span> <span class="inventory-message" v-else-if="product.availableInventory - cartItemCount < 5"> 2 Only {{product.availableInventory - cartItemCount}} left! </span> <span class="inventory-message" v-else>Buy Now! 3 </span>
When working with v-if, v-else, and v-else-if, we must be aware of a few things. Any time you use v-else, it must immediately follow a v-if or v-else-if. You cannot create extra elements in between. Like v-else, a v-else-if must also immediately follow a v-if or v-else-if element. Failure to do this causes the v-else-if or v-else not to be recognized.
Keep in mind that the v-else-if directive can be used more than once in the same block. For example, in our application we could have included multiple messages when the item was close to selling out. This would have been possible with the v-else-if directive.
With all that said, be careful with using too many conditionals and putting too much logic in the template. Instead, use computed properties and methods when needed. This will make your code easier to read and more understandable.
Since we introduced our pet depot store in chapter 2, we’ve been working with only one product. This has worked well up to now with the examples in previous chapters, but once we add more products, we’ll need a way to display them all in our template. In addition, we want to show a simple star rating at the bottom of each product. The versatile v-for directive can handle both scenarios.
Vue.js has the v-for directive to loop through items, as we saw briefly in chapter 4. It’s worth mentioning that we can use it with objects, arrays, or even components. One of the simplest ways of using the v-for directive is by giving it an integer. When added to an element, it will be repeated that many times, which is sometimes referred as a v-for range.
Let’s begin by adding a five-star rating system to our product. To keep it simple, let’s use a span tag and add the v-for directive to it. The v-for syntax is always in the form of item in items. The items refers to the source array data being iterated on. The item is an alias element being iterated on. As you can see in figure 5.4, the v-for directive uses the item as an alias of items. The items is an array.
When using v-for ranges, the source data is the upper inclusive limit of the range. This signifies how many times an element will be repeated. In figure 5.5 you can see that n is iterated on five times.
In listing 5.4, we’re going to repeat the symbol five times. We also add in a div with a class called rating. (Remember to download the ap.css for this book; you’ll find more information in appendix A.) Add this span as shown in the following listing inside the index.html below the inventory messages we added in section 5.1.
<span class="inventory-message" v-else>Buy Now! </span> <div class="rating"> <span v-for="n in 5"></span> 1 </div>
Once we add the stars to the template, refresh your browser. It should look like figure 5.6.
As you can see, there isn’t much to show with our star rating: each star is empty. We’ll need a way to dynamically bind a class to our CSS so we can show a filled-in star.
Vue.js gives us a way to dynamically add or remove classes to HTML elements in our template. We can pass data objects, arrays, expressions, methods, or even computed properties to help determine which classes appear.
Before we can get started, we need to edit the product data object property and add a rating. This rating will determine how many stars each product should display. Open the index.html file and find the product property under order. Add the rating to the bottom of the product property, as shown in the following listing.
product: { id: 1001, title: "Cat Food, 25lb bag", description: "A 25 pound bag of <em>irresistible</em>, organic goodness for your cat.", price: 2000, image: "assets/images/product-fullsize.png", availableInventory:10, rating: 3 1 },
Next, we need to display the star rating on the screen. The easiest way of doing this is with CSS and a little bit of JavaScript. We’ll add simple CSS that creates a black star when the class is added to our span element. For our example, we’ll need the first three stars to appear black. The last two remaining stars will appear as white. Figure 5.7 is an example of what it should look like when we’re done.
As I mentioned, we can use a method to help determine whether a class should appear. Because we’re using a v-for range, we’ll need to pass in the range to a method. Let’s add a new method that reads the rating from the product and then returns true if the class should be added or not to the span. The class will make the star black.
To make this work, we must pass in a variable n to the method. The passed-in variable n comes from the v-for range directive <span v-for="n in 5"></span>. Although we didn’t display it in our template, n increments from 1 to 5. The first loop’s n is 1, the next loop’s n is 2, and so on. We know that as n loops it will increment from 1, 2, 3, 4, to 5. We can use simple math to determine if the star should be filled in.
In our example the first iteration n should be 1 and this.product.rating will always be 3. 3 – 1 = 2 is greater or equal to 0 so we return true and the class is added. The next iteration n will be 2. 3 – 2 = 1 is greater than or equal to 0 so it evaluates to true again. The next iteration n will be 3. 3 – 3 = 0, so the class is added again. The next iteration n will be 4. 3 – 4 = -1 and therefore the method returns false. It’s that simple. Add a new method called checkRating at the top of the method object in the index.html file, as shown here.
methods: { checkRating(n) { return this.product.rating - n >= 0; 1 },
To put our new star rating all together, we need to add the v-bind:class syntax to our span element. It will add our new class, rating-active, if the method returns true. Otherwise it will be ignored. In this example, we’re passing an object to the v-bind:class. The truthiness of the checkRating method will determine if the rating-active class is added. Because this is in a loop, we must also pass in the value n, as we discussed before, which iterates with every loop.
Update the rating span in index.html and add the new v-bind:class directive to it, as shown in the next listing. Make sure to add the quotes around rating-active. Otherwise, you’ll get an error in the console.
<span class="inventory-message" v-else>Buy Now! </span> <div class="rating"> <span v-bind:class="{'rating-active': checkRating(n)}" 1 v-for="n in 5"> </span> </div>
These are the basics of binding HTML classes. Vue.js allows you to add multiple classes and use arrays and components. For more information on how to bind classes, check out the official Vue.js guides on classes and style bindings at https://vuejs.org/v2/guide/class-and-style.html.
Until now we’ve worked with only one product. A real pet store app would have hundreds if not thousands of products. We won’t go that far! Let’s see what it takes to add in five new products and what we could use to loop through those products on our product page.
To get started, we’ll look at our product object. It’s already taking up space in our index.html file and at this point it will be easier to put it in a separate file.
We need to create a new products.json file and add it to the chapter-05 folder. That way, we can more easily organize our data from our main application. If you want, you can add your own products, the way you did in the data object. But if you don’t want to type all this, you can grab the products.json file from the code that’s included with the book and copy it to the chapter-05 folder. You can find instructions on how to download the code for this book in appendix A. This listing shows the products in the products.json file.
{ "products":[ 1 { "id": 1001, 2 "title": "Cat Food, 25lb bag", "description": "A 25 pound bag of <em>irresistible</em>, organic goodness for your cat.", "price": 2000, "image": "assets/images/product-fullsize.png", "availableInventory": 10, "rating": 1 }, { "id": 1002, 3 "title": "Yarn", "description": "Yarn your cat can play with for a very <strong>long</strong> time!", "price": 299, "image": "assets/images/yarn.jpg", "availableInventory": 7, "rating": 1 }, { "id": 1003, 4 "title": "Kitty Litter", "description": "Premium kitty litter for your cat.", "price": 1100, "image": "assets/images/cat-litter.jpg", "availableInventory": 99, "rating": 4 }, { "id": 1004, 5 "title": "Cat House", "description": "A place for your cat to play!", "price": 799, "image": "assets/images/cat-house.jpg", "availableInventory": 11, "rating": 5 }, { "id": 1005, 6 "title": "Laser Pointer", "description": "Drive your cat crazy with this <em>amazing</em> product.", "price": 4999, "image": "assets/images/laser-pointer.jpg", "availableInventory": 25, "rating": 1 } ] }
After you’ve added or downloaded the products.json file and moved it to the chapter 5 root folder, you’ll need to do additional refactoring. If you’re following along, there’s a good chance you’re loading up everything locally from your hard drive instead of using a web server. This is perfectly fine and works great, except for one thing. Due to security concerns from browser creators, we can’t easily load our products.json file. To do this right, we need to create a web server.
When you run a site using a local web server, it can load JSON files from your hard drive with no problem, and you won’t have any security concerns. In later chapters, we’ll use the Vue CLI. This command-line tool will create a web server for us. Until we get there, we can use an npm module called http-server. You can find instructions on how to install npm in appendix A. This lightweight module makes it a snap to create a simple web server for our app.
We’ll use npm to create a web server. Open a terminal window and run the following command from the command prompt to install the http-server module:
$ npm install http-server -g
After the installation completes, change directories to the chapter 5 folder. Run the command to start a server running your index.html on port 8000:
$ http-server -p 8000
If you receive any errors after running this command, verify that you don’t have any other programs running on port 8000. You may want to try 8001 as the port number instead.
Once it starts, open your favorite web browser and head over to http://localhost:8000 to display your web page. If the page doesn’t display, double-check the command line for errors. You may want to try changing the port if port 8000 is already taken.
Remember in chapter 2 when we learned about Vue.js lifecycle hooks? We need to load our JSON file as soon as the webpage loads. One of those hooks would be perfect in this situation. Do you know which one? If you said the created lifecycle hook you are correct! The created lifecycle hook is called after the instance is created. We can use this hook to load the JSON file. To make this work we’ll need another library.
Axios is a promise-based HTTP client for the browser and Node.js. It has several helpful features such as automatic transforms of JSON data that will come in handy. Let’s add this library to our project. Inside the index.html, add a new script tag for Axios in the head tag, as shown in the following listing.
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css " integrity="sha384- BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous"> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"> </script> 1 </head>
After this tag is added, we can use Axios in our created lifecycle hook. Insert the created hook in the index.html file right after the filters object. We’ll need to add in code that retrieves the products.json file from the hard drive and overwrites our existing product data. Update the index.html and add the Axios code.
... }, created: function() { axios.get('./products.json') 1 .then((response) =>{ this.products=response.data.products; 2 console.log(this.products); }); },
The axios.get command takes in a location, in our case the local file. It then returns a promise that has a .then method. The promise is fulfilled or rejected and returns a response object. Per Axios documentation, this object has a data property. We copy the response.data.products reference to this.products (this refers to the Vue instance). To make sure everything is okay, we also console-logged the output.
If you look closely at the code in listing 5.10, you may realize that we’re assigning the data from the JSON file to this.products, not this.product. We need to create a new products property on our data object because it helps clean up the code.
Open the index.html file and look for the data object near the middle of the file. Add the new products property and then replace the product property because we no longer need it, as shown in the following listing.
business: 'Business Address', 1 home: 'Home Address', gift:'', sendGift: 'Send As A Gift', dontSendGift: 'Do Not Send As A Gift' }, products: [], 2
At this point, if you try to refresh your browser you’ll get an error because we removed the product object. We’ll fix that when we add the v-for directive to loop through all the products.
Before we can start looping through our products, we’ll need to make slight changes to the div classes that control our CSS. Because we’re using Bootstrap 3, we want each row to be a product because we must now accommodate more than one product. When we get done, it will look like figure 5.8.
Update index.html and locate the v-else directive that displays the checkout page. Add another div tag for a new row, as shown in the following listing.
<div v-else> <div class="row"> 1
We’ll need to move the div with the class row that’s right before the showProduct v-if directive. Move the div class to the position below showProduct, as seen in the following listing. Update the index.html so it matches.
<div v-if="showProduct"> <div class="row product"> 1
Now that we have the minor issues with the CSS/HTML resolved, we can add our v-for directive that loops through all the products. This will display all the products on our page. In our example, we’ll use the syntax product in products. products is the object that we loaded earlier; it is now an alias to each individual product in products. We’ll also update the column widths using Bootstrap so that our products are displayed a little nicer.
Inside index.html, add the v-for directive below the showProduct v-if directive. Make sure to end the div tag at the bottom of the page, as shown here.
<div v-if="showProduct"> <div v-for="product in products"> 1 <div class="row"> <div class="col-md-5 col-md-offset-0"> 2 <figure> <img class="product" v-bind:src="product.image"> </figure> </div> <div class="col-md-6 col-md-offset-0 description"> 3 ... </div><!-- end of row--> <hr /> 4 </div><!-- end of v-for--> 5 </div><!-- end of showProduct-->
We’ve added the v-for directive but we have a few small problems. The checkRating method and the canAddToCart computed property are still referencing this.product. We need to change this so it references the this.products array instead.
This can be a little tricky. Let’s begin by fixing the checkRating method. This method helps us determine how many stars each product has. We can fix it by passing the product alias into the method. Inside index.html, update the checkRating method, as shown here.
methods: { checkRating(n, myProduct) { 1 return myProduct.rating - n >= 0; },
We now need to update the template and pass in the product to our updated method. Update the index.html and look for the checkRating method below the inventory messages. Add product to the checkRating method, as shown next.
<span class="inventory-message" v-else>Buy Now! </span> <div class="rating"> <span v-bind:class="{'rating-active': checkRating(n, product)}" 1 v-for="n in 5" ></span> </div>
If you haven’t done so, grab the pictures from the chapter in the assets/images folder and copy them to your local assets/images folder. Also grab the app.css file, if you haven’t already, and copy it to your assets/css folder.
One of the last things we need to do to finish refactoring our app is to fix the canAddToCart computed property. This property greys out the Add to Cart button after the available inventory exceeds the amount in the cart.
You may be wondering how we can accomplish this. Before, we had only one product, so it was easy to figure out if that product’s inventory had been exceeded. With multiple products, we need to loop through every product in the cart and calculate whether or not we can add another item.
This is easier than you might think. We need to move the canAddToCart computed property and make it a method. Then we need to update the method so it can accept a product. Finally, we’ll update the conditional so it retrieves the count.
To retrieve the count, we’ll use a new method called cartCount that accepts an ID and returns the number of items for that ID. The cartCount method uses a simple for loop to iterate through the cart array. For every match, it increments the count variable. It then returns that variable at the end.
Update index.html with the new canAddToCart method. You can move it from the computed property section to methods. Create a cartCount method as well.
canAddToCart(aProduct) { return aProduct.availableInventory > this.cartCount(aProduct.id); 1 }, cartCount(id) { 2 let count = 0; for(var i = 0; i < this.cart.length; i++) { 3 if (this.cart[i] === id) { count++; } } return count; }
To complete our updates to canAddToCart, we must update our template and pass the product to it. At the same time, let’s update the addToCart method and make sure it also accepts a product. Update index.html and pass in the product alias to the canAddToCart and addToCart methods.
<button class="btn btn-primary btn-lg" v-on:click="addToCart(product)" 1 v-if="canAddToCart(product)">Add to cart</button> 2
This was a simple update to both methods. Because we’ve updated the template to addToCart, we must also update the method to push the ID of the product. For this we’ll use the push mutation method, shown in listing 5.19.
Vue has many mutation methods that you can use with your arrays. Vue, by convention, wraps arrays in observers. When any changes occur to the array, the template is notified and updated. Mutation methods mutate the original array they’re called upon. These mutation methods include push, pop, shift, unshift, splice, sort, and reverse.
Be careful: there are certain changes to arrays that Vue cannot detect. These include directly setting an item, this.cart[index] = newValue and modifying the length, this.item.length = newLength. To learn more about mutations, see the official guide at https://vuejs.org/v2/guide/list.html#Mutation-Methods.
addToCart(aProduct) { this.cart.push( aProduct.id ); 1 },
Now we can click the Add to cart button without any problems. Every time the button is clicked, the product ID will be pushed into the cart and the cart count will be updated automatically at the top of the screen.
The last step of our refactor is to fix the product inventory messages that we created earlier. The problem is that we’re still using the total cart item count to determine which messages to display. We need to change this code so that we now calculate the message based on the cart item count of only that item.
To fix this issue, let’s change from the cartItemCount method to our new cartCount method, which will accept a product ID. Update the index.html and locate the inventory messages. Update the v-if and v-else-if directives with the new expression that uses cartCount, as shown here.
<span class="inventory-message" v-if="product.availableInventory - cartCount(product.id) === 0"> 1 All Out! </span> <span class="inventory-message" v-else-if="product.availableInventory - cartCount(product.id) < 5"> 2 Only {{product.availableInventory - cartCount(product.id)}} left! </span> <span class="inventory-message" v-else>Buy Now! </span>
That’s it! We can now load our page and see the results. Make sure that you have the http-server running http-server -p 8000 and reload your web browser. You should see the updated webpage with all the items listed that were pulled from the products.json file. Figure 5.9 shows our completed refactored app using the v-for directive to loop through our products object.
In your browser, make sure that everything works as you expect. Click the Add to cart button and see the messages change. Verify that the button is disabled when the item count reaches zero. Try to change the products.json file and reload the browser. Everything should update accordingly.
Often when working with arrays, or objects in our case, you may want to sort the values when displaying them using the v-for directive. Vue makes this easy to do. In our case, we’ll need to create a computed property that returns our sorted results.
In our app, we load the product list from the JSON file. The order displayed matches the order in the file. Let’s update the sort order so products are listed alphabetically by product title. To do this, we’ll create a new computed property called sortedProducts. We first need to update the template.
Update the index.html file and find the v-for directive in the template that lists our products. Change the v-for to use sortedProducts instead of the products object.
<div v-if="showProduct"> <div v-for="product in sortedProducts"> 1 <div class="row">
Now that we’ve sortedProducts in place in the template, we need to create the computed property. But we’ve a problem to solve. We’ll need to be aware that the data for this.products may not be available right away because information from the products.json file is loaded from a promise in the create lifecycle hook as the app loads. To make sure this isn’t a problem, we’ll surround our code with an if block that verifies the products exist.
Let’s define our own compare function that will sort on the title, as shown in the following listing. Then we’ll use JavaScript’s array sort, with our passed-in compare function, to alphabetically sort by title.
sortedProducts() { if(this.products.length > 0) { let productsArray = this.products.slice(0); 1 function compare(a, b) { 2 if(a.title.toLowerCase() < b.title.toLowerCase()) return -1; if(a.title.toLowerCase() > b.title.toLowerCase()) return 1; return 0; } return productsArray.sort(compare); 3 } }
That should do it. Refresh the browser and you should see an alphabetical list of all the products by title. Figure 5.10 shows the output of our sorted array. If you scroll down, all products should be listed. Double-check to verify that the functionality works as expected.
Use your knowledge from this chapter to answer this question.
See the solution in appendix B.