Building an Online Store with Vue CLI — Part 4

Actions & mutations, component architecture, and v-if

Nathan Magyar
9 min readJan 16, 2019

Part 1 | Part 2 | Part 3 | Part 4:

Previously we added product detail pages to our app and learned about state management, routes, getters, and mixins. In this segment, we’ll add the following:

  • a new “Cart” page component that displays products the user selects
  • actions and mutations to update the store when a product is added to or removed from the cart
  • a badge component that appears/updates when an item is added to the user’s cart

Step 1: Create the Cart page and route

Make a new file in the “views” directory and name it Cart.vue. Add a page heading in the template (line 3) and component name (line 9).

Update router.js. Import the “Cart” component you just made (line 5 below), then create a new route object inside the routes array (lines 21–25). Make sure it has “path”, “name” and “component” properties. You can also remove the import statement and route object for the “About” page:

In App.vue, change the “About” route to point to the new “Cart” component (line 7):

The “Home” and “Cart” pages should respectively look like this in your browser (visit localhost:8080/):

Homepage with new Cart component and route
Cart component view

If you don’t have your development server running, activate it with “npm run serve” or “yarn serve” in your terminal. Make sure you’re in the “online-store” directory first.

Step 2: Display cart data

When a user adds an item to her cart, she might think she’s adding the whole object to the “cart” property of our store state. However, to make our code more efficient and to keep our product data in just one place, we’ll do something different whenever a user performs this action.

We’ll simply push the id of the product she added into our “cart” array, then use a getter to retrieve the rest of the product information for the “Cart” page.

Let’s assume a user has already added one item to her cart. Store.js should look like this (line 8):

The “cart” array has the id of one product in our “products” array.

Now, at the bottom of store.js, write a getter named “cartItems” that maps the array of id’s into their respective product objects like so (lines 19–25). Read more about javascript’s built-in map method here. In short, map() allows us to “map,” or transform, an array’s contents into something else (like mapping a product id into the entire product object itself):

All getters require the “state” object as an argument, so we pass this in first on line 19. Our getter then has to perform two steps:

  1. Map over/iterate through the cart array
  2. For each itemId in the cart, it should look in the products array to find and return the product whose id is a match

Fortunately for us, Javascript comes with a find()method that does exactly what we need. According to MDN, “The find() method returns the value of the first element in the array that satisfies the provided testing function. Otherwise undefined is returned.” For us, our testing function is whether the itemId matches the product id. We can use this method because our id’s are unique, so we never have to worry about only finding the first element that satisfies our testing function. There will only ever be one product with each id.

Now update Cart.vue to have a “cartItems” computed property. It should look very similar to the “products” computed property we created for the homepage, only this time it should return the “cartItems()” getter (lines 16–18). In the template, add a v-for template loop that displays the product names (line 5):

Step 3: Create “Add to Cart” functionality

Users should be able to add an item to their cart on each product detail page.

First add a button to the Product.vue template that the user can click (line 8 below). Note the “@click” attribute that specifies the method (function) we’ll soon use to carry out the add-to-cart functionality. Think of it as the event handler, similar to traditional HTML’s attribute “onclick=’myFunction()’”. “@” is shorthand for “v-on:”, so it’s part of Vue syntax that indicates we’re using some sort of component-related property or method:

Optional: add the following styles to the <style> section in Product.vue:

.btn {
padding: .5rem .75rem;
border-radius: 3px;
border: none;
background-color: transparent;
font-size: .9rem;
font-weight: bold;
cursor: pointer;
transition: all .15s ease;
}
.btn--grey {
background-color: #2c3e50;
color: #FFF;
&:hover, &:focus {
background-color: #42b983;
}
}

Next we’ll start setting up the pieces that will ultimately update the “cart” array in store.js. These pieces are:

  1. Use a method in our Product.vue to dispatch an action.
  2. Create an action in store.js that commits a mutation, also in store.js
  3. Create a mutation that updates the cart array in store.js

Start by adding a methods section to Product.vue with a function called “addToCart”. The function (lines 37– 40) should dispatch an action that is also named “addToCart,” for clarity and consistency. Besides specifying the action name, the dispatch method should also pass in the product id that we want to eventually save in the carts array. Do so by passing the id in as an argument from the $route’s params (line 39), just like we did for the “products” computed property:

So we’ve dispatched the action from Product.vue, but it doesn’t exist yet. Open store.js and write it in the “actions” section:

The action above accepts two arguments, “{ commit }” and payload. “{ commit }” is needed to move onto the next step, committing a mutation. Don’t worry too much about understanding more than that for now. If you do want to learn more, visit this page. The second argument, “payload,” is a standard term used to describe data that’s getting passed around; in this case, that data is the product id. With these arguments passed in, we’re able to use a method called “commit” to trigger two mutations, both of which need the payload / product id: “addToCart” and “decrementProductInventory”.

Finally, create these mutations in the “mutations” block. “addToCart” will add the product id to the “cart” array and make sure that a number is stored. “decrementProductInventory” will find the correct product using the given id and subtract 1 from its “quantity” property.

Besides the payload, mutations also need the “state” object as an argument. Inside the “addToCart” mutation, use Javascript’s “push()” method to add the product id to the “cart” array. Note: be sure to convert the payload/product id back into a Number, as it got “coerced” (turned into a String) at some point by Javascript’s default behavior. We want to store the id’s as Numbers in the cart so that they are the same type as the id’s of the products in the “products” array.

In “decrementProductInvetory”, use the “find” method to pick out the correct product with an id that matches our payload (a product id). Line 22 uses decrementing shorthand to subtract one from the value stored at “product.quantity”.

That should be it! Visit a product detail page, click the button, then check to see if that product’s name appears as the second item in the list on the “Cart” page.

Step 4: Create “remove from cart” functionality

Now that a user can add products to her cart, she should be able to remove them. We’ll follow a similar series of steps that we just completed:

  1. Flesh out the item display on “Cart” page, adding a button for removing each item
  2. Add a method to the “Cart” component that dispatches an action, passing in the product id to start the removal process
  3. Write an action that commits two mutations, passing along the product id. One mutation removes the product id from “cart” while the other increments the product’s quantity back up by 1
  4. Write the mutations just described

The following Cart.vue file adds more details for each cart item and some styling, as well as the “removeFromCart” method (if you are confused about the “makeImagePath” method/mixin, check out Part 3 of this series):

Store.js then needs a new action and two new mutations:

  • The action is called “removeFromCart” (lines 107–110) and it commits two mutations, “removeFromCart” and “incrementProductInventory”
  • The “removeFromCart” mutation (lines 89–92) removes the id from the “cart” array. Unfortunately, Javascript doesn’t have a straightforward “remove” method. Instead, deleting something from an array takes two steps, each performed by one of Javascript’s built-in methods: first, indexOf, which returns the first index of a given value in an array; and second, splice, which takes the index at which we want to start deleting items, and the number of array items to remove)
  • The “incrementProductInventory” (lines 97–100) mutation works similar to “decrementProductInventory” only it adds 1 instead of subtracting 1 (I smell an opportunity for refactoring in the future!):

After this, the “Remove” button on the cart items should work!

Step 5: Add a cart notification badge

So far, when a user adds an item to her cart, the item quantity updates on the detail page, but a more noticeable confirmation would provide a better user experience. Let’s add a notification badge component to our navigation that will appear/update when the cart changes. To do so, we will:

  1. Make a new component called CounterBadge
  2. Import it to App.vue, register it, and use it in the template

Inside the “components” directory, make a new file called CounterBadge.vue and write the following:

The above code creates a green circle with a number inside it, represented by the computed property “count.” In order to make this component as reusable as possible, I’ve declared “count” as a prop, a piece of data that will get passed in from a parent component. We could define a computed property called “count” in this component, returning the length of the cartItems array in store.js. But, what if we want to use this component to display counts of other things in the future, such as messages, product quantities, days left in a sale, etc. That’s why “count” should be a prop that gets passed. It can then be whatever we need it to be!

The v-if statement on line 2 will conditionally show or hide the <span> element, depending on whether or not “count” is greater than 0. If “count” is 1 or greater, the component will show. If it’s 0 or less, it will hide. The CSS styles take care of the visual styles and positioning, respective to the parent component, which we’ll look at next.

Inside App.vue:

  1. Import the “CounterBadge” component (line 18 below)
  2. Register it to be used in the template (line 22)
  3. Nest it inside the “cart” router-link element (line 9)
  4. Create a computed property called “cartCount” that will return the number of id’s in the “cart” by evaluating the array’s length (lines 23–27)
  5. Pass “cartCount” into “CounterBadge” as a prop value. Remember how we registered “count” as a prop within “CounterBadge”? To pass data in as that prop, we have to use the same name (“:count”) as a dynamic attribute on the component in the parent template, hence
<counter-badge :count="cartCount"></counter-badge>

Note the use of the “:” again before “count”, which lets us pass in the returned value of the cartCount computed property. Without it, “count” will equal the string “cartCount”.

The homepage should now look like this:

When you click “Add to Cart” on a product detail page, the badge should update by one! And when you remove an item on the “Cart” page, it should decrease by one, and disappear if there are no items in the user’s cart.

Recap

We got a lot done in this stage!

  • We created a new “Cart” page for a user to see items they want to buy
  • We enabled users to add and remove items from the cart through various actions and mutations
  • We practiced creating and using new components while simultaneously improving the app’s user experience. Now users will receive multiple forms of confirmation that their action was successful

Next time we’ll do more review and practice of the concepts we’ve covered so far by adding to the homepage and product detail pages. See you then! 👋

--

--

Nathan Magyar

User Experience Designer and Front End Developer, University of Michigan Office of Academic Innovation