Content projection and queries

Angular · Practical · 12 min

Layout components (cards, dialogs, tabs) depend on projection, and integrations need direct element access.

Content projection lets a parent pass markup into a child. The child marks where it goes with <ng-content />:

<!-- card template -->
<div class="card"><ng-content /></div>
<!-- usage --> <app-card><p>Any content</p></app-card>

Multiple slots use select with a CSS selector: <ng-content select="[card-title]" /> receives elements with that attribute, and a plain <ng-content /> takes the rest. A fallback can be placed inside <ng-content> itself. Projected content is created by the parent and belongs to the parent's scope; the child can't conditionally create or re-create it. For lazily rendered or repeated content, pass an <ng-template> and render it with ngTemplateOutlet.

Queries give a component references to things in its template or projected content, as signals:

  • viewChild('ref') finds an element or component with a template reference variable #ref in its own template; viewChild(ChildComponent) finds by type. The result is an ElementRef for elements.
  • viewChild.required(...) when it always exists; viewChildren(...) for all matches.
  • contentChild(...) and contentChildren(...) search projected content, e.g. a tabs component finding its <app-tab> children.

Query signals are undefined until the view is created, so read them in computed, effect, afterNextRender or event handlers rather than the constructor.

Example

import { Component, ElementRef, afterNextRender, contentChildren, input, viewChild } from '@angular/core';

@Component({ selector: 'app-tab', template: `<ng-content />` })
export class Tab { label = input.required<string>(); }
@Component({
  selector: 'app-tabs',
  template: `
    <nav>@for (t of tabs(); track t.label()) { <button>{{ t.label() }}</button> }</nav>
    <ng-content select="[tabs-header]" />
    <ng-content />
    <input #search placeholder="search" />`,
})
export class Tabs {
  protected tabs = contentChildren(Tab);                 // projected <app-tab>s
  private search = viewChild.required<ElementRef<HTMLInputElement>>('search');
  constructor() { afterNextRender(() => this.search().nativeElement.focus()); }
}
@Component({
  selector: 'app-page',
  imports: [Tabs, Tab],
  template: `<app-tabs><h2 tabs-header>Settings</h2>
    <app-tab label="Profile">Profile form</app-tab><app-tab label="Billing">Billing</app-tab>
  </app-tabs>`,
})
export class Page {}

Remember

  • <ng-content /> projects parent markup; select creates named slots
  • viewChild/viewChildren query the component's own template
  • contentChild/contentChildren query projected content; results are signals

Watch out for

  • Reading a view query in the constructor, before the view exists.

Check yourself

  1. A Tabs component needs the list of <app-tab> elements the parent placed between its tags. Which query fits?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: angular.dev/guide/components/content-projection