Search code examples
angularangular-material

Angular content projection in standalone component


It is not rendering the @ContentChild in another standalone component.

Added stackblitz

HTML

<div class="card">
  <h6 class="card-header">
    <ng-container [ngTemplateOutlet]="cardHeader.tpl"></ng-container>
    @if (hasActions()) {
      <!-- <ng-content select="app-card-header-actions"> </ng-content> -->
    }
  </h6>
  <div class="card-body">
    @defer(when cardMainContent.tpl) {
    <ng-container [ngTemplateOutlet]="cardMainContent.tpl"></ng-container>
    } @loading(minimum 750ms) {
      <div class="spinner-border" role="status">
      </div>
    } @placeholder() {
    <div class="my-5">
      <div class="alert alert-primary text-center" role="alert">
        No Data Found!
      </div>
    </div>
    }
  </div>
</div>

TS

@Directive({
  selector: '[appCardHeader]',
  standalone: true,
})
export class CardHeaderDirective {
  constructor(public tpl: TemplateRef<any>) {}
}

@Directive({
  selector: 'app-card-header-actions',
  standalone: true,
})
export class CardHeaderActionsDirective {}

@Directive({
  selector: '[appCardMainContent]',
  standalone: true,
})
export class CardContentDirective {
  constructor(public tpl: TemplateRef<any>) {}
}

@Component({
  selector: 'app-card',
  standalone: true,
  imports: [CommonModule, CardContentDirective, CardHeaderDirective],
  templateUrl: './card.component.html',
  styleUrl: '',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CardComponent {
  hasActions = input<boolean>(false);
  @ContentChild(CardContentDirective) cardMainContent!: CardContentDirective;
  @ContentChild(CardHeaderDirective) cardHeader!: CardHeaderDirective;
}

ERROR TypeError: Cannot read properties of undefined (reading 'tpl') at CardComponent_Template (card.component.html:3:19)

Content projection should work and it should display the content from different cards.


Solution

  • We cannot use a directive on ng-template since it does not fire, ng-template is a virtual element and is not rendered in the DOM, so the better option, is to just create template reference variables like #cardHeader and #cardMainContent and access these through ContentChild and directly render them on the HTML.

      @ContentChild('cardMainContent') cardMainContent!: TemplateRef<any>;
      @ContentChild('cardHeader') cardHeader!: TemplateRef<any>;
    

    card.component.ts

    import { CommonModule } from '@angular/common';
    import {
      ChangeDetectionStrategy,
      Component,
      ContentChild,
      Directive,
      TemplateRef,
      inject,
      input,
    } from '@angular/core';
    
    @Directive({
      selector: 'app-card-header-actions',
      standalone: true,
    })
    export class CardHeaderActionsDirective {}
    
    @Component({
      selector: 'app-card',
      standalone: true,
      imports: [CommonModule],
      templateUrl: './card.component.html',
      changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class CardComponent {
      hasActions = input<boolean>(false);
      @ContentChild('cardMainContent') cardMainContent!: TemplateRef<any>;
      @ContentChild('cardHeader') cardHeader!: TemplateRef<any>;
      barChartOptions = {};
    }
    

    card.component.html

    <div class="card">
      <h6 class="card-header">
        <ng-container [ngTemplateOutlet]="cardHeader"></ng-container>
        @if (hasActions()) {
        <ng-content select="app-card-header-actions"> </ng-content>
        }
      </h6>
      <div class="card-body">
        @defer(when cardMainContent) {
        <ng-container [ngTemplateOutlet]="cardMainContent"></ng-container>
        } @loading(minimum 750ms) {
        <div class="spinner-border" role="status"></div>
        } @placeholder() {
        <div class="my-5">
          <div class="alert alert-primary text-center" role="alert">
            No Data Found!
          </div>
        </div>
        }
      </div>
    </div>
    

    app.component.html

    <mat-grid-list
      cols="4"
      gutterSize="15px"
      rowHeight="375px"
      id="dashboard-tiles"
    >
      <mat-grid-tile [colspan]="1" [rowspan]="1" class="card shadow">
        <app-card>
          <ng-template #cardHeader>LOREM</ng-template>
          <ng-template #cardMainContent>
            <ul class="list-group">
              <li class="list-group-item btn-link">Item 1</li>
              <li class="list-group-item btn-link">Item 2</li>
              <li class="list-group-item btn-link">Item 3</li>
            </ul>
          </ng-template>
        </app-card>
      </mat-grid-tile>
    
      <mat-grid-tile [colspan]="2" [rowspan]="1" class="card shadow">
        <app-card [hasActions]="true">
          <ng-template #cardHeader>IPSUM</ng-template>
    
          <ng-template #cardMainContent>
            <ng-template
              [ngTemplateOutlet]="cardTitle"
              [ngTemplateOutletContext]="{ data: resData }"
            ></ng-template>
            <div class="card-body mt-0 p-0" [style.height.px]="400">
              <canvas
                baseChart
                [datasets]="[]"
                [labels]="[]"
                [options]="barChartOptions"
                [plugins]="[]"
                [legend]="true"
                [type]="'bar'"
              >
              </canvas>
            </div>
          </ng-template>
        </app-card>
      </mat-grid-tile>
    </mat-grid-list>
    
    <ng-template #cardTitle let-data="data">
      <div class="card-title">
        @if (data.length) {
        <!-- display-->
        } @else {
        <!-- some other display-->
        }
      </div>
    </ng-template>
    

    Stackblitz Demo