Angular 2 dynamic dependency injection based on @Input()
Solution 1
It is
// can be a service also for overriding and testing
export const trendyServiceMap = {
serviceA: ServiceA,
serviceB: ServiceB
}
constructor(private injector: Injector) {}
...
ngOnInit() {
if (trendyServiceMap.hasOwnProperty(this.use)) {
this.service = this.injector.get<any>(trendyServiceMap[this.use]);
} else {
throw new Error(`There's no such thing as '${this.use}'`);
}
}
Solution 2
In general, same approach is described in Angular2 documentation: InjectorComponent
@Component({
providers: [Car, Engine, Tires, heroServiceProvider, Logger]
})
export class InjectorComponent {
car: Car = this.injector.get(Car);
heroService: HeroService = this.injector.get(HeroService);
hero: Hero = this.heroService.getHeroes()[0];
constructor(private injector: Injector) { }
}
You must inject Injector
in constructor and list all services in providers
property of @Component
annotation. Then you can injector.get(type)
, where type
will be resolved from your @Input
. As per documentation, Service
is not actually injected until you ask for it (.get()
).
Solution 3
Here you have a way, a bit complex but work like a charm!
Setup a default search service in a shared module and multiple custom implementation. And that without having to explicitly reference all possible implementation.
Interface and default implementation
export interface ISearch {
searchByTerm(textInput: string);
}
export class DefaultSearch implements ISearch {
searchByTerm(textInput: string) { console.log("default search by term"); }
}
Create list of service implementation with InjectionToken
// Keep list of token, provider will give a implementation for each of them
export const SearchServiceTokens: Map<string, InjectionToken<ISearch>> = new Map();
// Add File service implementation token
SearchServiceTokens.set('default', new InjectionToken<ISearch>('default'));
Provider for default service implementation
providers: [
...
// Default implementation service
{
provide: SearchServiceTokens.get('default'),
useClass: DefaultSearch
}
]
Custom implementation (could be on another module)
export class Component1Search implements ISearch {
searchByTerm(textInput: string) { console.log("component1 search by term"); }
}
Add token for custom implementation
SearchServiceTokens.set('component1', new InjectionToken<ISearch>('component1'));
Add provider
providers: [
...
// Other implementation service
{
provide: SearchServiceTokens.get('component1'),
useClass: Component1Search
}
]
Finally, in your component
@Input() useService;
searchService: ISearch;
constructor(private injector: Injector) {
// Use default if none provided
let serviceToUse = 'default';
if (null !== this.useService) { serviceToUse = this.useService; }
this.searchService = this.injector.get(SearchServiceTokens.get(serviceToUse));
}
Solution 4
There is a service named Inject in @angular/core module. With @Inject you can achieve alternative way of injection. But that can be done only in constructor.
So you will need to put component's inputs in the inputs array of your @component decorator (do not use @Input decorator inside class) and then inject that input variable in constructor.
Solution 5
I would like to take Estus Flask answer a step further and create a logic that imports the service rather than having to declare the names as array objects.
Basically, we just have to pass in the path
and name
of the service and the rest is almost the same.
public _dynamicService: any;
dynamicDI(service_path, service_name){
import(service_path).then(s => {
this._dynamicService = this.injector.get<any>(s['service_name']);
})
}
Now, you can access the functions inside the dynamicService
as follows:
(Let's assume we have a http observable fn in the service we need)
this._dynamicService['your_function_name']().subscribe(res=> { console.log(res) } );
Related videos on Youtube
John
Web programmer for Moody Bible Institute. Experience with ASP.NET MVC, JavaScript, jQuery, KnockoutJs, HTML5, Twitter Bootstrap, CSS3, SignalR.
Updated on July 09, 2022Comments
-
John almost 2 years
Suppose I have an Angular 2 component-directive, where I want the injected dependency that the component uses to be determined by an @Input().
I want to write something like
<trendy-directive use="'serviceA'">
and have that instance of TrendyDirective use serviceA, or have it use serviceB if that's what I specify. (this is an oversimplified version of what I'm actually trying to do)(If you think this is a terrible idea to begin with, I'm open to that feedback, but please explain why.)
Here's one example of how to achieve what I'm thinking of. In this example, imagine that ServiceA and ServiceB are injectables that both implement iService by having a 'superCoolFunction'.
@Component({ selector: 'trendy-directive', ... }) export class TrendyDirective implements OnInit { constructor( private serviceA: ServiceA, private serviceB: ServiceB){} private service: iService; @Input() use: string; ngOnInit() { switch (this.use){ case: 'serviceA': this.service = this.serviceA; break; case: 'serviceB': this.service = this.serviceB; break; default: throw "There's no such thing as a " + this.use + '!'; } this.service.superCoolFunction(); } }
I think this technically would work, but there's got to be a better way to do dynamic dependency injection.
-
E. Hein over 7 yearsI'd vote for injection too: do you use trendy-directive more than once with different services in it's parent component or switch it dynamically? If not, the parent component can simply provide the right service and you do not need to implement complex logic.
@Component({ selector: 'i-use-trendy-directive', providers: [ {provide: MyServiceInterfase, useClass: MyServiceImpl } ] }
thenconstructor(private myService : MyServiceInterface) {}
-
John over 7 yearsIn my case, I would use the directive in more than one place with different services. Thanks though.
-
John over 7 years@Vineet 'DEVIN' Dev That's very interesting. Documentation on the inputs array in the decorator seems sparse. It sounds like inputs in the
@Component
decorator are addressed earlier in the component lifecycle than those specified with the@Input
decorator. Do you know when? -
John over 7 yearsI just tested and unfortunately inputs from the
@Component
decorator are not populated yet in the constructor (but they are populated by the time we get to ngOnInit, just like ones declared using@Input
). -
Vineet 'DEVIN' Dev over 7 yearsHave you tried directly using the service rather than trying to inject it first? Like, just use @input to get the reference of service's variable from parent component and just use it without injection? I also tried the approach that I answered, but it seems that parent component initializes after child component.
-
Marc J. Schmidt over 6 yearsSince 4.0
get
is deprecated. -
Marc J. Schmidt over 6 yearsI mean the signature. See github.com/angular/angular/blob/5.2.5/packages/core/src/di/…
-
Estus Flask over 6 yearsIt's always better to call generic method with type. I'm not sure if this signature will be ever removed. It's there to let users know that it's better to use a generic. Any way, I updated the answer, thanks for the attention. Usually I don't bother with thorough typing in the solution and focus on providing the gist of it.
-
mila over 5 yearsThank you, your answer helped me so much!
-
userx about 3 yearsI tried this but getting "No provider found for ComponentA" whereas I did mention ComponentA in the list of providers. Any thoughts?
-
Camille almost 3 yearsIssue, for me, is dependencies on all implementations. My answer at the bottom don't have this constraint, but bit more complex to implement.
-
Indhu about 2 yearsThanks a ton! i was searching for this behavioral changes long time.