Why does an Angular card that wraps <ng-content select="[card-body]"> in @if (expanded()) still create the projected body, and how do you make it lazy?
answer
- the consumer's view owns the content
- created before the card decides
- hiding a slot does not destroy it
- pass a template instead
- render it only when needed
basics
~20 sProjected content belongs to the consumer's view, so Angular creates it, runs its lifecycle and keeps it alive whether or not the card's @if shows the slot. For lazy content, have the consumer pass an <ng-template> and render it only when expanded.
solid answer
~40 sContent written between `<app-card>` tags is declared in the **consumer's** template, so it is part of the consumer's view. Angular creates those nodes and component instances when the consumer's view is created, runs their `ngOnInit`, and checks them with the consumer, before and regardless of whether the card's own `@if` places `<ng-content>` on screen. Hiding the placeholder only detaches the DOM; nothing is destroyed or recreated. The projection guide therefore says not to wrap `<ng-content>` in `@if`, `@for` or `@switch`. To create the body only when it is shown, have the consumer wrap it in `<ng-template>` and hand the `TemplateRef` to the card, as an input or through a content query, and let the card instantiate it inside its `@if`, typically with `ngTemplateOutlet`. Each expand then creates the body and each collapse destroys it.
code
html · 10 lines<!-- Eager: app-sales-chart is created with this template, even if the card never expands -->
<app-card>
<app-sales-chart card-body />
</app-card>
<!-- Lazy: the card instantiates the template only while expanded -->
<app-card [body]="chartTpl" />
<ng-template #chartTpl>
<app-sales-chart />
</ng-template>go deeper
Remember that content you project is created by your template even if the receiving component hides it.
Explain that projected nodes belong to the consumer's view, so creation, destruction and checking follow the consumer, not the slot.
Replace conditional ng-content with a template the component renders on demand, and catch the pattern in reviews of heavy or polling children.
Decide which library containers accept content and which accept templates, trading consumer ergonomics against lazy creation and reuse.
## The symptom A collapsible card hides its body until the user expands it: ```ts import {Component, signal} from '@angular/core'; @Component({ selector: 'app-card', template: ` <button (click)="expanded.set(!expanded())">Details</button> @if (expanded()) { <ng-content select="[card-body]" /> } `, }) export class Card { expanded = signal(false); } ``` The consumer puts a heavy chart in the body: ```html <app-card> <app-sales-chart card-body /> </app-card> ``` The chart loads its data on page load, even though no card was ever expanded, and collapsing a card does not stop its polling timer. The `@if` looks like it should prevent that, but it does not. ## Why: who owns projected content In Angular, **projected content lives in the view of the component that declared it**. `<app-sales-chart>` is written in the consumer's template, so: 1. **Creation** happens with the consumer's view. When the consumer is created, `<app-sales-chart>` is instantiated and its inputs set, and its `ngOnInit` runs when the consumer's view is first checked. The card has no say. 2. **The placeholder only positions nodes.** `<ng-content>` decides where already-created nodes are attached in the DOM. When the card's `@if` is false, the nodes are simply not attached. 3. **Destruction** also follows the consumer. Toggling the card's `@if` does not destroy the chart, so its `ngOnDestroy` does not run and anything it started keeps running. 4. **Change detection** follows the consumer too: projected content is checked as part of the consumer's view, so a card using `OnPush` (the default for components without a `changeDetection` setting in Angular 22) does not stop the projected chart from being checked with its consumer. The Angular projection guide states the rule directly: do not conditionally include `<ng-content>` with `@if`, `@for` or `@switch`, because Angular always instantiates and creates DOM nodes for content bound for an `<ng-content>`, even when the placeholder is hidden. ## The fix: project a template, not content An `<ng-template>` is a description of content that is not instantiated until someone renders it. If the consumer passes a template, the card controls when it becomes real: ```ts import {Component, TemplateRef, input, signal} from '@angular/core'; import {NgTemplateOutlet} from '@angular/common'; @Component({ selector: 'app-card', imports: [NgTemplateOutlet], template: ` <button (click)="expanded.set(!expanded())">Details</button> @if (expanded()) { <ng-container [ngTemplateOutlet]="body()" /> } `, }) export class Card { body = input.required<TemplateRef<unknown>>(); expanded = signal(false); } ``` ```html <app-card [body]="chartTpl" /> <ng-template #chartTpl><app-sales-chart /></ng-template> ``` Now the chart is created when the card expands and destroyed when it collapses. Its bindings still evaluate in the consumer's context, because the template was declared there. Two common ways to hand over the template: - **As an input**, as above: explicit and easy to type. - **Through a content query**: the consumer writes `<ng-template cardBody>...</ng-template>` inside `<app-card>`, and the card finds it with a content query reading `TemplateRef`. This keeps the familiar nested markup. ## Choosing between the two models | Need | `<ng-content>` | `TemplateRef` rendered by the card | |---|---|---| | Content always shown, simple layout | ideal | unnecessary ceremony | | Create only when visible | not possible | yes | | Destroy on hide, stop timers | no | yes | | Render the same content several times | no, nodes exist once | yes, one instance per render | | Card supplies context, such as the current row | no | yes, via template context | ## Cost of the template approach Passing templates is not free. The consumer's markup becomes less direct, with a named `<ng-template>` and a reference instead of plain nested content. Each expand pays the full creation cost of the body again, and any state inside it, such as a scroll position or a half-filled form, is lost on collapse unless it is stored outside the template. For a body that is cheap and stateful, keeping `<ng-content>` and hiding it with CSS can be the better trade; for one that is expensive or side-effecting, lazy creation wins. ## Review checklist - Any `<ng-content>` inside `@if`, `@for`, `@switch` or `@defer` in a component's own template deserves a question: is the intent lazy creation? If so, it is not happening. - Heavy or side-effecting projected children that should pause when hidden need the template approach, or their own visibility input. - `@defer` inside the *consumer's* content defers loading that consumer-side code, which is a separate tool from the card's decision to show a slot.
- Why can the card not render projected content twice, for example in a @for?Projection moves nodes the consumer's view already created; each node exists once in the DOM, so it cannot appear in two places. A template can be instantiated any number of times, so repeated rendering needs a `TemplateRef` rather than `<ng-content>`.
- Does projected content see providers from the card's viewProviders?No. Projected content resolves dependencies from where it is declared, so the card's `viewProviders` are invisible to it. The card's regular `providers` sit on its host element and are reachable, but the full resolution rules belong to the DI hierarchy topic.
Projecting content is like a guest bringing a finished dish to a dinner: it is cooked before anyone checks whether the host has space on the table. Passing an ng-template is handing over the recipe, so the host cooks it only if and when there is room.
saying these in an interview costs you the question
- Wrapping ng-content in @if defers creation of the projected components
- Collapsing the card destroys and later recreates the projected children
- An OnPush card stops its projected content from being checked
- ng-content inside @for renders the projected content once per item
- Projected content binds against the receiving component's properties