angularsignalsng-templateangular-signalsangular-content-projection

Multiple similar named conditional ng-content in child component not projecting


I first had a parent/child components that looked like this

Parent

<child-comp>
  <div firstContent>This is my first content</div>
  <div secondContent>This is my second content</div>
</child-comp>

Child

<div redColourDirective> 
  <div>This is the template with directive applied</div>
  <ng-content select="[firstContent]"></ng-content>
  <ng-content select="[secondContent]"></ng-content>
</div>

... then I wanted the directive, redColourDirective, to be conditional so the template became this (primarily because I can't have a conditional directive)

<ng-container *ngTemplateOutlet="withDirective() ? tmp1 : tmp2"> </ng-container>
<ng-template #tmp1>
  <div redColourDirective>
    <div>This is the template with directive applied</div>
    <ng-content select="[firstContent]"></ng-content>
    <ng-content select="[secondContent]"></ng-content>
  </div>
</ng-template>
<ng-template #tmp2>
  <div>
    <div>This is the template WITHOUT directive applied</div>
    <ng-content select="[firstContent]"></ng-content>
    <ng-content select="[secondContent]"></ng-content>
  </div>
</ng-template>

... where withDirective() is a signal as input. Now firstContent, secondContent project into the child component when withDirective is set to true from the parent but not when it is false. What am I doing wrong here?

//works, ng-content visible
<child-comp [withDirective]="true">
  <div firstContent>This is my first content</div>
  <div secondContent>This is my second content</div>
</child-comp>

//fails, no ng-content visible
<child-comp [withDirective]="false">
  <div firstContent>This is my first content</div>
  <div secondContent>This is my second content</div>
</child-comp>

What I've tried: Stackblitz demo


Solution

  • It is not recommended to use ng-content inside of conditionals or loops - Docs Reference

    The fact that ng-content only grabs the two slots when it is dynamically rendered, I suppose, is the code's issue.

    It won't render even after you try adding the directive to both conditionals, so the directive is not the root cause of the issue.


    Using *ngTemplateOutlet, which can render templates, in place of the ng-content is a preferable approach.

    child.component.html

    <ng-container *ngTemplateOutlet="withDirective() ? tmp1 : tmp2"> </ng-container>
    <ng-template #tmp1>
      <div redColourDirective>
        <div>This is the template with directive applied</div>
        <ng-container *ngTemplateOutlet="firstContent()"></ng-container>
        <ng-container *ngTemplateOutlet="secondContent()"></ng-container>
      </div>
    </ng-template>
    <ng-template #tmp2>
      <div>
        <div>This is the template WITHOUT directive applied</div>
        <ng-container *ngTemplateOutlet="firstContent()"></ng-container>
        <ng-container *ngTemplateOutlet="secondContent()"></ng-container>
      </div>
    </ng-template>
    

    Next, we pass in ng-templates, which will include the content you wish to render dynamically, in place of divs.

    app.component.html

    <child-comp [withDirective]="false">
      <ng-template #firstContent>This is my first content</ng-template>
      <ng-template #secondContent>This is my second content</ng-template>
    </child-comp>
    <hr />
    <child-comp [withDirective]="true">
      <ng-template #firstContent>This is my first content</ng-template>
      <ng-template #secondContent>This is my second content</ng-template>
    </child-comp>
    

    Lastly, we access this template and make it available to the child HTML by using the signal contentChild.

    child.component.ts

    import { Component, input, output, signal, contentChild } from '@angular/core';
    import { Action } from './models';
    import { NgTemplateOutlet } from '@angular/common';
    import { RedColourDirective } from './colour-directive';
    
    @Component({
      selector: 'child-comp',
      templateUrl: './child.component.html',
      standalone: true,
      imports: [NgTemplateOutlet, RedColourDirective],
    })
    export class ChildComponent {
      firstContent: any = contentChild('firstContent');
      secondContent: any = contentChild('firstContent');
      withDirective = input<boolean>();
    
      constructor() {}
    }
    

    Stackblitz Demo