So I have a setup where I have a layout component that uses multi-slot approach making certain sections of my page customizable:
import {
Component,
OnInit,
Input,
ContentChild,
AfterViewInit,
} from '@angular/core';
import { FooterComponent } from '../footer/footer.component';
import { HeaderComponent } from '../header/header.component';
@Component({
selector: 'app-layout',
template: `<div>
<div *ngIf="header" style="border-style: dotted;">
<h2>Header:{{ header.title }}</h2>
<ng-content select="app-header"></ng-content>
</div>
<div>
<h2>Content</h2>
<ng-content></ng-content>
</div>
<p>End of Content</p>
<div *ngIf="footer" style="border-style: dashed;">
<h2>Footer</h2>
<ng-content select="app-footer"></ng-content>
</div>
</div>`,
})
export class LayoutComponent implements AfterViewInit {
@ContentChild(HeaderComponent)
public header?: HeaderComponent;
@ContentChild(FooterComponent)
public footer?: FooterComponent;
public ngAfterViewInit(): void {
console.log('Injected', this.header, this.footer);
}
}
Notice that I use @ContentChild
to inject and display specific elements on demand.
Now I want to create a more specialized component that pre-populates the footer:
import { Component } from '@angular/core';
@Component({
selector: 'app-nested-layout',
template: `<app-layout>
<ng-content select="app-header" ngProjectAs="app-header"></ng-content>
<ng-content></ng-content>
<app-footer>
<p>Predefined Footer</p>
</app-footer>
</app-layout>
`,
})
export class NestedLayoutComponent {}
Then I would use NestedLayoutComponent
like this:
<app-nested-layout>
<app-header title="Header">
<p>Header stuff</p>
</app-header>
Regular content
</app-nested-layout>
I use ngProjectAs
here so that app-layout
actually recognizes the content. The problem is that even though the ngProjectAs
correctly places the content into the right place in the DOM. the @ContentChild
annotation for injecting the header component will not work in LayoutComponent
and yields an undefined
.
Is this maybe related to this issue?
I have also put together the whole example into Stackblitz
Possible Solution
You could pass the headerComponent
as an @Input from the NestedLayoutComponent
to the LayoutComponent
and there you check whether the header
is defined, which is a direct child of the LayoutComponent
or the parentHeader
is defined which is the direct child of the NestedLayoutComponent
.
Here is my stackblitz example
Your NestedLayoutComponent Template:
<app-layout [parentHeader]="header">
<ng-content select="app-header" ngProjectAs="app-header"></ng-content>
<ng-content></ng-content>
<app-footer>
<p>Predefined Footer</p>
</app-footer>
</app-layout>
Your LayoutComponent Class:
...
@ContentChild(HeaderComponent)
public header?: HeaderComponent;
@Input() parentHeader: HeaderComponent;
...
Your LayoutComponent Template:
<div *ngIf="header || parentHeader" style="border-style: dotted; margin-top: 10px">
<h2>Header:{{ header.title }}</h2>
<ng-content select="app-header"></ng-content>
</div>
...
Feel free pass the HeaderComponent
further to the NestedLayoutComponent
by your favorite way.
Explained
The @ContentChild header is undefined
in the LayoutComponent
, because the HeaderComponent
in this case is not a content child of the LayoutComponent
, but a content child of the NestedLayoutComponent
. Even though angular projects the content in a child component by selector, still when querying with @ContentChild the content child element must be a real child in the component template.
In your example under AppComponent
the app-header
is a child of app-nested-layout
and app-layout
in app-nested-layout
is does not have the app-header
as a child content.