rcbiczok
rcbiczok

Reputation: 73

Accessing content from <ng-content> passed through ngProjectAs via @ContentChild

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

Upvotes: 3

Views: 949

Answers (1)

Mehyar Sawas
Mehyar Sawas

Reputation: 1266

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.

Upvotes: 2

Related Questions