Dynamic Dependency Injection in Angular

I keep getting impressed by how feature-rich dependency injection in Angular is. This time I needed it to inject the appropriate implementation of a dependency based on runtime information. Of course, the scenario is well supported.

Choosing the Class to Inject at Runtime

In essence, I had two different implementations of the same provider:

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

@Injectable()
export class Api {
  constructor(public http: Http) {
    console.log('Hello Api Provider');
  }
}

@Injectable()
export class MobileApi extends Api {
  constructor(public http: Http) {
    super(http);
    console.log('For Mobile');
  }
}

This was in an Ionic app and I wanted to choose the one or the other implementation based on whether the code is running in a web browser or in a mobile application. Factory provider is the tool to achieve that:

export function apiFactory(http: Http, platform: Platform) {
  if (!platform.url().startsWith('http')) {
    return new MobileApi(http);
  } else {
    return new Api(http);
  }
}

Instead of leaving the class instantiation up to the injector, I now do it in the factory method based on my URL check, which determines if the page is running in a mobile app or not. Because of that I am also responsible for providing all the dependencies to the instantiated class. In order to have them injected into the factory provider, I need to manually list them all when registering the provider:

@NgModule({
  // ...
  providers: [
    // ...
    {provide: Api, useFactory: apiFactory, deps: [Http, Platform]}
  ]
})

Injecting the Decision Logic into the Factory

This configuration was working already, but I had another requirement: the providers needed to be registered in another module, where Ionic's Platform wasn't available. Hence, I introduced a Configuration class to contain the decision making logic:

export class Configuration {
  isMobile: () => boolean;
}

export function apiFactory(http: Http, config: Configuration) {
  if (config.isMobile && config.isMobile()) {
    return new MobileApi(http);
  } else {
    return new Api(http);
  }
}

@NgModule({
  // ...
  providers: [
    // ...
    // notice the changed dependencies in provider declaration
    {provide: Api, useFactory: apiFactory, deps: [Http, Configuration]},
    Configuration
  ]
})

I can now extend the class in the main module to inject it instead of the base one:

export class AppConfiguration extends Configuration {
  constructor(platform: Platform) {
    super();
    this.isMobile = () => !platform.url().startsWith('http');
  }
}

@NgModule({
  // ...
  providers: [
    // ...
    {provide: Configuration, useClass: AppConfiguration}
  ]
})

If necessary I could even implement a different decision logic when using the two providers in a different application without any changes to their module.

Copyright
Creative Commons License