angularangular13transclusion

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


Solution

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