Create a shopping cart with Nuxt 3

Build a persistent shopping cart for an online store using Nuxt js. Nuxt is a Vue js framework that implements server-side rendering, as well as some other server-side features coming with its latest version Nuxt 3.

Eli Lopez avatar

YouTuber

Published 12/21/2021

How to create a shopping cart - Online store with Nuxt 3 & Firebase
How to create a shopping cart - Online store with Nuxt 3 & Firebase

GitHub repository: https://github.com/EnterFlash/online-store-nuxt3

Nuxt.js is a framework that implements server-side rendering using Vue.js, therefore improving on very important things like speed and SEO for your website. The latest version of this framework has very cool new features that I'd like to explore. The main one is the feature of creating Node.js server functions in the same project.

This is going to be a series of tutorials that will end up in a fully functional online store, with authentication, Firestore connection, user order history, and payments with Stripe. During this tutorial, we'll be using Nuxt 3 to create a persistent shopping cart that saves its information in local storage to persist it. All the shopping cart products will be validated later when we add the Checkout function.

By the way! I always upload the project files to GitHub, but my preferred git managing GUI tool is GitKraken. Personally, I haven't found anything better. Give it a go here: https://www.gitkraken.com/invite/c4SJ1sK3

Creating the Nuxt.js project

To create a simple Nuxt.js 3 project first we need to install Node and NPM on our computer. If you haven't installed it, head on to their website and download the installer:

https://nodejs.org/en/download/

If you are on Mac or Linux, there's another very cool alternative that I use and that's Homebrew. It's a tool that helps you manage Node/NPM versions very easily. To install it run the installation script on their website front page:

https://brew.sh/

Once you have installed it, run the next command:

$ brew install node

Now we are going to create the Nuxt project. This is going to create all the files needed so we need to make sure we are in the correct folder before we run the command. To move between folders use the cd <folder> command on your computer.

$ npx nuxi init online-store-nuxt

NPX comes with the latest versions of Node. The project name is "online-store-nuxt" and that's also the name of the folder that is getting created. When it has finished installing, move into the folder, install all dependencies and run the project with the next commands:

$ cd online-store-nuxt$ npm install$ npm run dev

Styling the project with Bootstrap 5

Bootstrap is a CSS framework that contains many pre-built and styled-components to freely use in our application. You can see all the different ones in their documentation here:

https://getbootstrap.com/docs/

To add it to the project there are different ways. You can use NPM to have more control over the files or you can import them using a CDN. This is the way we are going to use it for now because it's the simplest and that's all we need.

In Nuxt 3, to add a "link" and a "script" tag globally you need to write them in the nuxt.config.ts file inside a "meta" object just like this:

import { defineNuxtConfig } from 'nuxt3'export default defineNuxtConfig({    meta: {        link: [            {                rel: 'stylesheet',                href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'            }        ],        script: [            {                src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js'            }        ]    }})

This is going to create a <script> tag with the corresponding src attribute as well as a <link> tag with the attributes rel and href.

Using the Nuxt.js Router

The Nuxt.js router is going to allow us to create different paths that lead to different pages on our site. For example, the login page, the account page, or the orders history page.

Open the app.vue page, which is the component that loads first every time, and substitute the boilerplate code <NuxtWelcome/> with a <NuxtPage/> tag. This tag is going to get substituted with the content of every different page.

Your app.vue file should now look like this:

<template>  <div>    <NuxtPage />  </div></template>

Add a new folder called "pages" and inside insert a new file called "index.vue". This is going to be the main page on our site and here's where we are going to list the products.

Installing Bootstrap from CDN

Bootstrap is a front-end framework that gives you multiple options on components to use in your website like buttons, cards, modals, etc. Also, it helps you create a responsive site very easily.

To install it in the Nuxt project we need to add the <script> and <link> tag that we get from the Bootstrap documentation.

Adding the Bootstrap CSS first

Visit https://getbootstrap.com/docs/ and look for the CSS section to find the <link> tag. We are going to copy the URL inside the href attribute of the tag.

Then, open the "nuxt.config.ts" file in your Nuxt project and create a new meta object inside the defineNuxtConfig function. Inside this new object, insert a property named link, which is going to be an array of objects. Add a final object inside the array with the href link you copied before, just like this:

import { defineNuxtConfig } from 'nuxt3'export default defineNuxtConfig({    meta: {        link: [            {                rel: 'stylesheet',                href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'            }        ]    }})

Adding the Bootstrap Javascript

Then, we are going to add the Bootstrap script tag to our project. This is going to allow us to use Javascript-powered components like carousels, sidebars, modals, etc.

Same as before, look for the JS Bundle in the Bootstrap Introduction documentation and copy the src URL.

Head back to your nuxt.config.ts file and inside the meta object insert a new property named "script" that contains an array. Inside this array add a new object with the property src and the value is going to be the URL you copied.

Your code should look like the one below:

import { defineNuxtConfig } from 'nuxt3'export default defineNuxtConfig({    meta: {        link: [            {                rel: 'stylesheet',                href: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css'            }        ],        script: [            {                src: 'https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js'            }        ]    }})

Creating a shopping cart component

Create a components folder and insert a ShoppingCart.vue component file.

We are going to make our shopping cart a right side offcanvas component from Bootstrap. You can check the documentation on this component here: https://getbootstrap.com/docs/5.1/components/offcanvas/#placement

Copy the code for the right side offcanvas, go to your ShoppingCart.vue file, add a <template> tag, and inside paste the code. It should end up looking like this:

<template>    <div class="offcanvas offcanvas-top"          tabindex="-1"          id="offcanvasTop"          aria-labelledby="offcanvasTopLabel">    <div class="offcanvas-header">        <h5 id="offcanvasTopLabel">Offcanvas top</h5>        <button type="button"                 class="btn-close text-reset"                 data-bs-dismiss="offcanvas"                 aria-label="Close"></button>    </div>  <div class="offcanvas-body">    ...  </div></div>

As you can see, I removed the <button> tag from the code because we are going to be adding it somewhere else later in the tutorial.

Creating a navbar component

Since our Navbar is going to be repeated in all views, we are going to create it as a component that we can then import. Go ahead and add a components folder and inside insert a new file called TheNavbar.vue

We are going to start with the HTML section of the component. I'm just going to use the navbar example from the Bootstrap documentation with a couple of tweaks:

https://getbootstrap.com/docs/5.1/components/navbar/#nav

I'm also going to add an extra button that has the attributes data-bs-toggle and data-bs-target to open the shopping cart offcanvas that we created before.

<template>  <div>  <nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-3">    <div class="container-fluid">      <NuxtLink to="/" class="navbar-brand">         Online Store      </NuxtLink>      <!-- Toggle button for mobile devices -->       <button        class="navbar-toggler"        data-bs-toggle="collapse"        data-bs-target="#navbarSupportedContent">        <span class="navbar-toggler-icon"></span>      </button>      <div class="collapse navbar-collapse" id="navbarSupportedContent">        <ul class="navbar-nav me-auto mb-2 mb-lg-0">          <li class="nav-item">            <NuxtLink to="/" class="nav-link" active-class="active">              Home            </NuxtLink>          </li>        </ul>        <div class="d-flex">          <!-- This button opens the shopping cart-->          <button            class="btn btn-outline-primary"            type="button"            data-bs-toggle="offcanvas"            data-bs-target="#offcanvasRight"            aria-controls="offcanvasRight"          >            Shopping cart          </button>        </div>      </div>    </div>      </nav>    </div>  </template>

Now go ahead and run the project with

$  npm run dev

Once it is running, open the generated URL (in my case, localhost:3000) you should see this in your browser:

Listing the products on the index page

Head on to your index.vue file, where we are going to list the products available.

Add a <script> tag with a data and a methods section Vue style. In the data section, we are going to add a shoppingCart array and a products array.

The shoppingCart array will hold all the items that were added to the cart, while the products array will contain all the products available. I'm going to be adding 4 sample hard-coded sample products. In the next tutorial, we'll be retrieving these from Firestore Database: <link>

Also, in the methods section add an empty function called addToCart that receives a product parameter. We are going to set up this later.

Your script section should end up looking like this:

// index.vue<script>export default {    data() {        return {            shoppingCart: [], // Store the added-to-cart items            products: [                {                    uuid: '5b9ed8b5-201e-4297-babb-29a566952e91',                    name: 'Camera model 1',                     description: 'Lorem ipsum dolor sit amet.',                     price: 950,                    photoURL: 'https://images.pexels.com/photos/51383/photo-camera-subject-photographer-51383.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'                },                {                     uuid: '973b3d42-e039-428a-b2ad-e6444b5895f4',                    name: 'Camera model 2',                     description: 'Lorem ipsum dolor sit amet.',                     price: 950,                    photoURL: 'https://images.pexels.com/photos/1203803/pexels-photo-1203803.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260'                },                {                    uuid: '33d3332e-42ac-4692-8523-ae76c3d8a773',                    name: 'Camera model 3',                     description: 'Lorem ipsum dolor sit amet.',                     price: 950,                    photoURL: 'https://images.pexels.com/photos/249597/pexels-photo-249597.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'                },                {                     uuid: 'c70080cd-330f-4398-b169-03f057582e2a',                    name: 'Camera model 4',                     description: 'Lorem ipsum dolor sit amet.',                     price: 950,                    photoURL: 'https://images.pexels.com/photos/1091294/pexels-photo-1091294.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'                }            ]        }    },    methods: {        addToCart(product) {}    }}</script>

Once we have the script section set up, we can go onto the HTML part of the component.

Using a v-for attribute, loop through each of the products and set them up as a card item. Also, add a button with a @click Vue attribute to run the addToCart method when clicked.

The HTML section of the index.vue component should look like this:

// index.vue<template>    <div class="container">        <div class="row">            <div v-for="(product, index) in products"                  :key="'product-' + index"                  class="col-md-4">                <div class="card mb-3">                    <img :src="product.photoURL"                          class="card-img-top">                    <div class="card-body">                        <h5 class="card-title">                            {{ product.name }}                        </h5>                        <p class="card-text">                            {{ product.description }}                        </p>                        <div class="d-grid">                            <button @click="addToCart(product)"                                     class="btn btn-outline-primary">                                Add to cart                            </button>                        </div>                    </div>                </div>            </div>        </div>    </div></template>

Now in the addToCart method that we created before, we are going to receive the product, then loop through the shopping cart array to try to find it in it.

If the product is found in the shopping cart, we are going to add one to the amount property of the item. If it's not in the shopping cart, we are going to push it into the array with a property amount set to one.

// index.vue...methods: {    addToCart(product) {        let exists = false                    for (const cartItem of this.shoppingCart) {            if (cartItem.uuid === product.uuid) {                cartItem.amount = cartItem.amount + 1                exists = true                break            }        }        if (!exists) {            this.shoppingCart.push({                ...product,               amount: 1,            })        }    },}...

If reload the page, the information goes away. So we need to persist it by saving the current state of the shopping cart in the local storage of our browser.

To do this, we are going to use the "watch" section to watch for any changes on the shopping cart array. Every time an item in the array changes, we update the value in localStorage.

Also, to retrieve the items from the localStorage when the user first loads, we add a mounted() method that does this.

// index.vue...mounted() {    /// Retrieves cart from local storage when user first loads    this.shoppingCart = JSON.parse(        localStorage.getItem('shoppingCart') || "[]")},watch: {    shoppingCart: {        handler(newValue) {            /// Updates the item in local storage            localStorage.setItem(                'shoppingCart', JSON.stringify(newValue));        }, deep: true    }}...

Creating a shopping cart component

In your index.vue file, add the <ShoppingCart /> component tag at the very bottom but still inside the div root element. Also, pass in the shoppingCart array that contains the added products like this:

// index.vue<template>    <div class="container">        <div class="row">            <div v-for="(product, index) in products"                  :key="'product-' + index" class="col-md-4">                <div class="card mb-3">                    <img :src="product.photoURL" class="card-img-top">                    <div class="card-body">                        <h5 class="card-title">                            {{ product.name }}                        </h5>                        <p class="card-text">                            {{ product.description }}                        </p>                        <div class="d-grid">                            <button @click="addToCart(product)"                                     class="btn btn-outline-primary">                                Add to cart                            </button>                        </div>                    </div>                </div>            </div>        </div>        <!-- Add the shopping cart here! 👀✌️ -->        <ShoppingCart v-model="shoppingCart"/>    </div></template>...

Go to your shopping cart component file and start your script tag. In the props array we are going to receive a parameter called "modelValue" that will contain the value passed in the v-model, which was the shopping cart array.

Add a computed section that has a totalSum() method. This will return the total sum of the prices from each product in the shopping cart.

Create a new method called removeFromCart that receives a parameter product. This method will be in charge of removing the product from the shopping cart or reducing the amount property by one.

At the end of the removeFromCart method, instead of removing the item from the modelValue array directly, we are going to "emit" an event called "update:modelValue". In the background, this event will update the value of the shoppingCart array passed in the v-model attribute and therefore also update the value of the item in the localStorage of your browser.

Your code should look like this:

// components/ShoppingCart.vue<script>export default {  props: ['modelValue'],  computed: {    totalSum() {      /// Sums all the prices and returns a TOTAL price      let sum = 0      for (const product of this.modelValue) {        sum += product.price * product.amount      }            return sum    }  },  methods: {    removeFromCart(product) {      // Looks for the item in the shopping cart array      // Then, substracts one to the amount or removes it      const shoppingCart = this.modelValue      const productIndex = shoppingCart.findIndex(                             item => item.uuid === product.uuid)      shoppingCart[productIndex].amount -= 1            if (shoppingCart[productIndex].amount < 1) {        shoppingCart.splice(productIndex, 1)      }      this.$emit('update:modelValue', shoppingCart)    }  }}</script>

Finally, we are going to use the v-for attribute to loop through the shopping cart items and list them as Bootstrap cards in the offcanvas.

<template>  <div>    <div      class="offcanvas offcanvas-end"      tabindex="-1"      id="offcanvasRight"      aria-labelledby="offcanvasRightLabel"    >      <div class="offcanvas-header">        <h5 id="offcanvasRightLabel">          Shopping cart - $ {{ totalSum }}        </h5>        <button          type="button"          class="btn-close text-reset"          data-bs-dismiss="offcanvas"          aria-label="Close"></button>      </div>      <div class="offcanvas-body">        <div v-for="(product, index) in modelValue"              :key="'cart-product-' + index"              class="card mb-3">          <div class="row">            <div class="col-md-4">              <img :src="product.photoURL"                    class="img-fluid rounded-start">            </div>            <div class="col-md-8">              <div class="card-body">                <h5 class="card-title">                  {{ product.name }}                </h5>                <p class="card-text">                  {{ product.description }}                </p>                <p class="card-text">                  {{ product.price }} x $ {{ product.amount }}                </p>                <div class="d-grid">                  <button @click="removeFromCart(product)"                           class="btn btn-outline-secondary">                    Remove                  </button>                </div>              </div>            </div>          </div>        </div>      </div>    </div>  </div></template>

In the next tutorial, we are going to be getting the items from Firestore using the Admin SDK so stay tuned for that. You can also always take a look at the GitHub repository here:

https://github.com/EnterFlash/online-store-nuxt3/