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.