This is a follow-up question to eCommerce Mockup App in JS.
How do I implement the 'observe' pattern for the cart to regenerate html? So that, it will keep refresh its own. And any review on my coding style and code structure design?
"use strict";
// Bugs
// 1. UI bug for cart item count. Non responsive.
// Initialization of data and page variables - start.
// For actual system, get data from database via API call either in JSON format.
const productList = [
{ id: 101, product: "Logitech Mouse", unitprice: 45.0, image: "LogitechMouse.jpg" },
{ id: 102, product: "Logitech Keyboard", unitprice: 50.0, image: "LogitechKeyboard.jpg" },
{ id: 103, product: "HP Mouse", unitprice: 35.0, image: "HpMouse.jpg" },
{ id: 104, product: "HP Keyboard", unitprice: 32.0, image: "HpKeyboard.jpg" },
{ id: 105, product: "Microsoft Mouse", unitprice: 43.0, image: "MsMouse.jpg" },
{ id: 106, product: "Microsoft Keyboard", unitprice: 39.0, image: "MsKeyboard.jpg" }
];
let productListFilter = []
var cart = [];
const $shoppingCartContainer = document.getElementById("shoppingCartContainer");
const $clearAll = document.getElementById("clearAll");
const $shoppingCart = document.getElementById("shoppingCart");
const $totalCartItems = document.getElementById("totalCartItems");
const $summary = document.getElementById("summary");
// Initialization of data and page variables - end.
// Functions - start -------------------------------------
const createCartHTMLElements = () => {
if (cart.length === 0) {
return;
}
// Start our HTML
let html = "<table><tbody>";
cart.forEach(function (item) {
html += `<tr class=productData><td class="productName">${item.product}</td>\
<td class="productPrice">${item.unitprice.toFixed(2)}</td>\
<td class="quantityProduct">\
<i class="fa fa-plus plus-btn" data-id="${item.id}"></i>\
<label class="quantity">${item.quantity}</label>\
<i class="fa fa-minus minus-btn" data-id="${item.id}"></i>\
</td>\
<td class="total">${item.total.toFixed(2)}</td>\
<td class="deleteProduct"><i class="fa fa-remove del" data-id="${
item.id
}"></i></td>\
</tr>`;
});
// Finish the table:
html += "</tbody></table>";
// Return the table
return html;
};
const updateQuantity = (operation, productId, tr) => {
// Update the quantity in UI
const $quantity = tr.find(".quantity");
const n = $quantity.html();
let i;
switch (operation) {
case "plus":
i = parseInt(n) + 1;
break;
case "minus":
i = parseInt(n) - 1;
if (i < 0) i = 0; // prevent negative quantity
if (i == 0) {
// Duplicate code with delete function - start
cart = cart.filter(function (el) {
return el.id != productId;
});
if (cart.length === 0) {
$clearAll.click();
}
updateCartCount();
updateOrderSummary();
// Duplicate code with delete function - end
tr.closest("tr").remove(); // this is different
}
break;
}
$quantity.html(i);
// Update the total price in UI
const $price = tr.find(".productPrice");
const price = parseFloat($price.html());
const $total = tr.find(".total");
const total = i * price;
$total.html(total.toFixed(2));
// Update the quantity and total in list object
// Find index of specific object using findIndex method.
const objIndex = cart.findIndex(obj => obj.id == productId);
if (objIndex >= 0) {
// Update object's name property.
cart[objIndex].quantity = i;
cart[objIndex].total = total;
updateOrderSummary();
}
};
const populateProducts = () => {
debugger
// Start our HTML
let html = "";
// Loop through members of the object
productListFilter.forEach(function (item) {
html += `<div class="column"><div class="card">\
<div><img class=AddToCart data-id="${item.id}"
src="img/${item.image}" width=250 height=250></div>
<h2>${item.product}</h2>
<p class="price">RM ${item.unitprice.toFixed(2)}</p>
<p><button class=AddToCart data-id="${item.id}">Add to Cart</button></p>\
</div></div>`;
});
document.getElementById("productRow").innerHTML = html;
createAddToCartEventListener();
};
const updateOrderSummary = () => {
document.getElementById("totalItem").innerHTML = cart.length + " item";
const subTotal = cart
.reduce(function (acc, obj) {
return acc + obj.total;
}, 0)
.toFixed(2);
const shippingFee = 10;
document.getElementById("subTotal").innerHTML = subTotal;
document.getElementById("shippingFee").innerHTML = shippingFee.toFixed(2);
document.getElementById("total").innerHTML = (
parseInt(subTotal) + shippingFee
).toFixed(2);
};
const updateCartCount = () => {
//let totalCount = 0;
//cart.forEach(element => totalCount += element.quantity)
$totalCartItems.innerHTML =
cart.reduce((previous, element) => previous + element.quantity, 0);
};
// Functions - End -------------------------------------
// Event listener - start -------------------------------------
const createAddToCartEventListener = () => {
var addToCart = document.getElementsByClassName("AddToCart");
//Array.prototype.forEach.call(addToCart, function (element) {
[...addToCart].forEach(function (element) {
element.addEventListener("click", function () {
// Filter the selected "AddToCart" product from the ProductList list object.
// And push the selected single product into shopping cart list object.
productList.filter(prod => {
if (prod.id == element.dataset.id) {
// Update the quantity in list object
// Find index of specific object using findIndex method.
let objIndex = cart.findIndex(
obj => obj.id == parseInt(element.dataset.id)
);
if (objIndex >= 0) {
// Old item found
cart[objIndex].quantity = cart[objIndex].quantity + 1;
cart[objIndex].total = cart[objIndex].quantity * prod.unitprice;
} else {
// For new item
prod.quantity = 1;
prod.total = prod.unitprice;
cart.push(prod);
}
$shoppingCart.innerHTML = createCartHTMLElements();
createDeleteEventListener();
createPlusButtonEventListener();
createMinusButtonEventListener();
$totalCartItems.style.display = "block";
$clearAll.style.display = "block";
$summary.style.display = "block";
updateCartCount();
updateOrderSummary();
return;
}
});
});
});
};
const createSearchProducts = () => {
document.getElementById('btnSearch').addEventListener('click', function () {
let $productName = document.getElementById('productName').value;
console.log($productName)
if ($productName.trim().length === 0) {
productListFilter = productList;
} else {
productListFilter = productList.filter(product =>
product.product.includes($productName)
)
}
populateProducts();
})
//document.getElementById('productName').addEventListener('keyup', function (element) {
// productList = productList.filter(product => {
// console.log(product.product)
// console.log(element.key)
// product.product == element.key;
// })
// createAddToCartEventListener();
//})
}
const createDeleteEventListener = () => {
var del = document.getElementsByClassName("del");
[...del].forEach(function (element) {
element.addEventListener("click", function () {
// Duplicate code with minus quantity function
// When quantity is zero, it will delete that item
cart = cart.filter(el => el.id != element.dataset.id);
if (cart.length === 0) {
$clearAll.click();
}
updateCartCount();
updateOrderSummary();
element.closest("tr").remove();
});
});
};
const createPlusButtonEventListener = () => {
var plusBtn = document.getElementsByClassName("plus-btn");
[...plusBtn].forEach(function (element) {
element.addEventListener("click", function () {
let productId = element.dataset.id;
let $tr = $(this).closest("tr");
updateQuantity("plus", productId, $tr);
})
})
}
const createMinusButtonEventListener = () => {
var minusBtn = document.getElementsByClassName("minus-btn");
[...minusBtn].forEach(function (element) {
element.addEventListener("click", function () {
let productId = element.dataset.id;
let $tr = $(this).closest("tr");
updateQuantity("minus", productId, $tr);
})
})
}
// $(document.body).on("click", ".plus-btn", function () {
// let productId = $(this).attr("data-id");
// let $tr = $(this).closest("tr");
// updateQuantity("plus", productId, $tr);
// });
// $(document.body).on("click", ".minus-btn", function () {
// let productId = $(this).attr("data-id");
// let $tr = $(this).closest("tr");
// updateQuantity("minus", productId, $tr);
// });
$clearAll.addEventListener("click", function () {
$shoppingCart.innerHTML = "";
$shoppingCartContainer.style.display = "none"
cart.length = 0;
$clearAll.style.display = "none";
$summary.style.display = "none";
updateOrderSummary();
updateCartCount();
});
document.getElementById("cartIcon").addEventListener("click", function () {
if ($shoppingCartContainer.style.display === "none") {
if (cart.length === 0) {
return
}
$shoppingCartContainer.style.display = "block";
} else {
$shoppingCartContainer.style.display = "none";
}
});
window.addEventListener("load", function () {
$shoppingCartContainer.style.display = "none";
window.setTimeout(function () { }, 1000); // prevent flickering
productListFilter = productList;
populateProducts();
createSearchProducts();
//createAddToCartEventListener();
//updateCartCount();
});
// Event listener - end -------------------------------------
.card {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
margin: auto;
text-align: center;
font-family: arial;
width: 18em;
}
.price {
color: grey;
font-size: 1.5em;
}
img {
cursor: pointer;
}
.card button {
border: none;
outline: 0;
padding: 12px;
color: white;
background-color: #000;
text-align: center;
cursor: pointer;
width: 100%;
font-size: 1em;
}
.card button:hover {
opacity: 0.7;
}
.productContainer {
margin: 15px;
}
.summaryDetails {
width: 100px;
text-align: right;
}
#productRow {
display: flex;
flex-wrap: wrap;
}
.column {
flex: 1;
margin-top: 12px;
}
@media (max-width: 1333px) {
.column {
flex-basis: 33.33%;
}
}
@media (max-width: 1073px) {
.column {
flex-basis: 33.33%;
}
}
@media (max-width: 815px) {
.column {
flex-basis: 50%;
}
}
@media (max-width: 555px) {
.column {
flex-basis: 100%;
}
}
#header {
width: 100%;
display: flex;
justify-content: space-between;
height: 50px;
}
#left,
#right {
width: 30%;
padding: 10px;
padding-right: 30px;
}
#right {
text-align: right;
position: relative;
}
#main {
max-width: 1000px;
border: 1px solid black;
margin: 0 auto;
position: relative;
}
#shoppingCartContainer {
width: 400px;
background-color: lightyellow;
border: 1px solid silver;
margin-left: auto;
position: absolute;
top: 50px;
right: 0;
padding: 10px;
}
.fa {
color: blue;
cursor: pointer;
}
.fa-shopping-cart {
font-size: 36px;
}
.fa-remove {
font-size: 24px;
color: red;
}
#totalCartItems {
margin: 0px 0px -2px auto;
color: red;
padding: 2px;
border-radius: 25px;
width: 10px;
font-size: 1em;
position: absolute;
}
.plus-btn img {
margin-top: 2px;
}
tr.productData,
td.productName,
td.productPrice,
td.quantityProduct,
td.price,
td.deleteProduct {
padding: 10px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="styles.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="main">
<div id="header">
<div id="left">Login as Joe Doe, <a href="">Logout</a></div>
<div id="right">
<i id="cartIcon" class="fa fa-shopping-cart"> </i>
<span id="totalCartItems"></span>
</div>
</div>
<div>
<input type="text" placeholder="Search your product" id="productName" />
<button id="btnSearch">Search</button>
</div>
<hr>
<div class="productContainer">
<div id="productRow"></div>
</div>
<div id="shoppingCartContainer">
<div id="shoppingCart"></div>
<button id="clearAll">Clear Cart</button>
<div id="summary">
<hr>
<h3>Order Summary</h3>
<table>
<tr>
<td>Subtotal (<span id="totalItem"></span>)</td>
<td class="summaryDetails"><span id="subTotal"></span></td>
</tr>
<tr>
<td>Shipping Fee</td>
<td class="summaryDetails"><span id="shippingFee"></span></td>
</tr>
<tr>
<td>Total</td>
<td class="summaryDetails"><span id="total"></span></td>
</tr>
</table>
<button>Proceed to checkout</button>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script src="app.js"></script>
</body>
</html>