3
\$\begingroup\$

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>

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

I already gave many suggestions in my review of your previous question. I see that some of the advice has been incorporated - e.g. "use strict", using the spread operator, etc. Yet it seems some of the advice hasn't been used (e.g. variables starting with $, using jQuery more, etc.) but I won't lose sleep about those.

I suggest avoiding use of var unless you have a good reason - e.g. you need a global variable.

In createCartHTMLElements() instead of appending to html each time, map() could be used to return the interpolated string for each row and then Array.join() could be used. Another option there is the HTML added in the forEach() callback could be stored in a <template> element though browser support might be an issue.

In updateQuantity() instead of finding the index of the object to update, use find() to get a reference to the object and update that directly.

I see updateOrderSummary() has this variable:

const shippingFee = 10;

If that is a true variable, perhaps it should be declared at the top of the code:

const SHPPING_FEE = 10;

I suggest ALL_CAPS because that is a common convention in many style guides to distinguish constants within code - like you did for DELAY (even though it was commented out) in your blackjack code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.