Content projection lets a parent component inject HTML content into designated slots in a child component’s template. This is the Angular equivalent of HTML slots — a CardComponent can define a <ng-content> slot where the parent places the card’s body content, making the card reusable across different contexts. @ViewChild gives a component programmatic access to child components or DOM elements in its template — essential for interacting with third-party libraries that require a DOM element.
Content Projection
// ── Reusable card component with content projection slots ─────────────────
@Component({
selector: 'app-card',
standalone: true,
template: `
<div class="card">
<!-- Named slot: receives content marked with select="[card-header]" -->
<div class="card-header">
<ng-content select="[card-header]" />
</div>
<!-- Default slot: receives all content NOT matched by other selectors -->
<div class="card-body">
<ng-content />
</div>
<!-- Named footer slot ─────────────────────────────────────────────── -->
<div class="card-footer">
<ng-content select="[card-footer]" />
</div>
</div>
`,
styles: [`
.card { border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; }
.card-header { padding: 1rem; background: #f5f5f5; font-weight: bold; }
.card-body { padding: 1rem; }
.card-footer { padding: 0.75rem 1rem; border-top: 1px solid #e0e0e0; }
`],
})
export class CardComponent { }
// ── Using the card with projected content ─────────────────────────────────
@Component({
selector: 'app-post-detail',
standalone: true,
imports: [CardComponent],
template: `
<app-card>
<!-- Goes into [card-header] slot ─────────────────────────────────── -->
<h2 card-header>{{ post?.title }}</h2>
<!-- Goes into default slot ──────────────────────────────────────── -->
<p>{{ post?.body }}</p>
<p class="tags">{{ post?.tags.join(', ') }}</p>
<!-- Goes into [card-footer] slot ─────────────────────────────────── -->
<div card-footer>
<button (click)="onEdit()">Edit</button>
<button (click)="onDelete()">Delete</button>
</div>
</app-card>
`,
})
export class PostDetailComponent {
post = signal<PostDto | null>(null);
}
@ViewChild to access a child component’s public API or a DOM element after the view is initialised. Common use cases: focusing an input element after a modal opens (@ViewChild('searchInput') searchInput!: ElementRef; ngAfterViewInit() { this.searchInput.nativeElement.focus(); }), calling a method on a child component, or initialising a chart library that requires a DOM canvas element. Access @ViewChild references in ngAfterViewInit — they are undefined in ngOnInit because the view has not been rendered yet.ElementRef.nativeElement except when absolutely necessary. Direct DOM access bypasses Angular’s change detection, breaks server-side rendering, and can introduce XSS vulnerabilities if used with user-provided content. Use Angular’s binding syntax ([style], [class], renderer2) instead. Reserve nativeElement for cases that cannot be expressed through Angular bindings — like .focus(), reading measured dimensions, or initialising third-party DOM libraries.ViewChild Example
import { Component, ViewChild, ElementRef, AfterViewInit, signal } from '@angular/core';
@Component({
standalone: true,
template: `
<input #searchInput type="text" (input)="onSearch($event)" placeholder="Search...">
<!-- Template reference variable #searchInput ──────────────────────── -->
`,
})
export class SearchComponent implements AfterViewInit {
// Access the element with the #searchInput template reference variable
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
searchTerm = signal('');
ngAfterViewInit(): void {
// DOM element available here — not in ngOnInit
this.searchInput.nativeElement.focus();
}
onSearch(event: Event): void {
const input = event.target as HTMLInputElement;
this.searchTerm.set(input.value);
}
}
Common Mistakes
Mistake 1 — Accessing @ViewChild in ngOnInit (undefined — view not yet rendered)
❌ Wrong — ngOnInit() { this.myInput.nativeElement.focus(); } — throws “Cannot read property ‘nativeElement’ of undefined”.
✅ Correct — access @ViewChild references in ngAfterViewInit().
Mistake 2 — Using innerHTML with user data (XSS vulnerability)
❌ Wrong — this.el.nativeElement.innerHTML = userContent — executes any JavaScript in userContent.
✅ Correct — use Angular’s interpolation ({{ safeText }}) which HTML-escapes content automatically.