Angular Content Projection: How to Safely Conditionally Render and Query Projected Elements
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
@ifdirectly on<ng-content>if you want to avoid instant initialization. UsengTemplateOutletwith<ng-template>instead. - Use
@ContentChildor the moderncontentChild()signal query to select projected elements.@ViewChildwill returnundefinedfor projected elements. - Query native elements using their template reference string (e.g.,
'btnincr') and read them asElementRef, while custom components can be queried directly by their class type.