CPCODELAB
Angular

Content Projection and Custom Structural Directives

12 min read

Content Projection with ng-content

Content projection lets a parent component inject HTML into a child component's template at designated slots. The child uses <ng-content> as a placeholder. This is the Angular equivalent of React's children prop and is essential for reusable layout components like cards, modals, and tabs.

ts
// card.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-card',
  standalone: true,
  template: `
    <div class="card">
      <div class="card-header">
        <ng-content select="[slot=header]" />
      </div>
      <div class="card-body">
        <ng-content />  <!-- default slot -->
      </div>
      <div class="card-footer">
        <ng-content select="[slot=footer]" />
      </div>
    </div>
  `
})
export class CardComponent {}
ts
// parent usage
import { Component } from '@angular/core';
import { CardComponent } from './card.component';

@Component({
  selector: 'app-profile-page',
  standalone: true,
  imports: [CardComponent],
  template: `
    <app-card>
      <h2 slot="header">Jane Doe</h2>
      <p>Full-stack developer from Lagos.</p>
      <img src="/jane.jpg" alt="Jane" />
      <div slot="footer">
        <button>Follow</button>
        <button>Message</button>
      </div>
    </app-card>
  `
})
export class ProfilePageComponent {}

Custom Structural Directive with TemplateRef

Structural directives manipulate the DOM by adding or removing <ng-template> views. Writing your own requires TemplateRef (the template to stamp) and ViewContainerRef (where to stamp it).

ts
// repeat.directive.ts
import {
  Directive, Input, OnChanges,
  TemplateRef, ViewContainerRef
} from '@angular/core';

@Directive({
  selector: '[appRepeat]',
  standalone: true
})
export class RepeatDirective implements OnChanges {
  @Input('appRepeat') count = 0;

  constructor(
    private templateRef: TemplateRef<{ $implicit: number }>,
    private viewContainer: ViewContainerRef
  ) {}

  ngOnChanges() {
    this.viewContainer.clear();
    for (let i = 0; i < this.count; i++) {
      this.viewContainer.createEmbeddedView(this.templateRef, { $implicit: i });
    }
  }
}

// Usage in a template:
// <p *appRepeat="3; let i">Item {{ i + 1 }}</p>

The $implicit context key maps to the let variable in the microsyntax (let i). You can expose multiple context properties ({ $implicit: i, last: i === count - 1 }) and bind them with let i; let isLast = last.

Change Detection and OnPush

By default Angular uses Default change detection: it checks every component in the tree on every event. OnPush tells Angular to only check a component when its @Input() references change, an event originates inside it, or an Observable it subscribes to via async pipe emits. This can dramatically reduce unnecessary checks in large trees.

ts
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="card">
      <h3>{{ user.name }}</h3>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserCardComponent {
  @Input({ required: true }) user!: { name: string; email: string };
}

With OnPush, mutating an object or array passed as @Input() will NOT trigger change detection because the reference stays the same. Always pass new object/array references (immutable update pattern) or use signals, which bypass this limitation entirely.

Ready to test yourself?
Practice Angular— quiz & coding exercises