DEV Community

Nhan Nguyen
Nhan Nguyen

Posted on

Angular Dynamic Service Instantiation Using Injector

Dynamic service instantiation can be a powerful technique for creating flexible and scalable solutions in modern Angular applications. One use case is selecting a specific implementation of a service at runtime based on user input or application state. This post will explore how to achieve this using Angular's Injector and provide a practical example of dynamically initializing payment services.

The Problem

Imagine an application that supports multiple payment methods, such as PayPal, Stripe, and Venmo. Each payment method has its own implementation but adheres to a common interface. Based on the user's selection, the appropriate service needs to be instantiated dynamically at runtime. Angular's Injector provides a seamless way to accomplish this.

Example Setup

Below is a sample implementation of dynamic service instantiation:

Service Definitions

First, define a base class PaymentBaseService and create specific services for each payment method.

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

@Injectable({ providedIn: 'root' })
export class PaymentBaseService {
  pay() {}
}

@Injectable({ providedIn: 'root' })
export class PaypalService extends PaymentBaseService {
  constructor() {
    super();
    console.log('PaypalService!');
  }

  override pay() {
    console.log('Paypal payment!');
  }
}

@Injectable({ providedIn: 'root' })
export class StripeService extends PaymentBaseService {
  constructor() {
    super();
    console.log('StripeService!');
  }

  override pay() {
    console.log('Stripe payment!');
  }
}

@Injectable({ providedIn: 'root' })
export class VenmoService extends PaymentBaseService {
  constructor() {
    super();
    console.log('VenmoService!');
  }

  override pay() {
    console.log('Venmo payment!');
  }
}
Enter fullscreen mode Exit fullscreen mode

Component Implementation

Create a component where users can select a payment method and dynamically initialize the corresponding service:

import { Component, inject, Injector } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';

@Component({
  selector: 'app-root',
  imports: [FormsModule],
  template: `
    <label for="payment">Choose a payment method:</label>
    <select name="payments" id='payments' [(ngModel)]="paymentMethod">
      <option value="">None</option>
      <option value="paypal">Paypal</option>
      <option value="stripe">Stripe</option>
      <option value="venmo">Venmo</option>
    </select>
    <br><br>
    <button (click)="updatePaymentService()">Submit</button>
  `,
})
export class App {
  readonly #injector = inject(Injector);

  paymentMethod: '' | 'paypal' | 'stripe' | 'venmo' = '';
  paymentService!: PaymentBaseService;

  updatePaymentService() {
    switch (this.paymentMethod) {
      case 'paypal':
        this.paymentService = this.#injector.get(PaypalService);
        break;
      case 'stripe':
        this.paymentService = this.#injector.get(StripeService);
        break;
      case 'venmo':
        this.paymentService = this.#injector.get(VenmoService);
        break;
      default:
        throw new Error(`Unknown payment type: ${this.paymentMethod}`);
    }

    this.paymentService.pay();
  }
}

bootstrapApplication(App);
Enter fullscreen mode Exit fullscreen mode

Key Points

  • Injector Usage: The Injector fetches the appropriate service dynamically. This avoids directly injecting all possible services into the component, keeping the design clean and efficient.

  • Dynamic Initialization: The updatePaymentService method determines the selected payment type and initializes the corresponding service at runtime.

  • Error Handling: The implementation includes a fallback for unknown payment types, ensuring robustness.

Benefits of Using Injector

  • Scalability: Adding a new payment method requires only defining a new service and updating the switch case in the component.

  • Flexibility: Services are instantiated only when needed, reducing memory usage.

  • Clean Design: This approach adheres to the dependency inversion principle, ensuring a loosely coupled architecture.

Conclusion

Dynamic service instantiation using Angular's Injector is a powerful feature for building flexible and scalable applications. Following the example outlined above, you can easily implement runtime-based service selection. This approach is especially useful in scenarios where the application's behavior depends on user input or dynamic configurations.

Happy coding!


I hope you found it helpful. Thanks for reading. 🙏
Let's get connected! You can find me on:

Top comments (0)