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 : -4 px ;
right : -8 px ;
/* Badge styling */
min-width : 18 px ;
height : 18 px ;
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:
.header__nav-link--cart - The cart link element
[data-cart-count] - Must have the data attribute
:not([data-cart-count="0"]) - Exclude when count is “0”
::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?
Feature Map Object Key types Any type Strings only Size property .sizeManual count Iteration .forEach(), for...ofObject.keys()Performance Optimized for frequent add/delete Slower Order Insertion order guaranteed Not 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
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
}
Update Map
Increment quantity and save: cart . set ( productId , currentQuantity + 1 );
This works for both new products and existing ones.
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 }
cart.values() → Iterator of [2, 1, 3]
Array.from() → Convert to array [2, 1, 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