Search code examples
angulardesign-patternsdependency-injectionarchitecture

Angular Service Injection in reusable components


I have a reusable component, ReusableComponent, that needs to use a service that contains an specific function. This service must implement an interface, ServiceAbstraction, to make sure that it contains that function signature. I inject that abstraction in ReusableComponent using a token, so the consumer component has to define it:

export const SERVICE_ABSTRACTION_TOKEN = new InjectionToken<ServiceAbstraction>(
  'ServiceAbstraction Token'
);

export interface ServiceAbstraction {
  doSomething(): void;
}

@Injectable()
export class ServiceImplementation implements ServiceAbstraction {
  doSomething() {
    console.log('I do something');
  }
}

@Component({
  selector: 'app-reusable-component',
  template: ``,
  standalone: true,
})
export class ReusableComponent {
  constructor(
    @Inject(SERVICE_ABSTRACTION_TOKEN)
    private serviceAbstraction: ServiceAbstraction
  ) {
    this.serviceAbstraction.doSomething();
  }
}

Then in any consumer component I can include this reusable component in its template and make use of a concrete service that implements that interface using the providers array of the parent component decorator.

@Component({
  selector: 'app-consumer',
  standalone: true,
  template: `
     <app-reusable-component />
  `,
  imports: [ReusableComponent],
  providers: [
    { provide: SERVICE_ABSTRACTION_TOKEN, useClass: ServiceImplementation },
  ],
})
export class ConsumerComponent { }

The problem that I have is that I'd like to be able to have in the parent component different instances of the reusable component that uses different services concretions. Is that possible using angular DI? I know that I could use an input to pass the service to the reusable component, but that would be an antipattern, and I want to use angular architecture and design for DI.

Here you have a stackblitz to tinker with.


Solution

  • After lots of research I found a way that meets all my requirements and is fairly simple: As of Angular 14, the createEmbeddedView ViewContainerRef class method allows to add an injector when creating a view that will replace the default injector.

    We can create a structural directive that takes the host component View container reference and template reference, and creates the view replacing the injector with whatever provided:

    @Directive({
      selector: '[injectService2]',
      standalone: true,
    })
    export class InjectService2Directive {
      constructor(private vcr: ViewContainerRef, private tpl: TemplateRef<any>) {}
    
      ngOnInit() {
        this.vcr.createEmbeddedView(
          this.tpl,
          {},
          {
            injector: Injector.create({
              providers: [
                {
                  provide: SERVICE_ABSTRACTION_TOKEN,
                  useClass: ServiceImplementation2,
                },
              ],
            }),
          }
        );
      }
    }
    

    And it would be used this way:

    @Component({
      selector: 'app-root',
      standalone: true,
      template: `
        <app-reusable-component /> <!-- Default injector will be used (ServiceImplementation) -->
        <app-reusable-component *injectService2 /> <!-- Specified injector will be used (ServiceImplementation2) -->
      `,
      imports: [ReusableComponent, InjectService2Directive],
      providers: [
        { provide: SERVICE_ABSTRACTION_TOKEN, useClass: ServiceImplementation },
      ],
    })
    export class App {}
    
    

    I find this solution very elegant since the reusable component is not modified and doesn't take the responsibility of bypassing the Angular DI system. The only thing that I find not so clean is having to create a directive for each service you need to inject, but I guess that it keeps things flexible and allows not to interact with the Angular DI in an explicit way.

    Stackblitz with this solution