Welcome to the Basic Level of the Angular Shopping Cart Workshop! In this level, you'll implement a reactive shopping cart using traditional RxJS patterns with BehaviorSubjects and Observables.
By completing this level, you will:
- Master RxJS
BehaviorSubjectandObservablepatterns - Implement reactive state management for a shopping cart
- Handle asynchronous operations and side effects
- Create reactive UI components that respond to state changes
- Understand data flow in reactive applications
- Implement local storage persistence
Primary Files:
src/app/basic/services/shopping-cart-rxjs.service.ts- STARTER FILE (your main workspace)src/app/basic/components/cart-basic.component.ts- UI component (pre-built)
Reference Files:
src/app/basic/services/shopping-cart-signals.service.ts- SOLUTION (don't peek too early!)src/app/shared/services/product.service.ts- Product data service (pre-built)
The RxJS implementation follows these patterns:
βββββββββββββββββββββββββββββββββββββββββββββββ
β Component Layer β
β βββββββββββββββββββββββββββββββββββββββ β
β β CartBasicComponent β β
β β - Subscribes to cart state β β
β β - Displays products and cart β β
β β - Handles user interactions β β
β βββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β subscribes to
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β βββββββββββββββββββββββββββββββββββββββ β
β β ShoppingCartRxjsService β β
β β - BehaviorSubject<CartItem[]> β β
β β - Observable streams β β
β β - State management logic β β
β β - LocalStorage persistence β β
β βββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββ
Open src/app/basic/services/shopping-cart-rxjs.service.ts and examine the starter code:
@Injectable({ providedIn: 'root' })
export class ShoppingCartRxjsService {
private itemsSubject = new BehaviorSubject<CartItem[]>([]);
items$ = this.itemsSubject.asObservable();
constructor() {
// TODO: Load items from localStorage if available
}
// TODO: Implement methods
}- Start the development server:
npm start - Open your browser to
http://localhost:4200 - Click on "Basic Level" in the navigation
- You should see the basic shopping cart interface
Goal: Set up the service constructor with localStorage loading.
Requirements:
constructor() {
// Load cart from localStorage on initialization
this.loadCartFromStorage();
}Key Concepts:
- Service initialization
- LocalStorage persistence setup
- Preparation for reactive patterns
Goal: Add products to the cart, handling both new items and quantity updates.
Method Signature:
addItem(product: Product): voidRequirements:
- If item doesn't exist, add it with quantity 1
- If item exists, increase quantity by 1
- Update the BehaviorSubject with new state
- Save to localStorage
Hints:
addItem(product: Product): void {
const currentItems = this.itemsSubject.value;
const existingItem = currentItems.find(item => item.productId === product.id);
if (existingItem) {
// TODO: Update quantity
} else {
// TODO: Create new CartItem and add to array
}
// TODO: Update BehaviorSubject and save to storage
}Goal: Remove items from the cart completely.
Method Signature:
removeItem(productId: string): voidRequirements:
- Remove item with matching productId
- Update BehaviorSubject
- Save to localStorage
Goal: Update the quantity of an existing cart item.
Method Signature:
updateQuantity(productId: string, quantity: number): voidRequirements:
- If quantity β€ 0, remove the item
- Otherwise, update the item's quantity
- Update BehaviorSubject
- Save to localStorage
Goal: Remove all items from the cart.
Method Signature:
clearCart(): voidRequirements:
- Set items to empty array
- Update BehaviorSubject
- Clear localStorage
Goal: Create an Observable that provides calculated cart totals.
Method Signature:
getCartSummary(): Observable<CartSummary>Requirements:
- Return Observable that emits CartSummary
- Calculate total items, total price, discounts, tax, and final price
- Use the
mapoperator to transform cart items
CartSummary Interface:
interface CartSummary {
totalItems: number;
totalPrice: number;
totalDiscount: number;
tax: number; // 8% of (totalPrice - totalDiscount)
finalPrice: number; // totalPrice - totalDiscount + tax
}Goal: Create an Observable that emits the total number of items.
Method Signature:
getTotalItems(): Observable<number>Requirements:
- Return Observable
- Sum all item quantities
- Use RxJS operators
Goal: Complete the utility methods for the service.
Methods to Implement:
private calculateTotal(items: CartItem[]): number {
// Calculate the total price of all items
// Include quantity multiplication
}
private generateId(): string {
// Generate unique ID for cart items
// Combine timestamp and random string
}
private saveCartToStorage(): void {
// Save current cart items to localStorage
// Handle browser compatibility
}
private loadCartFromStorage(): void {
// Load cart items from localStorage
// Handle JSON parsing errors gracefully
}- Add Items: Click "Add to Cart" on various products
- Quantity Updates: Use +/- buttons to modify quantities
- Remove Items: Click "Remove" buttons
- Clear Cart: Use "Clear Cart" button
- Persistence: Refresh the page and verify cart persists
- Calculations: Verify totals, discounts, and tax are correct
Run the test suite for the basic level:
addItem(product: Product): void {
const currentItems = this.itemsSubject.value;
const existingItem = currentItems.find(item => item.productId === product.id);
if (existingItem) {
// Item exists, increase quantity
this.updateQuantity(product.id, existingItem.quantity + 1);
} else {
// New item, add to cart
const newItem: CartItem = {
id: this.generateId(),
productId: product.id,
name: product.name,
price: product.price,
quantity: 1,
image: product.image,
category: product.category,
discount: product.discount
};
const updatedItems = [...currentItems, newItem];
this.itemsSubject.next(updatedItems);
this.saveCartToStorage();
}
}getCartSummary(): Observable<CartSummary> {
return this.items$.pipe(
map(items => {
const totalItems = items.reduce((sum, item) => sum + item.quantity, 0);
const totalPrice = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
const totalDiscount = items.reduce((sum, item) => {
const discount = item.discount || 0;
return sum + (item.price * item.quantity * discount / 100);
}, 0);
const tax = (totalPrice - totalDiscount) * 0.08;
const finalPrice = totalPrice - totalDiscount + tax;
return {
totalItems,
totalPrice,
totalDiscount,
tax,
finalPrice
};
})
);
}Issue: "Cannot read property 'value' of undefined" Solution: Ensure BehaviorSubject is properly initialized
Issue: Items not persisting after page refresh Solution: Check localStorage browser compatibility and error handling
Issue: Cart total not updating automatically
Solution: Verify subscription in constructor is set up correctly
- Open Angular DevTools in your browser
- Navigate to the "Components" tab
- Select the CartBasicComponent
- Observe the service subscriptions and data flow
Add debug logging to your service:
addItem(product: Product): void {
console.log('Adding item:', product);
// ... implementation
console.log('Current cart:', this.itemsSubject.value);
}- Memory Management: Component properly unsubscribes in
ngOnDestroy - Efficient Updates: Using
BehaviorSubject.next()for state updates - Derived State: Automatic total calculation via subscription
- Immutability: Creating new arrays instead of mutating existing ones
- How to manage reactive state with RxJS
- Subscription management and cleanup
- Data transformation with operators
- Side effect handling (localStorage)
- Testing reactive services
You've successfully completed the Basic Level when:
- β Cart functionality works in the browser
- β Items persist across page refreshes
- β Calculations are accurate (including tax and discounts)
- β Code follows RxJS best practices
- β No memory leaks or subscription issues
Once you've completed the Basic Level:
- Review your implementation against the solution file
- Understand the RxJS patterns you've implemented
- Consider the challenges of this approach (subscription management, boilerplate)
- Move to Intermediate Level to see how Signals can simplify this code
The RxJS approach provides:
- Powerful reactive patterns for complex data flows
- Fine-grained control over subscriptions and operators
- Mature ecosystem with extensive operator library
- Excellent interoperability with existing Angular code
However, it also introduces:
- Subscription management complexity
- Memory leak potential if not handled properly
- Verbose syntax for simple state management
- Learning curve for developers new to reactive programming
In the next level, you'll see how Angular Signals can address these challenges while maintaining the reactive benefits!