A shopping cart is a vital component for any e-commerce website, allowing customers to easily make a purchase of products while keeping track of the total price. Additionally, features such as infinite scroll and search functionality can greatly enhance the user experience, reducing the distance between customers and products and ultimately leading to increased sales and profits.
Vuejs is my favorite framework. In this post, I would like to share with you my implementation to use it in conjunction with other libraries within the Vue.js ecosystem to build a shopping cart. By leveraging these libraries, we can easily build components and manage the state changes of the application, resulting in a more efficient and effective development process.
Requirements
To follow along with me, you need to be familier with Vuejs and it's ecosystem.
Content
- Setting up the project.
- Creating the components that will construct and display the shopping cart.
- Creating the necessary stores to manage app states.
- Create infinite scroll composable and SearchInput component.
- Conclusion, Full-Code and Working-Example.
I will use DummyJSON, to get dummy/fake JSON data to use as placeholder in development and prototyping, without worrying about writing a backend.
😃 Allow me to show you.
Setting up the project
Vite is simplifying the workflow, I will use it to create a new project and install all needed libraries:
- Pinia is a store library for Vuejs, it allows you to share a state across components/pages.
- Vueuse a collection of essential Vuejs composition utilities.
- Primevue is a big collection of Vuejs UI Components with top-notch quality to help you implement all your UI requirements in style.
- Tailwindcss it helps a lot to write css quicker.
npm i --save @vueuse/core pinia primevue primeicons
Creating the components
I will break down the Shopping-Cart into four components:
1- Cart-Toggle-Btn component that will hide/show the cart list items and give a counting summary of the total items number within the cart. It will also watch for the changes in items number and play the animate__heartBeat
animation on itself to tell that a change has happened:
<script setup>
import { ref, watch } from "vue";
import Badge from '../Common/Badge.vue';
const props = defineProps(['totalItems']);
const cartRef = ref(null);
watch(() => props.totalItems, () => {
cartRef.value.classList.add('animate__heartBeat');
});
const removeTadaClass = () => {
cartRef.value.classList.remove('animate__heartBeat');
};
</script>
<template>
<div id="animation-box" class="inline-block cursor-pointer animate__animated" @animationend="removeTadaClass" ref="cartRef">
<Badge
:value="totalItems"
class="toggle-btn
pi pi-shopping-cart
border-2 border-secondary rounded-full
text-lg text-white
p-4"
/>
</div>
</template>
2- MainDialog component will display the cart list items, the total cost, and the submit order button in an overlay window:
<script setup>
import BtnIconRounded from "../Common/BtnIconRounded.vue";
import Tabs from "./Tabs.vue";
defineProps(['toggleCart', 'totalCost']);
const emits = defineEmits(['closeCart']);
</script>
<template>
<Dialog :visible="toggleCart" header="Header" :closable="false">
<template #header>
<div class="w-full flex flex-col">
<div class="w-full max-w-bd-sm mx-auto p-4 flex items-center justify-between">
<div class="text-md text-primary dark:text-primary-dark font-ssp font-bold">$ {{ totalCost }}</div>
<BtnIconRounded icon="pi pi-times" @click="emits('closeCart')" />
</div>
</div>
</template>
<Tabs />
</Dialog>
</template>
3- List component will present the cart items and enable the user to add/remove units of a particular product within the cart:
<script setup>
import { ref } from "vue";
import useCartStore from "../../stores/cart";
import { useTranslate } from "../../composables/useTranslate";
import { useSimpleBar } from "../../composables/useSimpleScroll";
import BtnIconRounded from "../Common/BtnIconRounded.vue";
import Btn from "../Common/Btn.vue";
const cartStore = useCartStore();
const { doTranslate } = useTranslate();
const cartList = ref(null);
const emits = defineEmits(['changeTab']);
useSimpleBar({elementRef: cartList});
</script>
<template>
<div id="list-box" ref="cartList" class="h-full max-w-bd-sm mx-auto relative overflow-auto">
<div
v-if="cartStore.items.length > 0"
class="w-full max-w-bd-sm mx-auto
pb-8 px-4 fixed z-10
bg-white dark:bg-primary-bg-dark"
>
<Btn :label="doTranslate('cart.submit')" icon="pi pi-shopping-cart" size="sm" @click="emits('changeTab', 'order')" />
</div>
<ul class="w-full px-2 pt-20">
<li v-for="item of cartStore.items" :key="item.id" class="mt-8 text-secondary">
<div class="text-sm text-secondary dark:text-white font-mont font-bold">{{ item.title }}</div>
<div class="text-xs text-secondary dark:text-white font-ssp flex">
<span>{{ item.count }}</span>
<span class="px-2">{{ doTranslate('cart.for') }}</span>
<span class="px-2">$</span>
<span class="font-bold">{{ item.price * item.count }}</span>
</div>
<div class="flex">
<div class="p-4"><BtnIconRounded icon="pi pi-plus" @click="cartStore.addItem(item)" /></div>
<div class="p-4"><BtnIconRounded icon="pi pi-minus" @click="cartStore.removeItem(item)"/></div>
</div>
</li>
</ul>
</div>
</template>
4- The Container component will wrap all the children components and position the Cart-Toggle-Btn component to be sticky on the page:
<script setup>
import { ref } from "vue";
import useCartStore from "../../stores/cart";
import ToggleBtn from "./ToggleBtn.vue";
import MainDialog from "./MainDialog.vue";
const cartStore = useCartStore();
const toggleCart = ref(false);
</script>
<template>
<div class="sticky left-100 bottom-0
z-10
p-4 pb-20"
@click="toggleCart = true"
>
<ToggleBtn :total-items="cartStore.totalItems" />
<MainDialog :total-cost="cartStore.totalCost" :toggleCart="toggleCart" @closeCart="toggleCart = false" />
</div>
</template>
Creating the necessary stores
I need two stores to manage the state of both the cart and the products list which the user selects from it:
1- Products-Store will store the items received from the API and push them into an array, paginate over that array, and check if the API end-point has more items:
import { ref } from "vue";
import { defineStore } from "pinia";
export default defineStore('products', () => {
const products = ref({
items: [],
currentPageNum: 0,
limitPerPage: 30,
hasMore: true
});
const productsReceived = (items) => {
if(items.length === 0) return products.value.hasMore = false;
products.value.items = [...products.value.items, ...items];
products.value.currentPageNum += 1;
};
const resetProducts = () => {
products.value = {
items: [],
currentPageNum: 0,
limitPerPage: 30,
hasMore: true
}
}
return {
products,
productsReceived,
resetProducts
}
});
2- Cart-Store will be responsible for adding a new item to the cart or removing it, calculating the total items number and the total cost:
import { ref } from "vue";
import { defineStore } from "pinia";
export default defineStore('cart', () => {
const items = ref([]);
const totalItems = ref(0);
const totalCost = ref(0);
const addItem = (item) => {
let targetItem = items.value.filter( currItem => currItem.id === item.id )[0];
if(targetItem) targetItem.count += 1;
else items.value = [...items.value, {...item, count: 1}];
totalItems.value += 1;
totalCost.value += item.price;
};
const removeItem = (item) => {
let targetItem = items.value.filter( currItem => currItem.id === item.id )[0];
if(targetItem.count === 1) items.value = items.value.filter( currItem => currItem.id !== item.id );
else targetItem.count -= 1;
totalItems.value -= 1;
totalCost.value -= item.price;
};
return {
items,
addItem,
removeItem,
totalItems,
totalCost
};
});
Create infinite scroll composable and SearchInput component
The last two pieces of the puzzle:
1- To add an Infinite-Scroll feature to the products list to load more items when the user scrolls down and reaches its bottom.
2- The SearchInput component will help the user to search the products list and quickly find the desired product.
Also, I want the style of the Scrollbar to be the same on all devices and browsers, I will achieve that by using simplebar
lib wrapped inside a Vuejs composable.
Ok let's add some cdns for the simplebar
lib:
<script src="https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css" />
Here is the composable:
import { onMounted, onBeforeUnmount } from "vue";
export function useSimpleBar({elementRef, callback = null}) {
let scrollerObj = null;
const onScrollEvent = (e) => {
if ((Math.floor(e.target.scrollTop) + Math.floor(e.target.clientHeight) + 150) > Math.floor(e.target.scrollHeight)) {
callback();
}
};
onMounted(() => {
scrollerObj = new SimpleBar(elementRef.value);
if(callback) {
scrollerObj.getScrollElement().addEventListener('scroll', onScrollEvent);
}
});
onBeforeUnmount(() => {
scrollerObj.unMount();
if(callback) {
scrollerObj.getScrollElement().removeEventListener('scroll', onScrollEvent);
}
});
return null;
}
Offering the ability to search the products list will help the user to browse the products list and choose the needed ones, useDebounceFn
is a useful composable from vueuse
lib that can help in reducing the number of API calls to the server:
<script setup>
import { useDebounceFn } from '@vueuse/core';
import { useTranslate } from "../composables/useTranslate";
const emit = defineEmits(['searching']);
const { doTranslate } = useTranslate();
const debouncedSearch = useDebounceFn((value) => {
emit('searching', value);
}, 1000);
</script>
<template>
<input class="p-8 text-secondary
placeholder:text-secondary placeholder:dark:text-white"
:placeholder="doTranslate('searching.placeholder')"
@input="debouncedSearch($event.target.value)" />
</template>
Conclusion
Breaking the task into smaller tasks definitely will help you to solve the problem and complete the task. Vuejs has a rich ecosystem that offers all the necessary tools to build any application.
Your feedback is greatly appreciated, thank you for reading.
Top comments (0)