Skip to main content

Shopping Cart Overview

The shopping cart uses JavaScript’s Map data structure to store product IDs and quantities. It features a visual badge that updates in real-time using CSS attribute selectors.

What You’ll Learn

  • Map data structure (key-value pairs)
  • Event delegation for dynamic elements
  • CSS attribute selectors
  • Array reduce for totals
  • Real-time UI updates

Cart Badge HTML

The cart badge is part of the header navigation:
<a href="#" class="header__nav-link header__nav-link--cart" data-cart-count="0">
  <svg><!-- Cart icon --></svg>
  <span class="header__cart-label">Carrito</span>
</a>
Key attribute: data-cart-count="0" stores the current count.

Cart Badge CSS

The badge is created entirely with CSS:
.header__nav-link--cart {
  position: relative; /* For absolute positioning of badge */
}

/* Show badge only when count > 0 */
.header__nav-link--cart[data-cart-count]:not([data-cart-count="0"])::after {
  content: attr(data-cart-count); /* Read from HTML attribute */
  
  /* Position in top-right corner */
  position: absolute;
  top: -4px;
  right: -8px;
  
  /* Badge styling */
  min-width: 18px;
  height: 18px;
  padding: 0 var(--spacing-xs);
  background-color: var(--color-error); /* Red badge */
  color: var(--color-white);
  font-size: var(--font-size-xs);
  font-weight: 600;
  border-radius: var(--border-radius-full); /* Circle */
  
  /* Center the number */
  display: flex;
  align-items: center;
  justify-content: center;
}

CSS Selector Breakdown

.header__nav-link--cart[data-cart-count]:not([data-cart-count="0"])::after
Let’s break this down:
  1. .header__nav-link--cart - The cart link element
  2. [data-cart-count] - Must have the data attribute
  3. :not([data-cart-count="0"]) - Exclude when count is “0”
  4. ::after - Pseudo-element for the badge
content: attr(data-cart-count) - This reads the value from the HTML attribute. When JavaScript updates the attribute, the badge updates automatically!

Map Data Structure

We use a Map to store cart data:
const cart: Map<number, number> = new Map();
Map<number, number> means:
  • Key (first number): Product ID
  • Value (second number): Quantity

Why Map Instead of Object?

FeatureMapObject
Key typesAny typeStrings only
Size property.sizeManual count
Iteration.forEach(), for...ofObject.keys()
PerformanceOptimized for frequent add/deleteSlower
OrderInsertion order guaranteedNot guaranteed

Map Methods

// Add or update
cart.set(productId, quantity);

// Get value (returns undefined if not found)
const qty = cart.get(productId);

// Check if exists
if (cart.has(productId)) { }

// Delete
cart.delete(productId);

// Get size
const total = cart.size;

// Get all values
const quantities = Array.from(cart.values());

// Iterate
for (const [id, qty] of cart) {
  console.log(`Product ${id}: ${qty}`);
}

Add to Cart Function

1

Get Current Quantity

Check if product already exists in cart:
function addToCart(productId: number): void {
  const currentQuantity = cart.get(productId) || 0;
  // If undefined, default to 0
}
2

Update Map

Increment quantity and save:
cart.set(productId, currentQuantity + 1);
This works for both new products and existing ones.
3

Update UI

Refresh the cart badge:
updateCartCount();
4

Show Notification

Give user feedback:
const product = appState.products.find(p => p.id === productId);
if (product) {
  showNotification(`"${product.title}" agregado al carrito`);
}

Complete Function

function addToCart(productId: number): void {
  // Get current quantity (0 if not in cart)
  const currentQuantity = cart.get(productId) || 0;
  
  // Update quantity
  cart.set(productId, currentQuantity + 1);
  
  // Update badge
  updateCartCount();
  
  // Find product for notification
  const product = appState.products.find(p => p.id === productId);
  if (product) {
    showNotification(`"${product.title}" agregado al carrito`);
  }
}

Update Cart Count

Calculate total items and update the badge:
function updateCartCount(): void {
  // Sum all quantities in the cart
  const totalItems = Array.from(cart.values())
    .reduce((sum, qty) => sum + qty, 0);
  
  // Update the data attribute
  const cartLink = document.querySelector('[data-cart-count]');
  if (cartLink) {
    cartLink.setAttribute('data-cart-count', totalItems.toString());
  }
}

Understanding reduce()

Array.from(cart.values()).reduce((sum, qty) => sum + qty, 0)
Example walkthrough: If cart contains: { 1: 2, 5: 1, 9: 3 }
  1. cart.values() → Iterator of [2, 1, 3]
  2. Array.from() → Convert to array [2, 1, 3]
  3. reduce() process:
    • Initial: sum = 0
    • Iteration 1: sum = 0 + 2 = 2
    • Iteration 2: sum = 2 + 1 = 3
    • Iteration 3: sum = 3 + 3 = 6
    • Result: 6
reduce() is perfect for calculating totals. The pattern is: reduce((accumulator, currentValue) => newAccumulator, initialValue)

Event Delegation

We use event delegation to handle clicks on dynamically created buttons:
function setupAddToCartButtons(): void {
  const grid = getElement<HTMLDivElement>("#products-grid");
  
  // One listener on parent, handles all product buttons
  grid.addEventListener('click', (event: MouseEvent) => {
    const target = event.target as HTMLElement;
    
    // Find the button (even if icon inside was clicked)
    const button = target.closest('[data-action="add-to-cart"]');
    if (!button) return; // Not a cart button
    
    // Find the product card to get ID
    const card = button.closest('.product-card') as HTMLElement;
    if (!card) return;
    
    // Get product ID from data attribute
    const productId = card.dataset.productId;
    if (productId) {
      addToCart(parseInt(productId, 10));
    }
  });
}

Why Event Delegation?

Without delegation (bad):
// Need to add listener to EVERY button
const buttons = document.querySelectorAll('.product-card__btn');
buttons.forEach(btn => {
  btn.addEventListener('click', handleClick); // 100 listeners!
});
With delegation (good):
// ONE listener on parent
grid.addEventListener('click', handleClick); // 1 listener!
Benefits:
  • Only one event listener (better performance)
  • Works with dynamically added products
  • Less memory usage

Event Bubbling

When you click a button, the event “bubbles” up:
Button (click here)

Product Card

Products Grid (listener here)

Body

HTML
The grid’s listener catches all clicks inside it.

Toast Notification

Show a temporary message when adding to cart:
function showNotification(message: string, duration: number = 3000): void {
  // Create notification element
  const notification = document.createElement('div');
  
  // Apply inline styles
  notification.style.cssText = `
    position: fixed;
    bottom: 20px;
    right: 20px;
    padding: 16px 24px;
    background-color: #333;
    color: white;
    border-radius: 8px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.15);
    z-index: 1000;
    animation: slideIn 0.3s ease;
    max-width: 300px;
  `;
  
  notification.textContent = message;
  document.body.appendChild(notification);
  
  // Remove after duration
  setTimeout(() => {
    notification.style.animation = 'slideOut 0.3s ease';
    setTimeout(() => notification.remove(), 300);
  }, duration);
}

Notification Animations

@keyframes slideIn {
  from { 
    transform: translateX(100%); 
    opacity: 0; 
  }
  to { 
    transform: translateX(0); 
    opacity: 1; 
  }
}

@keyframes slideOut {
  from { 
    transform: translateX(0); 
    opacity: 1; 
  }
  to { 
    transform: translateX(100%); 
    opacity: 0; 
  }
}

Cart Summary Example

Here’s how you could display cart contents:
function displayCartSummary(): void {
  console.log('=== CART SUMMARY ===');
  
  let total = 0;
  
  for (const [productId, quantity] of cart) {
    const product = appState.products.find(p => p.id === productId);
    if (product) {
      const subtotal = product.price * quantity;
      total += subtotal;
      
      console.log(`${product.title}`);
      console.log(`  Quantity: ${quantity}`);
      console.log(`  Price: $${product.price}`);
      console.log(`  Subtotal: $${subtotal}`);
    }
  }
  
  console.log(`TOTAL: $${total}`);
}

Data Flow Diagram

User clicks "Add to Cart"

Event bubbles to grid listener

Extract product ID from data attribute

addToCart(productId)

cart.set(id, quantity + 1)

updateCartCount()

Calculate total with reduce()

Update data-cart-count attribute

CSS ::after automatically shows new badge

showNotification()

Complete Code Reference

Map Declaration: /workspace/source/mi-tutorial/src/main.ts:283 Add to Cart: /workspace/source/mi-tutorial/src/main.ts:760-779 Update Count: /workspace/source/mi-tutorial/src/main.ts:812-827 Event Delegation: /workspace/source/mi-tutorial/src/main.ts:703-737 Notifications: /workspace/source/mi-tutorial/src/main.ts:847-882

Enhancement Ideas

function removeFromCart(productId: number): void {
  cart.delete(productId);
  updateCartCount();
  showNotification('Producto eliminado del carrito');
}
function updateQuantity(productId: number, newQuantity: number): void {
  if (newQuantity <= 0) {
    cart.delete(productId);
  } else {
    cart.set(productId, newQuantity);
  }
  updateCartCount();
}
function clearCart(): void {
  cart.clear();
  updateCartCount();
  showNotification('Carrito vaciado');
}
function saveCart(): void {
  const cartArray = Array.from(cart.entries());
  localStorage.setItem('cart', JSON.stringify(cartArray));
}

function loadCart(): void {
  const saved = localStorage.getItem('cart');
  if (saved) {
    const cartArray = JSON.parse(saved);
    cartArray.forEach(([id, qty]) => cart.set(id, qty));
    updateCartCount();
  }
}

Next Steps

API Integration

Learn how product data is fetched from the server

State Management

Understand how application state is managed

Build docs developers (and LLMs) love