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
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