Understanding Content Projection in Angular

Angular's content projection (using <ng-content>) is an incredibly powerful tool for creating reusable wrapper components. However, when you need to conditionally hide projected elements or interact with them directly from the wrapper component, things can get tricky. Let's address the two core challenges: conditional projection safety and querying projected elements using modern Angular practices.

Part A: Is Conditionally Hiding <ng-content> Safe?

The short answer is no, wrapping <ng-content> inside an @if block is not recommended. As the official Angular documentation states, Angular always instantiates and creates DOM nodes for projected content, regardless of whether the <ng-content> placeholder is rendered.

If you use @if on <ng-content>, the lifecycle hooks (like ngOnInit) of the projected components will still execute immediately when the parent component is initialized, even if they are hidden. This can lead to unexpected side effects, memory leaks, or performance bottlenecks.

The Solution: Use Template-Based Projection

If you need true lazy instantiation (where the projected component is only created when shown), you should project a <ng-template> instead of raw HTML/components, and render it using *ngTemplateOutlet or ngTemplateOutlet.

// Wrapper Component HTML
@if (isVisible) {
  <ng-container [ngTemplateOutlet]="contentTemplate"></ng-container>
}

// Wrapper Component TS
import { Component, ContentChild, TemplateRef } from '@angular/core';

@Component({
  selector: 'app-wrapper',
  templateUrl: './wrapper.component.html'
})
export class WrapperComponent {
  isVisible = true;
  @ContentChild('myContent') contentTemplate!: TemplateRef<any>;
}

In your main template, you would wrap the projected content like this:

<app-wrapper>
  <ng-template #myContent>
    <app-child-component></app-child-component>
  </ng-template>
</app-wrapper>
---

Part B: How to Correctly Query and Disable Projected Elements

To access elements or components that are passed into a wrapper via content projection, you must use @ContentChild (or @ContentChildren) rather than @ViewChild. @ViewChild only queries elements defined directly inside the wrapper's own template, whereas @ContentChild queries elements projected from the outside (the light DOM).

Using Modern Angular Signal Queries

In modern Angular (including Angular 17, 18, and 21), it is highly recommended to use the new Signal-based queries: contentChild and contentChildren. They are reactive, safer, and do not suffer from lifecycle timing issues.

Here is how you can query native HTML elements (like inputs, buttons, checkboxes) and custom child components to disable them:

// wrapper.component.ts
import { Component, contentChild, ElementRef } from '@angular/core';
import { ChildComponent } from './child.component';

@Component({
  selector: 'app-wrapper',
  standalone: true,
  template: `
    <div class="wrapper">
      <ng-content></ng-content>
      <button (click)="disableAll()">Disable All</button>
    </div>
  `
})
export class WrapperComponent {
  // Query native elements using template reference variables
  private button = contentChild<ElementRef<HTMLButtonElement>>('btnincr');
  private input = contentChild<ElementRef<HTMLInputElement>>('inpfield');
  private checkbox = contentChild<ElementRef<HTMLInputElement>>('cbx');

  // Query the child component directly by its class type
  private childComponent = contentChild(ChildComponent);

  disableAll() {
    // Disable native HTML elements
    const btn = this.button();
    if (btn) btn.nativeElement.disabled = true;

    const inp = this.input();
    if (inp) inp.nativeElement.disabled = true;

    const cb = this.checkbox();
    if (cb) cb.nativeElement.disabled = true;

    // Call the custom disable method on the projected child component
    const child = this.childComponent();
    if (child) {
      child.disableit();
    }
  }
}

The Main Template Setup

To make the queries above work, ensure you assign matching template reference variables (with the # prefix) to your HTML elements in your main template:

<app-wrapper>
  <button #btnincr>Increment</button>
  <input #inpfield type="text" />
  <input #cbx type="checkbox" />
  <app-child></app-child>
</app-wrapper>

Summary

  • Do not use @if directly on <ng-content> if you want to avoid instant initialization. Use ngTemplateOutlet with <ng-template> instead.
  • Use @ContentChild or the modern contentChild() signal query to select projected elements. @ViewChild will return undefined for projected elements.
  • Query native elements using their template reference string (e.g., 'btnincr') and read them as ElementRef, while custom components can be queried directly by their class type.