Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Inject Specific Service Instances into Multiple Instances of a Component in One Parent Component

I'm working on a very large project right now, and we're trying to make our code as modular as possible. We have multiple Angular applications, and we've created a separate library of common UI components and an API kit with common services used across these applications.

We're running into problems though when trying to build common components that need to work with services. For example, I'm working on an autocomplete component right now. That component should be able to fetch data from a remote source and filter the results based on what's been typed into the component's input field.

That implementation is easy enough for one instance. I inject the autocomplete service in my autocomplete component's constructor, then I provide it on the parent. That gives me the flexibility to change to the implementation details of the service when I used it while still being able to create a reusable component that works with a defined interface.

For example:

The service where we want to define the interface used by our Autocomplete Component:

@Injectable()
export class AutocompleteService {
  public url: string = 'my-api-default';

  constructor(http: Http) {}

  fetch(): any {
    return this.http.get(this.url);
  }
}

The autocomplete component implementation:

@Component({
  selector: 'my-autocomplete',
  templateUrl: 'my-autocomplete.component.html'
})
export class MyAutocompleteComponent {
  constructor(private autocompleteService: AutocompleteService) {}

  getData() {
    return this.autocompleteService.fetch();
  }
  ...autocomplete logic...
}

Now I can define a bear service that implements the Autocomplete service. I can hook up the bear service to my autocomplete component, so I can choose bear species in my form.

@Injectable()
export class BearService {
  public url: string = 'bear-url';

  constructor(http: Http){}

  fetch() {
    return this.http.get(this.url);
  }
}

Next, I define the parent that consumes my autocomplete component and provide the bear service to get my bear species data.

@Component({
  selector: 'my-form-component',
  template: `
    <form>
      <my-autocomplete [(ngModel)]="bear"></my-autocomplete>
      <button type="submit">Submit</button>
    </form>
  `,
  providers: [
    {provide: AutocompleteService, useClass: BearService}
  ]
})
export class MyFormComponent {
  ...component logic...
}

So far, so good.

My question arises when I need to build a large form that uses multiple autocomplete components. My boss tells me I need three autocomplete fields on this form, one for bear species, one for beet species, and one for a Battlestar Gallactica character. My first thought is to do this:

I define the service instances:

@Injectable()
export class BeetsService {
  public url: string = 'beets-url';

  constructor(http: Http){}

  fetch() {
    return this.http.get(this.url);
  }
}

@Injectable()
export class BattleStarGallacticaService {
  public url: string = 'battlestar-gallactica';
  constructor(http: Http){}

  fetch() {
    return this.http.get(this.url);
  }
}

Then I update the parent template and providers:

@Component({
  selector: 'my-form-component',
  template: `
    <form>
      <my-autocomplete [(ngModel)]="bear"></my-autocomplete>
      <my-autocomplete [(ngModel)]="beet"></my-autocomplete>
      <my-autocomplete [(ngModel)]="battleStarGallactica"></my-autocomplete>
      <button type="submit">Submit</button>
    </form>
  `,
  providers: [
    {provide: AutocompleteService, useClass: BearService},
    {provide: AutocompleteService, useClass: BeetService},
    {provide: AutocompleteService, useClass: BattlestarGalacticaService},
  ]
})
export class MyFormComponent {
  ...component logic...
}

Now how can I tell which autocomplete component to use which service?

I know what I have now will always use the last provider given for AutocompleteService, because that's how the Angular DI framework works.

I also know I can't use multi-providers on this since Angular only defines them for NG_VALIDATORS and NG_ASYNC_VALIDATORS.

So, does anyone have any idea how I can solve my problem? I don't care how the problem gets solve per se, but I still need to be able to:

  1. Define a service interface
  2. User that service interface in the reusable component
  3. Create a new service instance for my own needs that implements the original interface
  4. Be able to use multiple components that implement the same service interface using different service implementations within a single parent component
like image 702
Colin Alford Avatar asked Oct 18 '22 16:10

Colin Alford


1 Answers

I would move the providers to a directive that are applied selectively

@Directive({
  selector: 'my-autocomplete[bear]',
  providers: [{ provide: AutoCompleteService, useClass: BearService}]
})
export class BearDirective {}

@Directive({
  selector: 'my-autocomplete[battlestarGalactica]',
  providers: [{ provide: AutoCompleteService, useClass: BattlestarGalacticaService}]
})
export class BattelstarGalacticaDirective{}

and then use it like

<form>
  <my-autocomplete bear [(ngModel)]="bear"></my-autocomplete>
  <my-autocomplete bear [(ngModel)]="beet"></my-autocomplete>
  <my-autocomplete battlestarGalactica [(ngModel)]="battleStarGallactica"></my-autocomplete>
  <button type="submit">Submit</button>
</form>

and remove the providers from the MyFormComponent

like image 112
Günter Zöchbauer Avatar answered Oct 20 '22 20:10

Günter Zöchbauer