Services and Dependency Injection in Angular

Module 25: Frontend Frameworks & State Management

Understanding Angular Services

Services in Angular are specialized classes that serve a specific purpose in your application. They encapsulate non-UI logic and data that can be shared across components.

Why Use Services?

Real-world analogy: If components are like the front desk staff at a hotel (interacting with guests), services are like the back-office staff (managing reservations, handling payments, maintaining guest records). The front desk doesn't need to know all the details of how the back-office works, just how to interact with it.

graph TD A[Component 1] -->|Uses| C[Service] B[Component 2] -->|Uses| C D[Component 3] -->|Uses| C style A fill:#dd0031 style B fill:#dd0031 style C fill:#3498db style D fill:#dd0031 classDef default stroke:#333,stroke-width:2px;

Common Types of Services

Creating and Using Services

Creating a Service with Angular CLI

// Generate a service
ng generate service services/data
// or shorthand
ng g s services/data

This creates a service file with the following structure:

// data.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor() { }
}

The @Injectable Decorator

The @Injectable decorator marks a class as available for dependency injection. The providedIn: 'root' option registers the service at the application level, making it a singleton available throughout the app.

Implementing a Service

Let's build a simple product service:

// product.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { Product } from '../models/product';

@Injectable({
  providedIn: 'root'
})
export class ProductService {
  private apiUrl = 'https://api.example.com/products';
  
  constructor(private http: HttpClient) { }
  
  getProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(this.apiUrl)
      .pipe(
        catchError(this.handleError)
      );
  }
  
  getProduct(id: number): Observable<Product> {
    return this.http.get<Product>(`${this.apiUrl}/${id}`)
      .pipe(
        catchError(this.handleError)
      );
  }
  
  createProduct(product: Omit<Product, 'id'>): Observable<Product> {
    return this.http.post<Product>(this.apiUrl, product)
      .pipe(
        catchError(this.handleError)
      );
  }
  
  updateProduct(product: Product): Observable<Product> {
    return this.http.put<Product>(`${this.apiUrl}/${product.id}`, product)
      .pipe(
        catchError(this.handleError)
      );
  }
  
  deleteProduct(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`)
      .pipe(
        catchError(this.handleError)
      );
  }
  
  private handleError(error: any) {
    console.error('An error occurred', error);
    return throwError(() => new Error('Something went wrong. Please try again later.'));
  }
}

Using a Service in Components

To use a service, inject it in the component's constructor:

// product-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../services/product.service';
import { Product } from '../models/product';

@Component({
  selector: 'app-product-list',
  templateUrl: './product-list.component.html'
})
export class ProductListComponent implements OnInit {
  products: Product[] = [];
  loading = false;
  error: string | null = null;
  
  constructor(private productService: ProductService) { }
  
  ngOnInit(): void {
    this.loading = true;
    this.productService.getProducts()
      .subscribe({
        next: (products) => {
          this.products = products;
          this.loading = false;
        },
        error: (err) => {
          this.error = err.message;
          this.loading = false;
        }
      });
  }
  
  deleteProduct(id: number): void {
    this.productService.deleteProduct(id)
      .subscribe({
        next: () => {
          this.products = this.products.filter(p => p.id !== id);
        },
        error: (err) => {
          this.error = `Failed to delete product: ${err.message}`;
        }
      });
  }
}

Understanding Dependency Injection (DI)

Dependency Injection is a fundamental concept in Angular. It's a design pattern that lets you declare dependencies without creating them directly.

How DI Works in Angular

Angular's DI system consists of three main parts:

  1. Provider: Tells Angular how to create or obtain a dependency
  2. Injector: Maintains a container of dependency instances and creates new ones when needed
  3. Consumer: The class that declares dependencies through constructor parameters
graph TD A[Injector] --> B[Looks up provider] B --> C{Provider exists?} C -->|Yes| D[Creates/reuses instance] C -->|No| E[Error: No provider] D --> F[Injects into consumer] style A fill:#3498db style B fill:#3498db style C fill:#3498db style D fill:#3498db style E fill:#e74c3c style F fill:#3498db classDef default stroke:#333,stroke-width:2px;

Benefits of Dependency Injection

Real-world analogy: Imagine you're building a car (component). Instead of manufacturing your own engine, wheels, and electronics (services), you specify what you need, and the car factory (injector) provides these components. This allows you to focus on building your car without worrying about how each individual part is made.

Service Providers and Hierarchical Injection

Angular's dependency injection system is hierarchical, following the component tree structure.

Provider Scopes

Services can be provided at different levels:

graph TD A[Root Injector] --> B[Module Injector 1] A --> C[Module Injector 2] B --> D[Component Injector 1] B --> E[Component Injector 2] C --> F[Component Injector 3] style A fill:#3498db style B fill:#3498db style C fill:#3498db style D fill:#3498db style E fill:#3498db style F fill:#3498db classDef default stroke:#333,stroke-width:2px;

Root-Level Provider

@Injectable({
  providedIn: 'root'  // Available application-wide
})
export class GlobalService { }

Module-Level Provider

@NgModule({
  declarations: [/* components */],
  imports: [/* other modules */],
  providers: [
    FeatureService  // Available to all components in this module
  ]
})
export class FeatureModule { }

Component-Level Provider

@Component({
  selector: 'app-feature',
  templateUrl: './feature.component.html',
  providers: [
    LocalService  // Available only to this component and its children
  ]
})
export class FeatureComponent { }

Resolution Rules

When a component requests a service, Angular looks for a provider in this order:

  1. The component's own injector (if the service is provided at the component level)
  2. Parent component injectors, up the component tree
  3. The module injector
  4. The root injector

If no provider is found, Angular throws an error.

Practical Example: Component-Specific Services

// local-counter.service.ts
@Injectable()  // No providedIn: 'root'
export class LocalCounterService {
  private count = 0;
  
  increment(): void {
    this.count++;
  }
  
  decrement(): void {
    this.count--;
  }
  
  getCount(): number {
    return this.count;
  }
}

// parent.component.ts
@Component({
  selector: 'app-parent',
  template: `
    <h2>Parent Counter: {{ getCount() }}</h2>
    <button (click)="increment()">Increment</button>
    <app-child></app-child>
    <app-child></app-child>
  `,
  providers: [LocalCounterService]  // Provided at component level
})
export class ParentComponent {
  constructor(private counter: LocalCounterService) { }
  
  increment(): void {
    this.counter.increment();
  }
  
  getCount(): number {
    return this.counter.getCount();
  }
}

// child.component.ts
@Component({
  selector: 'app-child',
  template: `
    <div class="child">
      <h3>Child Counter: {{ getCount() }}</h3>
      <button (click)="increment()">Increment</button>
    </div>
  `
})
export class ChildComponent {
  constructor(private counter: LocalCounterService) { }
  
  increment(): void {
    this.counter.increment();
  }
  
  getCount(): number {
    return this.counter.getCount();
  }
}

In this example, both child components use the same instance of LocalCounterService as the parent, because the service is provided at the parent level. If we move the provider to the ChildComponent, each child would have its own counter instance.

Advanced Dependency Injection

Using Value and Factory Providers

Angular's DI system supports various provider types:

1. Class Provider (Default)

providers: [
  SomeService  // Shorthand for { provide: SomeService, useClass: SomeService }
]

2. Value Provider

// Provide a fixed value
providers: [
  { provide: 'API_URL', useValue: 'https://api.example.com' }
]

// Inject with @Inject
constructor(@Inject('API_URL') private apiUrl: string) { }

3. Factory Provider

// Define a factory function
export function configFactory(http: HttpClient) {
  return () => http.get<AppConfig>('/assets/config.json');
}

// Provide the factory
providers: [
  {
    provide: APP_INITIALIZER,
    useFactory: configFactory,
    deps: [HttpClient],
    multi: true
  }
]

4. Existing Provider (Alias)

// Use an existing service under a different token
providers: [
  { provide: AbstractLogger, useExisting: ConsoleLogger }
]

Configuration Injection Example

// config.ts
export interface AppConfig {
  apiUrl: string;
  theme: 'light' | 'dark';
  features: {
    notifications: boolean;
    analytics: boolean;
  };
}

// Injection token
import { InjectionToken } from '@angular/core';
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

// Providing configuration
@NgModule({
  providers: [
    {
      provide: APP_CONFIG,
      useValue: {
        apiUrl: 'https://api.example.com',
        theme: 'light',
        features: {
          notifications: true,
          analytics: true
        }
      }
    }
  ]
})
export class AppModule { }

// Using the config
@Injectable()
export class ApiService {
  constructor(@Inject(APP_CONFIG) private config: AppConfig) {
    console.log('API URL:', config.apiUrl);
  }
}

Using InjectionToken for Type Safety

InjectionToken provides type safety for non-class dependencies:

// Define a typed token
export const API_URL = new InjectionToken<string>('api.url');

// Provide a value
providers: [
  { provide: API_URL, useValue: 'https://api.example.com' }
]

// Inject in a service
constructor(@Inject(API_URL) private apiUrl: string) { }

Optional Dependencies

You can mark dependencies as optional:

import { Optional } from '@angular/core';

@Injectable()
export class FlexibleService {
  constructor(@Optional() private logger: LoggerService) {
    if (logger) {
      logger.log('FlexibleService instantiated');
    }
  }
}

Self, SkipSelf, and Host

Angular provides decorators to control where dependencies are resolved from:

import { Self, SkipSelf, Host } from '@angular/core';

@Component({
  providers: [SomeService]  // Component provides its own instance
})
export class MyComponent {
  constructor(
    @Self() private fromSelf: SomeService,      // Only check this component's injector
    @SkipSelf() private fromAncestor: SomeService, // Skip this component's injector
    @Host() private fromHost: SomeService       // Check only up to the host component
  ) { }
}

Stateful Services for State Management

Services can be used to manage state across components in Angular applications.

Simple State Service

// cart.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Product } from '../models/product';

export interface CartItem {
  product: Product;
  quantity: number;
}

@Injectable({
  providedIn: 'root'
})
export class CartService {
  // BehaviorSubject for the internal state
  private cartItems = new BehaviorSubject<CartItem[]>([]);
  
  // Expose as Observable for components to subscribe to
  cartItems$ = this.cartItems.asObservable();
  
  // Computed Observable for total items
  totalItems$: Observable<number> = this.cartItems$.pipe(
    map(items => items.reduce((total, item) => total + item.quantity, 0))
  );
  
  // Computed Observable for total price
  totalPrice$: Observable<number> = this.cartItems$.pipe(
    map(items => items.reduce(
      (total, item) => total + (item.product.price * item.quantity),
      0
    ))
  );
  
  addToCart(product: Product, quantity = 1): void {
    const currentItems = this.cartItems.getValue();
    const existingItemIndex = currentItems.findIndex(
      item => item.product.id === product.id
    );
    
    if (existingItemIndex > -1) {
      // Update quantity if product already exists
      const updatedItems = [...currentItems];
      updatedItems[existingItemIndex] = {
        ...updatedItems[existingItemIndex],
        quantity: updatedItems[existingItemIndex].quantity + quantity
      };
      
      this.cartItems.next(updatedItems);
    } else {
      // Add new product to cart
      this.cartItems.next([
        ...currentItems,
        { product, quantity }
      ]);
    }
  }
  
  updateQuantity(productId: number, quantity: number): void {
    if (quantity <= 0) {
      this.removeFromCart(productId);
      return;
    }
    
    const currentItems = this.cartItems.getValue();
    const updatedItems = currentItems.map(item => 
      item.product.id === productId
        ? { ...item, quantity }
        : item
    );
    
    this.cartItems.next(updatedItems);
  }
  
  removeFromCart(productId: number): void {
    const currentItems = this.cartItems.getValue();
    const updatedItems = currentItems.filter(
      item => item.product.id !== productId
    );
    
    this.cartItems.next(updatedItems);
  }
  
  clearCart(): void {
    this.cartItems.next([]);
  }
}

Using the State Service in Components

// product-detail.component.ts
@Component({
  selector: 'app-product-detail',
  template: `
    <div *ngIf="product">
      <h2>{{ product.name }}</h2>
      <p>{{ product.price | currency }}</p>
      <button (click)="addToCart()">Add to Cart</button>
    </div>
  `
})
export class ProductDetailComponent {
  @Input() product!: Product;
  
  constructor(private cartService: CartService) { }
  
  addToCart(): void {
    this.cartService.addToCart(this.product);
  }
}

// cart-summary.component.ts
@Component({
  selector: 'app-cart-summary',
  template: `
    <div class="cart-summary">
      <span>{{ totalItems$ | async }} items</span>
      <span>{{ totalPrice$ | async | currency }}</span>
    </div>
  `
})
export class CartSummaryComponent {
  totalItems$: Observable<number>;
  totalPrice$: Observable<number>;
  
  constructor(private cartService: CartService) {
    this.totalItems$ = this.cartService.totalItems$;
    this.totalPrice$ = this.cartService.totalPrice$;
  }
}

Advantages of Service-Based State Management

When to Use More Advanced State Management

For larger applications, you might consider more robust solutions like NgRx or NGXS when:

Integrating with HttpClient

Angular's HttpClient is used for making API requests, and is typically wrapped in services:

Basic HttpClient Example

// Import HttpClientModule in your app/feature module
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule
    // other imports
  ]
})
export class AppModule { }

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from './user.model';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';
  
  constructor(private http: HttpClient) { }
  
  getUsers(page = 1, limit = 10): Observable<User[]> {
    let params = new HttpParams()
      .set('page', page.toString())
      .set('limit', limit.toString());
      
    return this.http.get<User[]>(this.apiUrl, { params });
  }
  
  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

Error Handling

import { catchError, retry } from 'rxjs/operators';
import { throwError } from 'rxjs';

getUsers(): Observable<User[]> {
  return this.http.get<User[]>(this.apiUrl)
    .pipe(
      retry(2), // Retry failed requests up to 2 times
      catchError(this.handleError)
    );
}

private handleError(error: any): Observable<never> {
  let errorMessage = 'An unknown error occurred';
  
  if (error.error instanceof ErrorEvent) {
    // Client-side error
    errorMessage = `Error: ${error.error.message}`;
  } else {
    // Server-side error
    errorMessage = `Error Code: ${error.status}\nMessage: ${error.message}`;
  }
  
  console.error(errorMessage);
  return throwError(() => new Error(errorMessage));
}

HTTP Interceptors

Interceptors allow you to modify HTTP requests and responses globally:

// auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}
  
  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    // Get the auth token from the service
    const authToken = this.authService.getToken();
    
    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    if (authToken) {
      const authReq = request.clone({
        headers: request.headers.set('Authorization', `Bearer ${authToken}`)
      });
      
      // Send cloned request with header to the next handler.
      return next.handle(authReq);
    }
    
    return next.handle(request);
  }
}

// Register in app.module.ts
import { HTTP_INTERCEPTORS } from '@angular/common/http';

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class AppModule { }

Multiple Interceptors

You can register multiple interceptors that run in order:

@NgModule({
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
  ]
})
export class AppModule { }

Progress and Loading Indicators

// loading.service.ts
@Injectable({
  providedIn: 'root'
})
export class LoadingService {
  private loadingSubject = new BehaviorSubject<boolean>(false);
  loading$ = this.loadingSubject.asObservable();
  
  setLoading(isLoading: boolean): void {
    this.loadingSubject.next(isLoading);
  }
}

// loading.interceptor.ts
@Injectable()
export class LoadingInterceptor implements HttpInterceptor {
  private totalRequests = 0;
  
  constructor(private loadingService: LoadingService) {}
  
  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    this.totalRequests++;
    this.loadingService.setLoading(true);
    
    return next.handle(request).pipe(
      finalize(() => {
        this.totalRequests--;
        if (this.totalRequests === 0) {
          this.loadingService.setLoading(false);
        }
      })
    );
  }
}

Testing Services

Angular services are easier to test than components because they don't involve the DOM:

Unit Testing a Service

// cart.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
import { Product } from '../models/product';

describe('CartService', () => {
  let service: CartService;
  
  // Sample product for testing
  const testProduct: Product = {
    id: 1,
    name: 'Test Product',
    price: 100,
    imageUrl: 'test.jpg',
    description: 'Test description'
  };
  
  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CartService);
  });
  
  it('should be created', () => {
    expect(service).toBeTruthy();
  });
  
  it('should add an item to the cart', (done) => {
    service.addToCart(testProduct, 2);
    
    service.cartItems$.subscribe(items => {
      expect(items.length).toBe(1);
      expect(items[0].product).toEqual(testProduct);
      expect(items[0].quantity).toBe(2);
      done();
    });
  });
  
  it('should update quantity when adding an existing item', (done) => {
    service.addToCart(testProduct, 1);
    service.addToCart(testProduct, 2);
    
    service.cartItems$.subscribe(items => {
      expect(items.length).toBe(1);
      expect(items[0].quantity).toBe(3);
      done();
    });
  });
  
  it('should calculate total price correctly', (done) => {
    service.addToCart(testProduct, 2); // 2 * $100 = $200
    
    service.totalPrice$.subscribe(total => {
      expect(total).toBe(200);
      done();
    });
  });
  
  it('should remove an item from the cart', (done) => {
    service.addToCart(testProduct);
    service.removeFromCart(testProduct.id);
    
    service.cartItems$.subscribe(items => {
      expect(items.length).toBe(0);
      done();
    });
  });
  
  it('should clear the cart', (done) => {
    service.addToCart(testProduct);
    service.clearCart();
    
    service.cartItems$.subscribe(items => {
      expect(items.length).toBe(0);
      done();
    });
  });
});

Testing Services with Dependencies

// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';
import { User } from '../models/user';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify(); // Ensure that there are no outstanding requests
  });
  
  it('should retrieve users', () => {
    const mockUsers: User[] = [
      { id: 1, name: 'John', email: 'john@example.com' },
      { id: 2, name: 'Jane', email: 'jane@example.com' }
    ];
    
    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });
    
    const req = httpMock.expectOne('https://api.example.com/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers); // Provide mock data
  });
  
  it('should create a user', () => {
    const newUser = { name: 'Alice', email: 'alice@example.com' };
    const mockResponse = { id: 3, ...newUser };
    
    service.createUser(newUser).subscribe(user => {
      expect(user.id).toBe(3);
      expect(user.name).toBe(newUser.name);
    });
    
    const req = httpMock.expectOne('https://api.example.com/users');
    expect(req.request.method).toBe('POST');
    expect(req.request.body).toEqual(newUser);
    req.flush(mockResponse);
  });
});

Testing with Service Mocks

// Testing a component that uses a service
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ProductListComponent } from './product-list.component';
import { ProductService } from '../services/product.service';

// Create a mock service
class MockProductService {
  getProducts() {
    return of([
      { id: 1, name: 'Test Product', price: 100 }
    ]);
  }
  
  deleteProduct() {
    return of(null);
  }
}

describe('ProductListComponent', () => {
  let component: ProductListComponent;
  let fixture: ComponentFixture<ProductListComponent>;
  let productService: ProductService;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ProductListComponent],
      providers: [
        // Provide the mock service instead of the real one
        { provide: ProductService, useClass: MockProductService }
      ]
    })
    .compileComponents();
  });
  
  beforeEach(() => {
    fixture = TestBed.createComponent(ProductListComponent);
    component = fixture.componentInstance;
    productService = TestBed.inject(ProductService);
    fixture.detectChanges();
  });
  
  it('should load products on init', () => {
    expect(component.products.length).toBe(1);
    expect(component.products[0].name).toBe('Test Product');
  });
});

Service Design Patterns

Repository Pattern

This pattern abstracts the data access layer:

// user.repository.ts
@Injectable({
  providedIn: 'root'
})
export class UserRepository {
  private apiUrl = 'https://api.example.com/users';
  
  constructor(private http: HttpClient) {}
  
  findAll(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  findById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  create(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  update(user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${user.id}`, user);
  }
  
  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

// user.service.ts (higher-level service)
@Injectable({
  providedIn: 'root'
})
export class UserService {
  constructor(private userRepository: UserRepository) {}
  
  getAllUsers(): Observable<User[]> {
    return this.userRepository.findAll().pipe(
      map(users => users.sort((a, b) => a.name.localeCompare(b.name)))
    );
  }
  
  getActiveUsers(): Observable<User[]> {
    return this.userRepository.findAll().pipe(
      map(users => users.filter(user => user.isActive))
    );
  }
}

Facade Pattern

This pattern simplifies complex subsystems by providing a simpler interface:

// auth-facade.service.ts
@Injectable({
  providedIn: 'root'
})
export class AuthFacade {
  // Expose simple observables for components
  isLoggedIn$: Observable<boolean>;
  currentUser$: Observable<User | null>;
  authError$: Observable<string | null>;
  
  constructor(
    private authService: AuthService,
    private router: Router,
    private store: Store<AppState>
  ) {
    this.isLoggedIn$ = this.authService.isAuthenticated$;
    this.currentUser$ = this.authService.currentUser$;
    this.authError$ = this.authService.authError$;
  }
  
  // Simplified methods for components
  login(email: string, password: string): void {
    this.authService.login(email, password).subscribe({
      next: () => this.router.navigate(['/dashboard']),
      error: (err) => console.error('Login failed', err)
    });
  }
  
  logout(): void {
    this.authService.logout();
    this.router.navigate(['/login']);
  }
  
  register(userData: UserRegistration): void {
    this.authService.register(userData).subscribe({
      next: () => this.router.navigate(['/login']),
      error: (err) => console.error('Registration failed', err)
    });
  }
}

Strategy Pattern

This pattern allows you to select an algorithm at runtime:

// payment-strategy.interface.ts
export interface PaymentStrategy {
  processPayment(amount: number): Observable<PaymentResult>;
  validatePaymentData(data: any): boolean;
}

// credit-card-payment.service.ts
@Injectable()
export class CreditCardPaymentService implements PaymentStrategy {
  constructor(private http: HttpClient) {}
  
  processPayment(amount: number): Observable<PaymentResult> {
    // Credit card payment logic
    return this.http.post<PaymentResult>('/api/payments/credit-card', { amount });
  }
  
  validatePaymentData(data: any): boolean {
    // Validate credit card data
    return !!data.cardNumber && !!data.expiryDate && !!data.cvv;
  }
}

// paypal-payment.service.ts
@Injectable()
export class PayPalPaymentService implements PaymentStrategy {
  constructor(private http: HttpClient) {}
  
  processPayment(amount: number): Observable<PaymentResult> {
    // PayPal payment logic
    return this.http.post<PaymentResult>('/api/payments/paypal', { amount });
  }
  
  validatePaymentData(data: any): boolean {
    // Validate PayPal data
    return !!data.email;
  }
}

// payment.service.ts
@Injectable({
  providedIn: 'root'
})
export class PaymentService {
  private strategy: PaymentStrategy;
  
  constructor(
    private creditCardPayment: CreditCardPaymentService,
    private paypalPayment: PayPalPaymentService
  ) {
    this.strategy = this.creditCardPayment; // Default strategy
  }
  
  setPaymentMethod(method: 'credit-card' | 'paypal'): void {
    this.strategy = method === 'credit-card' 
      ? this.creditCardPayment 
      : this.paypalPayment;
  }
  
  processPayment(amount: number, paymentData: any): Observable<PaymentResult> {
    if (!this.strategy.validatePaymentData(paymentData)) {
      return throwError(() => new Error('Invalid payment data'));
    }
    
    return this.strategy.processPayment(amount);
  }
}

Practice Activity

Building a Shopping Cart Service

In this activity, you'll create a complete shopping cart service:

  1. Set up a new Angular project if you haven't already:
    ng new shopping-cart-app
    cd shopping-cart-app
  2. Create product models:
    ng g interface models/product
    ng g interface models/cart-item

    Define the interfaces:

    // models/product.ts
    export interface Product {
      id: number;
      name: string;
      price: number;
      imageUrl: string;
      description: string;
      inStock: boolean;
    }
    
    // models/cart-item.ts
    import { Product } from './product';
    
    export interface CartItem {
      product: Product;
      quantity: number;
    }
  3. Create a product service that fetches data:
    ng g service services/product

    Implement the service with mock data:

    // services/product.service.ts
    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';
    import { Product } from '../models/product';
    
    @Injectable({
      providedIn: 'root'
    })
    export class ProductService {
      private products: Product[] = [
        {
          id: 1,
          name: 'Laptop',
          price: 999.99,
          imageUrl: 'https://via.placeholder.com/200x150',
          description: 'Powerful laptop for work and play',
          inStock: true
        },
        {
          id: 2,
          name: 'Smartphone',
          price: 699.99,
          imageUrl: 'https://via.placeholder.com/200x150',
          description: 'Latest smartphone with amazing camera',
          inStock: true
        },
        {
          id: 3,
          name: 'Headphones',
          price: 149.99,
          imageUrl: 'https://via.placeholder.com/200x150',
          description: 'Noise cancelling wireless headphones',
          inStock: true
        },
        {
          id: 4,
          name: 'Tablet',
          price: 349.99,
          imageUrl: 'https://via.placeholder.com/200x150',
          description: 'Lightweight tablet for reading and browsing',
          inStock: false
        }
      ];
      
      getProducts(): Observable<Product[]> {
        return of(this.products);
      }
      
      getProduct(id: number): Observable<Product | undefined> {
        return of(this.products.find(p => p.id === id));
      }
    }
  4. Create a cart service:
    ng g service services/cart

    Implement the service:

    // services/cart.service.ts
    import { Injectable } from '@angular/core';
    import { BehaviorSubject, Observable } from 'rxjs';
    import { map } from 'rxjs/operators';
    import { Product } from '../models/product';
    import { CartItem } from '../models/cart-item';
    
    @Injectable({
      providedIn: 'root'
    })
    export class CartService {
      // TODO: Implement the cart service as shown in the lecture
      // Include methods for:
      // - Adding items to cart
      // - Updating quantities
      // - Removing items
      // - Calculating totals
      // - Clearing the cart
    }
  5. Create components that use these services:
    ng g c components/product-list
    ng g c components/product-item
    ng g c components/cart
    ng g c components/cart-item
  6. Implement the components to display products and manage the cart

Challenge

  1. Add a persistent cart that saves to localStorage
  2. Implement a checkout process with form validation
  3. Add HTTP services for fetching products from a real API
  4. Create an order history service that tracks past orders

Key Takeaways

Additional Resources