Content Projection and ViewChild — ng-content and Template References

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);
}
Note: Content projection is a powerful pattern for building design systems and reusable UI components (cards, modals, accordions, tabs). The parent controls the content; the child controls the presentation (layout, styling, animation). A card component provides the visual chrome (border, shadow, header/footer styling) while the parent provides the content. This separation of concerns makes it possible to maintain a consistent visual design while allowing each usage site to provide its own data and interactions.
Tip: Use @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.
Warning: Avoid direct DOM manipulation via 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.

🧠 Test Yourself

A ModalComponent has a default <ng-content /> slot. The parent passes <app-modal><p>Content</p></app-modal>. Where does <p>Content</p> appear in the rendered DOM?