Home > Enterprise >  How to access a service inside TemplateRef which was provided in parent component?
How to access a service inside TemplateRef which was provided in parent component?

Time:08-17

Recently I had a task where I needed to pass a TemplateRef to component, which provides some service, say SomeService. And inside that template I used a directive that injects service SomeService. I ended up with NullInjectorError.

So my question is how can I get service provided in component but not inside ViewContent.

Here is minimal representation of what I'm trying to do. Let's assume that all components and directives are declared in the same module.

root-component. All that's happening here is that I'm passing a template reference to the app-component and use the directive inside template.

@Component({
    selector: 'root-component',
    template: `
    <app-component [itemTemplate]="template"></app-component>

    <ng-template #template >
        <div testDirective>hello</div>
    </ng-template>
    `
})
export class RootComponent {
}

app-component. Here I provide SomeService

@Component({
    selector: 'app-component',
    template: `
        <ng-container [ngTemplateOutlet]="itemTemplate"></ng-container>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
    providers: [
        SomeService // <-- HERE IS THE SERVICE THAT I'M TRYING TO INJECT INTO DIRECTIVE
    ],
})
export class AppComponent {
    @Input()
    itemTemplate: TemplateRef<unknown> | null = null;

    constructor(
    ) {

    }
}

testDirective

@Directive({
    selector: '[testDirective]'
})
export class TestDirective {
    constructor(
        private _someService: SomeService // <-- THIS CAUSE AN NullInjectorError
    ) {
    }
}

There is no need to show the implementation of SomeService as it is just a regular service.

I apologize if something is not clear, this is my first question on StackOverflow

Stackblitz here

CodePudding user response:

Angular v14 solution

With Angular 14, you can pass injector to ngTemplateOutlet directive. And by that way, the embeddedView that is going to render inside ng-container will refer to the current component injector tree (and point to injected SomeService provider on the component level.

@Component({
  selector: 'app-component',
  template: `
      <ng-container 
        [ngTemplateOutlet]="itemTemplate"
        [ngTemplateOutletInjector]="injector">
      </ng-container>
  `,
  ...
})
export class AppComponent {
  @Input()
  itemTemplate: TemplateRef<unknown> | null = null;

  constructor(
    public readonly injector: Injector
  ) {}
}


Angular v13 or less

You have to make some changes in the way the template has been passed. Rather than passing TemplateRef as Input binding. Pass that ng-template as a Content Projection.

@Component({
  selector: 'root-component',
  template: `
  <app-component>
    <!--Pass template as content projection-->
    <ng-template #template>
      <div testDirective>hello</div>
    </ng-template>
  </app-component>
  `,
})
export class RootComponent {}

And then AppComponent can use @ContentChild to lookup for that transcluded view (ng-template) thing checking #template template variable. And that's it. itemTemplate will fill in as soon as ContentChild query retrieves the result. Boom

  • Related