Vue.js create cart

Overview

The goal of this guide is to demonstrate how to add cart functionality to your products page so multiple products can be added to a cart, increase or decrease the items, and also clear items from the cart.

Below is what we will be accomplishing with this guide:

  1. Retrieve and/or create a cart in our application
  2. Add products to cart
  3. Update line items in cart
  4. Remove line items from cart
  5. Empty cart contents

See live demo

vue.js cart

Requirements

To start this guide you will need:

  • An IDE or code editor
  • NodeJS, at least v8/10
  • npm or yarn
  • Vue.js devtools (recommended)

Prerequisites

This project assumes you have some knowledge of the below concepts before starting:

  • Basic knowledge of JavaScript
  • Some knowledge of Vue.js
  • An idea of the Jamstack architecture and how APIs work

Some things to note:

  • The purpose of this guide is to focus on the Commerce.js integration and using Vue.js to build out the application, we will therefore not be covering any styling details.
  • The cart application code is available in the GitHub repo along with all styling details.

Add cart functionality

The cart resource in Chec comes equipped with multiple intuitive endpoints to help develop a seamless shopping cart experience with Commerce.js. You will be interacting with the cart endpoint in multiple components in your application:

  • In your product listing page where you can add items to the cart
  • In the cart component where you will be rendering, updating, removing, and clearing the cart items.

1. Retrieve cart

First, go to your app component and pick up where we left off from the previous guide. Follow the same logic to fetch your cart data when your application mounts to the DOM, the same as fetching your products. Inside your data where the initial state gets stored, add an initial cart state and set it to null.

// App.js

data() {
  return {
    products: [],
    cart: null,
  }
},

Next, retrieve the current cart in session with the cart.retrieve() method. Commerce.js automatically creates a cart for you if one does not exist in the current browser session. Commerce.js tracks the current cart id with a cookie and stores the entire cart and its contents for 30 days. The cart is being handled clientside as we want to utilize cookie sessions to store the cart data. For instance, a user is shopping on a commerce website and starts to add cart items, he/she leaves the website and returns again, the expected behaviour would be that the cart data would persist and cart items still remain. With the Cart API endpoint and cart functionality methods in Commerce.js, the otherwise complex cart logic on commerce websites can be easily implemented. Now you should add a new cart method underneath fetchProducts().

// App.js

/**
 * Retrieve the current cart or create one if one does not exist
 * https://commercejs.com/docs/sdk/products
 * 
 * @return {object} cart object
 */
fetchCart() {
  this.$commerce.cart.retrieve().then((cart) => {
    this.cart = cart;
  }).catch((error) => {
    console.log('There is an error fetching the cart', error);
  });
}

Above, you created a new helper function called fetchCart() that will call the cart.retrieve() Commerce.js method to retrieve the cart in session or create a new one if one does not exist. When this method resolves, set the returned cart data object to the cart state. Otherwise, handle a failed request with an error message. In our created() hook, you'll need to then call the fetchCart() when the application mounts.

// App.js

created() {
  this.fetchProducts();
  this.fetchCart();
},

The cart.retrieve() method will run, resolve, and the returned data will be stored in the cart state. Once you save and refresh your browser upon a successful call to the Cart API endpoint, your result should be similar to the cart object response below:

{
  "id": "cart_Mo11bJPOKW9xXo",
  "created": 1599850065,
  "last_updated": 1599850065,
  "expires": 1602442065,
  "total_items": 0,
  "total_unique_items": 0,
  "subtotal": {
    "raw": 0,
    "formatted": "0.00",
    "formatted_with_symbol": "$0.00",
    "formatted_with_code": "0.00 USD"
  },
  "currency": {
    "code": "USD",
    "symbol": "$"
  },
  "discount_code": [],
  "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Mo11bJPOKW9xXo",
  "line_items": []
}

2. Add to cart

In Commerce.js, one of the main cart methods is add to cart. This method calls the POST v1/carts/{cart_id} Cart API endpoint. With your cart object response, you can start to interact with and add the necessary event handlers to handle your cart functionalities. Similar to how you can pass props as custom attributes, you can do that with native and custom events. Because you will need to display a button to handle your add to cart functionality, go back to the ProductItem.vue component to add that in the product card. The built-in v-on directive has a click event v-on:click which you can write as a shortform @click in Vue.js. Next, create a button tag and bind an expression to the directive which is the function handler you want to handle your event, a button click in this case. Name your custom callback function addToCart().

<!-- ProductItem.vue -->

<button
  class="product__btn"
  @click="addToCart"
>
  Quick add
</button>

In Vue.js, data being passed down from a parent component to a child component is called props. When a child component needs to pass data or propagate an event upstream to its parent, its called emitting. After attaching a click event in your Quick add button to call the addToCart() event handler, emit this logic to the next parent component. At this stage the application is still fairly small so sharing data between components via props and events is manageable. As your application scales, you will need to handle the application state and events with a state management layer like Vuex.

Getting back to your ProductItem.vue component in your methods property, attach your event handler's logic. The first argument you need to pass into the $emit() function is the event you want the parent component to listen to and additionally any data you are passing up. In order for Commerce.js to handle adding of items into the cart, pass in the product ID and the quantity of that product.

// ProductItem.vue

methods: {
    addToCart() {
      this.$emit('add-to-cart', this.product.id, 1);
    }
}

Because the ProductItem.vue component is a child component of ProductsList.vue, pass up the event through the ProductItem component instance as a custom event attribute. Like the native click event, use the custom event @add-to-cart to continue to emit the event back up to your outer most parent component, App.js.

<!-- ProductList.vue -->

<ProductItem
  v-for="product in products"
  :key="product.id"
  :product="product"
  @add-to-cart="$emit('add-to-cart', $event)"
  class="products__item"
/>

Finally in the outer most component App.js, pass in your callback add-to-cart as an event and attach a handleAddToCart() method where you will be making the cart request to the Chec API.

<!-- App.js -->

<template>
  <ProductsList
    :products="products"
    @add-to-cart="handleAddToCart"
  />
</template>

The data productID and quantity that were emitted from the ProductItem component will be received in the handling method. Go ahead and create the helper handling method and call it handleAddToCart(). You will also need to pass in the required parameters productID and quantity of the product item. Destructure the variables as these will be properties that will resolve into the cart object.

/**
 * Adds a product to the current cart in session
 * https://commercejs.com/docs/sdk/cart/#add-to-cart
 *
 * @param {string} productId The ID of the product being added
 * @param {number} quantity The quantity of the product being added
 */ 
handleAddToCart(productId, quantity) {
  this.$commerce.cart.add(productId, quantity).then((item) => {
    this.cart = item.cart;
  }).catch((error) => {
    console.log('There is an error fetching the cart', error);
  });
}

Make a call to the commerce.cart.add endpoint passing in the neccessary data and when the promise resolves, the cart object should return with the appropriate added product details. Upon a successful post request to add a new product to cart, you should see the below abbreviated response with a new line item in the cart object:

{
  "success": true,
  "event": "Cart.Item.Added",
  "line_item_id": "item_dKvg9l6vl1bB76",
  "product_id": "prod_8XO3wpDrOwYAzQ",
  "product_name": "Coffee",
  "media": {
    "type": "image",
    "source": "https://cdn.chec.io/merchants/18462/images/2f67eabc1f63ab67377d28ba34e4f8808c7f82555f03a9d7d0148|u11 1.png"
  },
  "quantity": 1,
  "line_total": {
    "raw": 7.5,
    "formatted": "7.50",
    "formatted_with_symbol": "$7.50",
    "formatted_with_code": "7.50 USD"
  },
  "_event": "Cart.Item.Added",
  "cart": {
    "id": "cart_Ll2DPVQaGrPGEo",
    "created": 1599854326,
    "last_updated": 1599856885,
    "expires": 1602446326,
    "total_items": 3,
    "total_unique_items": 3,
    "subtotal": {
      "raw": 66.5,
      "formatted": "66.50",
      "formatted_with_symbol": "$66.50",
      "formatted_with_code": "66.50 USD"
    },
    "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Ll2DPVQaGrPGEo",
    "line_items": [
      {
        "id": "item_7RyWOwmK5nEa2V",
        "product_id": "prod_NqKE50BR4wdgBL",
        "name": "Kettle",
        "media": {
          "type": "image",
          "source": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png"
        },
        "quantity": 1,
        "price": {
          "raw": 45.5,
          "formatted": "45.50",
          "formatted_with_symbol": "$45.50",
          "formatted_with_code": "45.50 USD"
        },
        "line_total": {
          "raw": 45.5,
          "formatted": "45.50",
          "formatted_with_symbol": "$45.50",
          "formatted_with_code": "45.50 USD"
        },
        "variants": []
      }
    ]
  }
}

In the json response, you can note that the added product is now given associated line_items details such as its line_item_id, and line_total. With this data, you can now create your cart component and render out cart details like a list of added items.

3. Create a cart component

Start by creating a cart item component in the components folder. Here you will want to follow the same pattern to try to encapsulate small components to be consumed by bigger components. This way you continue to keep your application dry as well and keeping your logic separated and clean.

In your components folder, create a CartItem.vue which will render each line item details such as the item image, name, price and quantity. First between your script tag, name your component and define an item prop:

export default {
    name: 'CartItem',
    props: ['item'],
}

Then in your template, bind the necessary data to eventually parse the values when the cart component renders.

<!-- CartItem.vue -->

<template>
    <div class="cart-item">
        <img class="cart-item__image" :src="item.media.source" />
        <div class="cart-item__details">
            <h4 class="cart-item__details-name">{{ item.name }}</h4>
            <div class="cart-item__details-qty">
              <p>{{ item.quantity }}</p>
            </div>
            <p class="cart-item__details-price">{{ item.line_total.formatted_with_symbol }}</p>
        </div>
    </div>
</template>

Next, create the main Cart.vue component. Once again, name your component, define a cart prop and import and register our child cart component CartItem.vue within your script tag.

// Cart.vue

import CartItem from './CartItem';

export default {
    name: 'Cart',
    components: {
        CartItem,
    },
    props: ['cart'],
}

Within your template tags, render your CartItem.vue component that you registered and pass in the neccessary prop attributes.

<!-- Cart.vue -->

<CartItem
  v-for="lineItem in cart.line_items"
  :key="lineItem.id"
  :item="lineItem"
  class="cart__inner"
/>

Similar to how you have rendered out a list of products in the previous guide, now loop through each of the cart items and render out each line item details. The v-for directive will render each lineItem in the cart.line_items array and parse out the data that was declaratively bound in the CartItem.vue component.

For the main cart component to render in your application, call the cart component instance in App.js and pass in the cart prop.

<!-- App.js -->

<Cart
  :cart="cart"
/>

At this stage, you should be able to see a minimal cart component rendered out in your main application view. Let's continue to add more cart functionalities and build out a more detailed cart interface.

4. Update cart items

Going back to the CartItem.vue component, you can start to implement the first cart line item action using the Commerce.js method commerce.cart.update(). This request uses the PUT v1/carts/{cart_id}/items/{line_item_id} endpoint to update the quantity or variant for the line item ID in the cart. For this guide, you will only be working with the main variant of the product item.

Add a new method in your CartItem.vue component to pass up to the parent Cart.vue component.

// CartItem.vue

methods: {
  updateQuantity(quantity) {
      this.$emit('update-quantity', this.item.id, quantity);
  },
}

Above, you created a helper function updateQuantity() to update the line item quantity in the cart object. Handle the emitting of this event the same way by passing in the event update-quantity you want the parent component to listen for. The other required arguments you need to propagate up is the item.id and the new quantity of the line item being updated.

Going back up to your template still in CartItem.vue, hook up your update-quantity event. Between the item quantity element, attach your custom updateQuantity method to button click events. In the first button, implement a click event to decrease the line item quantity by 1 and in the second button to increase it by 1.

<!-- CartItem.vue -->

<template>
  <div class="cart-item">
    <img class="cart-item__image" :src="item.media.source" />
    <div class="cart-item__details">
      <h4 class="cart-item__details-name">{{ item.name }}</h4>
      <div class="cart-item__details-qty">
        <button @click="() => updateQuantity(item.quantity - 1)">-</button>
        <p>{{ item.quantity }}</p>
        <button @click="() => updateQuantity(item.quantity + 1)">+</button>
      </div>
      <p class="cart-item__details-price">{{ item.line_total.formatted_with_symbol }}</p>
    </div>
  </div>
</template>

As you can see, when the click event fires, it will call the updateQuantity() method passed into it with the quantity of the item decreased or increased by 1. These click events will emit the updateQuantity() to the parent Cart.vue component passing in the required data.

For the parent component to handle the event being propagated up, create an event handler to handle updating of the line item quantities.

// Cart.vue

methods: {
  /**
   * Updates line_items in cart
   * https://commercejs.com/docs/sdk/cart/#update-cart
   *  
   * @param {string} lineItemId ID of the cart line item being updated
   * @param {number} quantity New line item quantity to update
   */ 
  handleUpdateQuantity(lineItemId, quantity) {
    this.$commerce.cart.update(lineItemId, { quantity }).then((resp) => {
        this.cart = resp.cart;
    }).catch((error) => {
        console.log('There is an error updating the cart items', error);
    });
  },
}

In this helper handler function, call the commerce.cart.update() endpoint with lineItemId and destructured quantity. When you fire the updateQuantity() button in your CartItem.vue component, this event handler will run and response with the line item updated with the new quantity. You'll also need to bind the handler to the CartItem.vue component.

<!-- Cart.vue -->

<CartItem
  v-for="lineItem in cart.line_items"
  :key="lineItem.id"
  :item="lineItem"
  @update-quantity="handleUpdateQuantity"
  class="cart__inner"
/>

Upon a successful request, you should receive a response similar to the below abbreviated data:

{
  "success": true,
  "event": "Cart.Item.Updated",
  "line_item_id": "item_7RyWOwmK5nEa2V",
  "product_id": "prod_NqKE50BR4wdgBL",
  "product_name": "Kettle",
  "media": {
    "type": "image",
    "source": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png"
  },
  "quantity": 2,
  "line_total": {
    "raw": 91,
    "formatted": "91.00",
    "formatted_with_symbol": "$91.00",
    "formatted_with_code": "91.00 USD"
  },
  "_event": "Cart.Item.Updated",
  "cart": {
    "id": "cart_Mo11z2Xn30K7Wo",
    "created": 1600021165,
    "last_updated": 1600125011,
    "expires": 1602613165,
    "total_items": 2,
    "total_unique_items": 1,
    "subtotal": {
      "raw": 91,
      "formatted": "91.00",
      "formatted_with_symbol": "$91.00",
      "formatted_with_code": "91.00 USD"
    },
    "currency": {
      "code": "USD",
      "symbol": "$"
    },
    "discount_code": [],
    "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Mo11z2Xn30K7Wo",
    "line_items": [
      {
        "id": "item_7RyWOwmK5nEa2V",
        "product_id": "prod_NqKE50BR4wdgBL",
        "name": "Kettle",
        "media": {
          "type": "image",
          "source": "https://cdn.chec.io/merchants/18462/images/676785cedc85f69ab27c42c307af5dec30120ab75f03a9889ab29|u9 1.png"
        },
        "quantity": 2,
        "price": {
          "raw": 45.5,
          "formatted": "45.50",
          "formatted_with_symbol": "$45.50",
          "formatted_with_code": "45.50 USD"
        },
        "line_total": {
          "raw": 91,
          "formatted": "91.00",
          "formatted_with_symbol": "$91.00",
          "formatted_with_code": "91.00 USD"
        },
        "variants": []
      }
    ]
  }
}

5. Remove items from cart

Now that you have the ability to update the quantity of individual line items in your cart, you might also want the flexibility of being able to completely remove that line item from your cart. The Commerce.js commerce.cart.remove() method helps to remove a specific line item from your cart object.

Now go back to your CartItem.vue component to add the remove item from cart logic. Underneath the previously added updateQuantity() function, first add a helper method and call it removeFromCart().

// CartItem.vue

methods: {
  removeFromCart() {
    this.$emit('remove-from-cart', this.item.id);
  }
}

Once again, emit the event up to the parent cart component when the method is called in your template.

Next, update our updateQuantity function to invoke removeFromCart() when a line item with a quantity of 1 is decreased. This conveniently removes the line item from the cart when a quantity of 0 is being passed in to the commerce.cart.update() method.

  methods: {
    updateQuantity(quantity) {
      if (quantity < 1) {
          return this.removeFromCart();
      }
      this.$emit('update-quantity', this.item.id, quantity);
    },
  }

Now attach the removeFromCart() method to an isolated Remove button as well. When this click event is fired, the associated line item will be removed from the cart object.

<!-- CartItem.vue -->

<template>
  <div class="cart-item">
    <img class="cart-item__image" :src="item.media.source" />
    <div class="cart-item__details">
      <h4 class="cart-item__details-name">{{ item.name }}</h4>
      <div class="cart-item__details-qty">
        <button @click="() => updateQuantity(item.quantity - 1)">-</button>
        <p>{{ item.quantity }}</p>
        <button @click="() => updateQuantity(item.quantity + 1)">+</button>
      </div>
      <p class="cart-item__details-price">{{ item.line_total.formatted_with_symbol }}</p>
    </div>
    <button class="cart-item__remove" @click="removeFromCart">Remove</button>
  </div>
</template>

In your Cart.vue component, continue to emit the remove-from-cart method up to the main App.js parent component.

<!-- Cart.vue -->

<CartItem
  v-for="lineItem in cart.line_items"
  :key="lineItem.id"
  :item="lineItem"
  @update-quantity="handleUpdateQuantity"
  @remove-from-cart="$emit('remove-from-cart', $event)"
  class="cart__inner"
/>

Finally in App.js, create the event handler to make the request to the commerce.cart.remove() endpoint to execute your remove-from-cart event. The commerce.cart.remove() method takes in the required lineItemId parameter and once the promise is resolved, the returned cart object is one less of the removed line item.

// App.js

/**
 * Removes line item from cart
 * https://commercejs.com/docs/sdk/cart/#remove-from-cart
 *
 * @param {string} lineItemId ID of the line item being removed
 */
handleRemoveFromCart(lineItemId) {
  this.$commerce.cart.remove(lineItemId).then((resp) => {
    this.cart = resp.cart;
  }).catch((error) => {
    console.log('There was an error updating the cart items', error);
  });
},

Update your template with the removeFromCart() event attribute in the Cart component instance.

<Cart
  :cart="cart"
  @remove-from-cart="handleRemoveFromCart"
/>

With a successful request, your response should look like the below abbreviated data:

{
  "success": true,
  "event": "Cart.Item.Removed",
  "line_item_id": "item_7RyWOwmK5nEa2V",
  "_event": "Cart.Item.Removed",
  "cart": {
    "id": "cart_Mo11z2Xn30K7Wo",
    "created": 1600021165,
    "last_updated": 1600129181,
    "expires": 1602613165,
    "total_items": 0,
    "total_unique_items": 0,
    "subtotal": {
      "raw": 0,
      "formatted": "0.00",
      "formatted_with_symbol": "$0.00",
      "formatted_with_code": "0.00 USD"
    },
    "currency": {
      "code": "USD",
      "symbol": "$"
    },
    "discount_code": [],
    "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Mo11z2Xn30K7Wo",
    "line_items": []
  }
}

6. Clear cart items

Lastly, the cart action we will be going over in this guide is the commerce.cart.empty() method. The empty() at the Cart endpoint completely clears the contents of the current cart in session.

Since removal of the entire cart contents will happen at the cart component level, intercept an event for it directly in the cart UI. Go back to your Cart.vue component and add a click event to execute a helper function you will call emptyCart(). Underneath the component instance of CartItem, add in the button below:

<!-- Cart.vue -->

 <button v-if="cart.line_items.length" @click="emptyCart">Empty cart</button>

The v-if Vue directive will first check if there are any items inside the cart and if so, the Empty cart will render.

Add a new method in the Cart.vue component to emit the empty-cart method up to App.js.

// Cart.js

emptyCart() {
  this.$emit('empty-cart');
},

Now in App.js, create an event handler to handle the emptyCart() method. The commerce.cart.empty() does not require any parameters as calling the function simply deletes all the items in the cart.

// App.js

/**
 * Empties cart contents
 * https://commercejs.com/docs/sdk/cart/#remove-from-cart
 */
handleEmptyCart() {
  this.$commerce.cart.empty().then((resp) => {
    this.cart = resp.cart;
  }).catch((error) => {
    console.log('There was an error clearing your cart', error);
  });
}

Now with the handler function created, hook it up to your cart component.

<!-- App.js -->

<Cart
  :cart="cart"
  @remove-from-cart="handleRemoveFromCart"
  @empty-cart="handleEmptyCart"
/>

With a successful request to the cart endpoint, your response will look similar to the below json data:

{
  "success": true,
  "event": "Cart.Item.Removed",
  "line_item_id": "item_1ypbroE658n4ea",
  "_event": "Cart.Item.Removed",
  "cart": {
    "id": "cart_Mo11z2Xn30K7Wo",
    "created": 1600021165,
    "last_updated": 1600131015,
    "expires": 1602613165,
    "total_items": 0,
    "total_unique_items": 0,
    "subtotal": {
      "raw": 0,
      "formatted": "0.00",
      "formatted_with_symbol": "$0.00",
      "formatted_with_code": "0.00 USD"
    },
    "currency": {
      "code": "USD",
      "symbol": "$"
    },
    "discount_code": [],
    "hosted_checkout_url": "https://checkout.chec.io/cart/cart_Mo11z2Xn30K7Wo",
    "line_items": []
  }
}

That's it!

And there you have it, you have now wrapped up part two of the Commerce.js Vue.js guide on implementing cart functionalities to your application. The next guide will continue from this one to add a checkout flow.

You can find the full finished code in GitHub here!

Edit this page on GitHub