Angular 2 dynamic dependency injection based on @Input()

20,056

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) } );
Share:
20,056

Related videos on Youtube

John
Author by

John

Web programmer for Moody Bible Institute. Experience with ASP.NET MVC, JavaScript, jQuery, KnockoutJs, HTML5, Twitter Bootstrap, CSS3, SignalR.

Updated on July 09, 2022

Comments

  • John
    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
    E. Hein over 7 years
    I'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 } ] } then constructor(private myService : MyServiceInterface) {}
  • John
    John over 7 years
    In my case, I would use the directive in more than one place with different services. Thanks though.
  • John
    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
    John over 7 years
    I 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
    Vineet 'DEVIN' Dev over 7 years
    Have 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
    Marc J. Schmidt over 6 years
    Since 4.0 get is deprecated.
  • Marc J. Schmidt
    Marc J. Schmidt over 6 years
  • Estus Flask
    Estus Flask over 6 years
    It'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
    mila over 5 years
    Thank you, your answer helped me so much!
  • userx
    userx about 3 years
    I tried this but getting "No provider found for ComponentA" whereas I did mention ComponentA in the list of providers. Any thoughts?
  • Camille
    Camille almost 3 years
    Issue, for me, is dependencies on all implementations. My answer at the bottom don't have this constraint, but bit more complex to implement.
  • Indhu
    Indhu about 2 years
    Thanks a ton! i was searching for this behavioral changes long time.