Angular Content Projection: ng-content, ng-template and Template Outlets

Angular Tutorial: Beginner to Advanced · 14. Directives and dynamic UI · 4 min read · Updated

Reusable components often need the parent to decide part of the content. A card has a title, a body and a footer. A list has items that each parent wants to draw differently. Angular solves this with content projection and templates.

What you will learn

  • Single and multi-slot projection with ng-content
  • How ng-template and ngTemplateOutlet render templates on demand
  • How a component receives a template from its parent with contentChild

1. ng-content slots

// src/app/projection/panel.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-panel',
  template: `
    <section class="panel">
      <header>
        <h3>{{ heading() }}</h3>
        <ng-content select="[panel-actions]" />
      </header>
      <div class="body"><ng-content /></div>
      <footer><ng-content select="[panel-footer]" /></footer>
    </section>
  `,
  styles: `
    .panel { border: 1px solid #ddd; border-radius: .5rem; padding: 1rem; margin: 1rem 0; }
    header { display: flex; justify-content: space-between; align-items: center; }
    footer { color: #666; font-size: .9rem; }
  `,
})
export class Panel {
  readonly heading = input.required<string>();
}

The parent decides what goes in each slot:

<app-panel heading="Weekly report">
  <button panel-actions type="button">Export</button>
  <p>Sales grew by 12% this week.</p>
  <small panel-footer>Updated today</small>
</app-panel>

Content without a select goes into the default <ng-content />. Elements with the attribute panel-actions go to that named slot.

2. ng-template and ngTemplateOutlet

An ng-template is a piece of template that is not rendered until you ask for it. ngTemplateOutlet renders it, and can pass data through a context.

// src/app/projection/list-with-template.ts
import { Component, contentChild, input, TemplateRef } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-smart-list',
  imports: [NgTemplateOutlet],
  template: `
    <ul>
      @for (item of items(); track $index) {
        <li>
          @if (itemTemplate(); as tpl) {
            <ng-container [ngTemplateOutlet]="tpl" [ngTemplateOutletContext]="{ $implicit: item, index: $index }" />
          } @else {
            {{ item }}
          }
        </li>
      } @empty {
        <li>Nothing here yet.</li>
      }
    </ul>
  `,
})
export class SmartList<T = unknown> {
  readonly items = input<T[]>([]);

  // Finds <ng-template #itemTemplate> inside the component's content.
  protected readonly itemTemplate = contentChild<TemplateRef<unknown>>('itemTemplate');
}

The parent supplies how each row looks:

// src/app/projection/projection-demo.ts
import { Component } from '@angular/core';
import { Panel } from './panel';
import { SmartList } from './list-with-template';

@Component({
  selector: 'app-projection-demo',
  imports: [Panel, SmartList],
  template: `
    <app-panel heading="Weekly report">
      <button panel-actions type="button">Export</button>
      <p>Sales grew by 12% this week.</p>
      <small panel-footer>Updated today</small>
    </app-panel>

    <h3>Plain list</h3>
    <app-smart-list [items]="fruits" />

    <h3>Custom rows</h3>
    <app-smart-list [items]="people">
      <ng-template #itemTemplate let-person let-i="index">
        <strong>{{ i + 1 }}. {{ person.name }}</strong> ({{ person.role }})
      </ng-template>
    </app-smart-list>
  `,
})
export class ProjectionDemo {
  protected readonly fruits = ['Apple', 'Banana', 'Cherry'];
  protected readonly people = [
    { name: 'Asha', role: 'Developer' },
    { name: 'Ben', role: 'Designer' },
  ];
}
// src/app/app.ts
import { Component } from '@angular/core';
import { ProjectionDemo } from './projection/projection-demo';

@Component({
  imports: [ProjectionDemo],
  selector: 'app-root',
  templateUrl: './app.html',
  styleUrl: './app.css',
})
export class App {}
<!-- src/app/app.html -->
<main>
  <h1>Projection</h1>
  <app-projection-demo />
</main>

The key parts

  • ng-content is a slot. Projected content is created by the parent and is not controlled by the component.
  • ng-template plus ngTemplateOutlet lets the component render the template once per item, with data passed in the context.
  • let-person reads the $implicit value. let-i="index" reads the named index property.
  • contentChild looks inside the content projected by the parent. Use viewChild for elements in the component's own template.
  • The component works with a default row, and the parent can override it. That is a flexible, still simple API.

Try it yourself

  1. Add a header template to SmartList in the same way as itemTemplate.
  2. Add an empty-state template slot that the parent can customise.
  3. Build a Tabs component that uses ng-content and contentChildren.

Quick check

  1. What does the select attribute of ng-content do?
  2. What is the difference between contentChild and viewChild?
  3. How do you pass data into a template rendered with ngTemplateOutlet?

Next, create components at run time in dynamic components and host directives. See the content projection guide.

Frequently asked questions

Is projected content created if the slot is not rendered?

Yes, it is created by the parent even if the component never shows it. Use ng-template when content should be created only on demand.

Should I always use templates instead of inputs?

No. Use plain inputs for data and templates only when the parent needs to control the markup.