Docs
/
Angular
Chapter 13

13 — Content Projection

What is Content Projection?

Content projection lets a parent pass HTML content into a child component's template — similar to React's children or Vue's slots.


Single-Slot Projection (<ng-content>)

// card.component.ts
@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <ng-content />      <!-- Parent's content renders here -->
    </div>
  `,
})
export class CardComponent {}
<!-- Usage -->
<app-card>
  <h2>Hello World</h2>
  <p>This content is projected into the card.</p>
</app-card>

Multi-Slot Projection (select)

@Component({
  selector: 'app-modal',
  template: `
    <div class="modal">
      <header>
        <ng-content select="[modal-header]" />
      </header>
      <div class="body">
        <ng-content />                <!-- Default slot (no select) -->
      </div>
      <footer>
        <ng-content select="[modal-footer]" />
      </footer>
    </div>
  `,
})
export class ModalComponent {}
<app-modal>
  <div modal-header>
    <h2>Confirm Delete</h2>
  </div>

  <p>Are you sure you want to delete this item?</p>

  <div modal-footer>
    <button (click)="cancel()">Cancel</button>
    <button (click)="confirm()">Delete</button>
  </div>
</app-modal>

Select By

SelectorExample
Elementselect="h2"
Attributeselect="[modal-header]"
CSS classselect=".header"
Componentselect="app-header"

Conditional Content (@ContentChild)

@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      @if (hasHeader) {
        <header><ng-content select="[card-header]" /></header>
      }
      <div class="body">
        <ng-content />
      </div>
    </div>
  `,
})
export class CardComponent {
  hasHeader = contentChild<ElementRef>('[card-header]');
  // Or older: @ContentChild('cardHeader') header?: ElementRef;
}

<ng-template> — Deferred Template

<ng-template> defines a template that is not rendered by default — it must be explicitly instantiated.

<!-- This is NOT rendered automatically -->
<ng-template #greeting>
  <h1>Hello, {{ user.name }}!</h1>
</ng-template>

<!-- Render it conditionally with ngTemplateOutlet -->
<div *ngTemplateOutlet="greeting"></div>

<ng-container> — Grouping Without Extra DOM

<ng-container> is a logical container that doesn't add any element to the DOM.

<!-- ❌ Adds an extra <div> to the DOM -->
<div *ngIf="user">
  <h1>{{ user.name }}</h1>
</div>

<!-- ✅ No extra DOM element -->
<ng-container *ngIf="user">
  <h1>{{ user.name }}</h1>
</ng-container>

<!-- With new control flow, ng-container is less needed -->
@if (user) {
  <h1>{{ user.name }}</h1>
}

ngTemplateOutlet — Dynamic Templates

Render a template reference dynamically, optionally passing context data.

@Component({
  template: `
    <!-- Define templates -->
    <ng-template #loading>
      <app-spinner />
    </ng-template>

    <ng-template #error let-message="message">
      <div class="error">{{ message }}</div>
    </ng-template>

    <ng-template #content let-data="data">
      <h1>{{ data.title }}</h1>
      <p>{{ data.body }}</p>
    </ng-template>

    <!-- Render dynamically based on state -->
    <ng-container [ngTemplateOutlet]="currentTemplate"
                  [ngTemplateOutletContext]="currentContext">
    </ng-container>
  `,
})
export class DynamicComponent {
  @ViewChild('loading') loadingTpl!: TemplateRef<any>;
  @ViewChild('error') errorTpl!: TemplateRef<any>;
  @ViewChild('content') contentTpl!: TemplateRef<any>;

  currentTemplate!: TemplateRef<any>;
  currentContext: any = {};

  showLoading() {
    this.currentTemplate = this.loadingTpl;
  }

  showError(message: string) {
    this.currentTemplate = this.errorTpl;
    this.currentContext = { message };
  }

  showContent(data: any) {
    this.currentTemplate = this.contentTpl;
    this.currentContext = { data };
  }
}

Reusable List Component with Custom Template

@Component({
  selector: 'app-data-list',
  standalone: true,
  imports: [NgTemplateOutlet],
  template: `
    @if (items.length === 0) {
      <ng-content select="[empty]" />
    } @else {
      @for (item of items; track trackFn(item)) {
        <ng-container
          [ngTemplateOutlet]="itemTemplate"
          [ngTemplateOutletContext]="{ $implicit: item, index: $index }"
        />
      }
    }
  `,
})
export class DataListComponent<T> {
  @Input() items: T[] = [];
  @Input() trackFn: (item: T) => any = (item: any) => item;
  @ContentChild('itemTemplate') itemTemplate!: TemplateRef<any>;
}
<!-- Usage — parent defines how each item looks -->
<app-data-list [items]="users" [trackFn]="trackById">
  <ng-template #itemTemplate let-user let-i="index">
    <div class="user-row">
      {{ i + 1 }}. {{ user.name }} — {{ user.email }}
    </div>
  </ng-template>

  <p empty>No users found.</p>
</app-data-list>

@ContentChild / @ContentChildren

Access projected content from the child component.

@Component({
  selector: 'app-tabs',
  template: `
    <div class="tab-headers">
      @for (tab of tabs(); track tab.label) {
        <button [class.active]="tab === activeTab()" (click)="selectTab(tab)">
          {{ tab.label }}
        </button>
      }
    </div>
    <div class="tab-body">
      <ng-content />
    </div>
  `,
})
export class TabsComponent {
  tabs = contentChildren(TabComponent);
  activeTab = signal<TabComponent | null>(null);

  ngAfterContentInit() {
    if (this.tabs().length > 0) {
      this.selectTab(this.tabs()[0]);
    }
  }

  selectTab(tab: TabComponent) {
    this.tabs().forEach(t => t.active.set(false));
    tab.active.set(true);
    this.activeTab.set(tab);
  }
}

Key Takeaways

  • <ng-content> is Angular's content projection (slot) mechanism
  • Use select attribute for multi-slot projection (header, body, footer)
  • <ng-template> defines a lazy template — not rendered until explicitly used
  • <ng-container> groups elements without adding DOM nodes
  • ngTemplateOutlet renders templates dynamically with context — great for reusable components
  • let-variable in ng-template receives context; $implicit is the default variable
  • Use @ContentChild / contentChild() to access projected content programmatically