How to build an e-commerce cart in Angular
Building a robust e-commerce cart is a fundamental requirement for any online storefront, yet managing state effectively across multiple components can often become complex. With over 25 years of experience in software development and as the creator of CoreUI, I have architected numerous enterprise-level e-commerce systems using Angular since its early versions. From my expertise, the most efficient and modern solution for Angular applications is to leverage a singleton service combined with Angular Signals for reactive state management. This approach ensures that your cart data is synchronized across the entire application without the overhead of heavy state management libraries.
Use an Angular service with signal and computed to manage the cart state globally and reactively.
import { Injectable, signal, computed } from '@angular/core'
export interface CartItem {
id: number
name: string
price: number
quantity: number
image: string
}
@Injectable({
providedIn: 'root'
})
export class CartService {
private cartItems = signal<CartItem[]>([])
// Computed signal for the total number of items
cartCount = computed(() =>
this.cartItems().reduce((acc, item) => acc + item.quantity, 0)
)
// Computed signal for the total price
totalPrice = computed(() =>
this.cartItems().reduce((acc, item) => acc + (item.price * item.quantity), 0)
)
getCart() {
return this.cartItems.asReadonly()
}
addToCart(product: CartItem) {
this.cartItems.update(items => {
const existing = items.find(i => i.id === product.id)
if (existing) {
return items.map(i => i.id === product.id
? { ...i, quantity: i.quantity + 1 }
: i
)
}
return [...items, { ...product, quantity: 1 }]
})
}
removeItem(id: number) {
this.cartItems.update(items => items.filter(i => i.id !== id))
}
updateQuantity(id: number, quantity: number) {
if (quantity <= 0) {
this.removeItem(id)
return
}
this.cartItems.update(items =>
items.map(i => i.id === id ? { ...i, quantity } : i)
)
}
}
This implementation utilizes a singleton service to hold the cart’s state using the signal primitive. By using computed signals, we automatically derive values like the total item count and the total price whenever the underlying cartItems signal changes. The addToCart method checks for existing items to increment quantity instead of creating duplicates, while updateQuantity and removeItem provide standard management functions. This architecture is highly performant because Angular only updates the specific parts of the DOM that depend on these signals, making it ideal for large-scale production apps.
Implementing the Product Card
To display products that users can add to the cart, we can use the CoreUI Card component to create a clean, professional look.
import { Component, Input, inject } from '@angular/core'
import { CurrencyPipe } from '@angular/common'
import { CardModule, ButtonModule, GridModule } from '@coreui/angular'
import { CartService, CartItem } from './cart.service'
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CurrencyPipe, CardModule, ButtonModule, GridModule],
template: `
<c-card style="width: 18rem;">
<img cCardImg="top" [src]="product.image" [alt]="product.name">
<c-card-body>
<h5 cCardTitle>{{ product.name }}</h5>
<p cCardText>{{ product.price | currency }}</p>
<button cButton color="primary" (click)="addToCart()">
Add to Cart
</button>
</c-card-body>
</c-card>
`
})
export class ProductCardComponent {
@Input({ required: true }) product!: CartItem
private cartService = inject(CartService)
addToCart() {
this.cartService.addToCart(this.product)
}
}
Creating the Cart Table View
For the checkout or cart summary page, we utilize the CoreUI Table component to list the items and allow for quantity adjustments.
import { Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { TableModule, ButtonModule, FormModule } from '@coreui/angular'
import { CartService } from './cart.service'
@Component({
selector: 'app-cart-list',
standalone: true,
imports: [CommonModule, TableModule, ButtonModule, FormModule],
template: `
<table cTable hover>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Quantity</th>
<th>Subtotal</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of cartItems()">
<td>{{ item.name }}</td>
<td>{{ item.price | currency }}</td>
<td>
<input cFormControl type="number"
[value]="item.quantity"
(change)="updateQty(item.id, $event)">
</td>
<td>{{ (item.price * item.quantity) | currency }}</td>
<td>
<button cButton color="danger" variant="outline"
(click)="remove(item.id)">
Remove
</button>
</td>
</tr>
</tbody>
</table>
`
})
export class CartListComponent {
private cartService = inject(CartService)
cartItems = this.cartService.getCart()
updateQty(id: number, event: Event) {
const val = parseInt((event.target as HTMLInputElement).value, 10)
this.cartService.updateQuantity(id, val)
}
remove(id: number) {
this.cartService.removeItem(id)
}
}
Handling Cart Summary and Totals
The final piece is a summary component that displays the total cost. This is where we leverage the computed signals from our service for real-time updates.
import { Component, inject } from '@angular/core'
import { CommonModule } from '@angular/common'
import { CardModule, ButtonModule } from '@coreui/angular'
import { CartService } from './cart.service'
@Component({
selector: 'app-cart-summary',
standalone: true,
imports: [CommonModule, CardModule, ButtonModule],
template: `
<c-card class="mb-4">
<c-card-body>
<h5 cCardTitle>Order Summary</h5>
<div class="d-flex justify-content-between mb-2">
<span>Items:</span>
<span>{{ cartCount() }}</span>
</div>
<div class="d-flex justify-content-between fw-bold border-top pt-2">
<span>Total:</span>
<span>{{ totalPrice() | currency }}</span>
</div>
<button cButton color="success" class="w-100 mt-3">
Proceed to Checkout
</button>
</c-card-body>
</c-card>
`
})
export class CartSummaryComponent {
private cartService = inject(CartService)
cartCount = this.cartService.cartCount
totalPrice = this.cartService.totalPrice
}
Persisting Cart State
In a real-world scenario, you want the cart to survive page refreshes. You can easily extend the CartService to use localStorage within an effect.
import { effect } from '@angular/core'
// Inside CartService constructor
constructor() {
const saved = localStorage.getItem('cart_data')
if (saved) {
this.cartItems.set(JSON.parse(saved))
}
effect(() => {
localStorage.setItem('cart_data', JSON.stringify(this.cartItems()))
})
}
Best Practice Note:
Using Signals for e-commerce carts is significantly more efficient than using Observables for simple UI state, as it removes the need for manual subscription management and pipe transformations. This is the same pattern we recommend in CoreUI templates for building scalable Angular Dashboard Templates where data consistency is paramount. For formatting the final prices, you might want to look at how to format a number as currency in JavaScript to ensure localized precision. If you are just starting your project, ensure you know how to create a new Angular project with the latest standalone component defaults.



