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#refin its own template;viewChild(ChildComponent)finds by type. The result is anElementReffor elements.viewChild.required(...)when it always exists;viewChildren(...)for all matches.contentChild(...)andcontentChildren(...)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;selectcreates named slotsviewChild/viewChildrenquery the component's own templatecontentChild/contentChildrenquery projected content; results are signals
Watch out for
- Reading a view query in the constructor, before the view exists.