Chapter 10.4☕ 25 min read

Shopping Cart — Objects, Reduce, Event Delegation & Templates

Cart ka total ek line mein nikalo (Reduce), buttons ko ek jagah sambhalo (Delegation), aur kabhi bhi catalog data mutate mat karo!

01🛒 Cart Data Structure & State Management

The cart is an array of objects: each item holds id, name, price, and quantity. The product catalog is a separate data source — the cart only stores references (the id) and how many the user wants.

The key rule: if an item already exists in the cart, increment its quantity instead of adding a duplicate entry. Use findIndex or find to check for existing items.

let cart = [];

function addToCart(product) {
  const existingIndex = cart.findIndex(item => item.id === product.id);
  
  if (existingIndex !== -1) {
    cart[existingIndex].quantity += 1;
  } else {
    cart.push({ ...product, quantity: 1 });
  }
  renderCart();
}

function removeFromCart(productId) {
  cart = cart.filter(item => item.id !== productId);
  renderCart();
}

function updateQuantity(productId, newQty) {
  if (newQty <= 0) {
    removeFromCart(productId);
    return;
  }
  const item = cart.find(item => item.id === productId);
  if (item) item.quantity = newQty;
  renderCart();
}
Why not splice? Array.splice() mutates the original array and requires index calculation. filter returns a brand new array — cleaner for state management. If you ever switch to a reactive framework (React, Angular), immutable updates (filter, map, spread) are the standard pattern.

Three core operations: addToCart (add or increment), removeFromCart (filter out), and updateQuantity (set new qty or remove if zero). Every cart action boils down to one of these three.

02🔢 Reduce: The King of Calculations

reduce is perfect for calculating totals from an array of objects. Instead of writing a for loop with a mutable variable, reduce takes the array and folds it into a single value — the total price.

Multiply price * quantity for each item, then accumulate the sum. You can also count total items the same way.

const cart = [
  { id: 1, name: "Laptop", price: 999, quantity: 1 },
  { id: 2, name: "Mouse", price: 25, quantity: 2 },
  { id: 3, name: "Keyboard", price: 50, quantity: 1 }
];

// Total Price
const totalPrice = cart.reduce((total, item) => {
  return total + (item.price * item.quantity);
}, 0); // 999 + 50 + 50 = 1099

// Total Items Count
const totalItems = cart.reduce((count, item) => {
  return count + item.quantity;
}, 0); // 1 + 2 + 1 = 4

// Format to currency
const formatted = new Intl.NumberFormat("en-IN", {
  style: "currency", currency: "INR"
}).format(totalPrice);
The initial value is critical: If you omit the 0 and the cart is empty, reduce throws TypeError: Reduce of empty array with no initial value. Always provide 0 for sum operations — it also makes the intent clear: "start from zero and accumulate."

Why reduce over for: No mutable external variable, no off-by-one index bugs, and the intent is declarative — "reduce this array to a single sum." This is the industry standard for deriving totals from arrays.

03🔄 Event Delegation: Handling Cart Actions

A cart has multiple buttons per item: +, -, and Delete. Adding a separate event listener to each button is inefficient and creates memory leaks when the DOM re-renders.

Event Delegation: Attach ONE listener to the cart container. Use data-action and data-id attributes on buttons. The handler reads the action, finds the product ID, and calls the corresponding state function.

const cartContainer = document.getElementById("cart-items");

cartContainer.addEventListener("click", (e) => {
  const btn = e.target.closest("button");
  if (!btn) return;
  
  const action = btn.dataset.action;
  const id = parseInt(btn.dataset.id);
  
  switch (action) {
    case "increase":
      const item = cart.find(i => i.id === id);
      if (item) updateQuantity(id, item.quantity + 1);
      break;
    case "decrease":
      const itemD = cart.find(i => i.id === id);
      if (itemD) updateQuantity(id, itemD.quantity - 1);
      break;
    case "remove":
      removeFromCart(id);
      break;
  }
});

// Button Template
function itemTemplate(item) {
  return '' +
         '' + item.quantity + '' +
         '' +
         '';
}

Why closest('button'): Sometimes the click lands on an icon or span inside the button. closest walks up the DOM tree to find the actual button element that holds the data-* attributes. This prevents undefined errors and makes the handler robust.

04📄 Template Rendering & DOM Manipulation

Template rendering means building the HTML string from your data and updating the DOM in one shot. Use map().join('') to convert the cart array into an HTML string.

Template functions that return strings keep your rendering logic clean and separate from your event logic. Update the DOM once by setting innerHTML, then update the total price span separately.

function renderCart() {
  const cartItemsDiv = document.getElementById("cart-items");
  const cartTotalSpan = document.getElementById("cart-total");
  
  if (cart.length === 0) {
    cartItemsDiv.innerHTML = '

Your cart is empty.

'; cartTotalSpan.textContent = '₹0'; return; } // Build HTML string const html = cart.map(item => { return '
' + '

' + item.name + ' (₹' + item.price + ')

' + '
' + itemTemplate(item) + '
' + '

Subtotal: ₹' + (item.price * item.quantity) + '

' + '
'; }).join(''); // Single DOM update cartItemsDiv.innerHTML = html; // Update totals const total = cart.reduce((sum, i) => sum + (i.price * i.quantity), 0); cartTotalSpan.textContent = '₹' + total; }
Why single innerHTML update: Every time you touch the DOM (read or write), the browser may trigger a reflow or repaint. Updating innerHTML once with the full HTML string is significantly faster than appending elements one by one in a loop. Batch your DOM writes!
05✅ Form Validation: Checkout Details

Before checking out, validate user details like Name, Address, and Phone. Use the Constraint Validation API or custom regex. Disable the checkout button until the form is valid, and give real-time feedback as the user types.

const checkoutForm = document.getElementById("checkout-form");

checkoutForm.addEventListener("submit", (e) => {
  e.preventDefault();
  
  const name = document.getElementById("name").value.trim();
  const phone = document.getElementById("phone").value.trim();
  
  // Validation
  if (name.length < 3) {
    showError("Name must be at least 3 characters");
    return;
  }
  
  const phoneRegex = /^[6-9]\d{9}$/; // Indian mobile
  if (!phoneRegex.test(phone)) {
    showError("Enter a valid 10-digit phone number");
    return;
  }
  
  // If valid, proceed with order
  processOrder(name, phone, cart);
});

function processOrder(name, phone, items) {
  const total = items.reduce((sum, i) => sum + (i.price * i.quantity), 0);
  console.log("Order placed!", { name, phone, total });
  cart = [];
  renderCart();
}
📋 Always validate forms on BOTH the client and server. Client-side validation (JS) provides instant user feedback and saves network requests. Server-side validation is essential for security, as JS validation can be bypassed.

Lo kar liya — Key Points:

  • ✅ Design the cart state as an array of objects holding id, price, and quantity, checking for duplicates before adding
  • ✅ Use reduce to elegantly calculate the total price and total item count from the cart array
  • ✅ Implement Event Delegation on the cart container using data-action and data-id attributes to handle increment, decrement, and delete actions
  • ✅ Build HTML strings using map().join('') and update the DOM once via innerHTML to batch reflows
  • ✅ Validate checkout forms using regex and constraint checks, providing real-time feedback before processing the order
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase