Content projection înseamnă că o componentă afișează conținut primit de la părinte, scris între tag-urile ei. E mecanismul din spatele oricărei componente de tip „container”: card, dialog, layout, panou, tab. Componenta definește unde apare conținutul cu <ng-content>; părintele decide ce apare.
Sloturi multiple cu select
@Component({
selector: 'app-card',
template: `
<header>
<ng-content select="[card-title]">Fără titlu</ng-content> <!-- fallback (v18+) -->
<ng-content select="[card-actions]" />
</header>
<div class="body">
<ng-content /> <!-- slotul implicit: restul -->
</div>
<footer>
<ng-content select="app-card-footer" /> <!-- după tag -->
</footer>
`,
})
export class Card {}3 produse · 1.249 lei · livrare mâine.
Card fără titlu și fără footer: slot-urile goale folosesc fallback-ul sau dispar.
ngProjectAsUtil când wrapper-ul nu poate purta atributul de selecție.
Ce poate fi în select
<ng-content select="app-card-footer" /> <!-- tag -->
<ng-content select="[card-title]" /> <!-- atribut -->
<ng-content select=".highlight" /> <!-- clasă -->
<ng-content select="h2, h3" /> <!-- listă de selectori -->
<ng-content select="button:not(.secondary)" /> <!-- :not() -->
<ng-content /> <!-- tot ce n-a fost prins de altele -->ngProjectAs
<!-- Vrei să proiectezi mai multe elemente într-un slot, fără un wrapper în DOM -->
<app-card>
<ng-container ngProjectAs="[card-title]">
<app-icon name="box" /> Comandă #1042
</ng-container>
</app-card>Capcana importantă: conținutul proiectat e creat oricum
Componentele proiectate sunt instanțiate de părinte, ca parte din template-ul părintelui. Dacă copilul pune <ng-content> într-un @if, doar DOM-ul e ascuns — componentele sunt deja create, au rulat constructorul, au pornit request-uri. Iar <ng-content> nu poate fi „re-proiectat” de două ori. Soluția: primește un <ng-template> și instanțiază-l când chiar ai nevoie.
- ExpensiveWidget #1 CREAT
// ✓ Primește un template și îl instanțiază doar când e nevoie
@Component({
selector: 'app-collapsible',
imports: [NgTemplateOutlet],
template: `
<button (click)="open.set(!open())">Toggle</button>
@if (open()) { <ng-container [ngTemplateOutlet]="content() ?? null" /> }
`,
})
export class Collapsible {
readonly content = contentChild(TemplateRef);
readonly open = signal(false);
}
// utilizare:
// <app-collapsible>
// <ng-template><app-heavy-report /></ng-template>
// </app-collapsible>// ✗ Conținutul proiectat e creat de PĂRINTE, indiferent dacă copilul îl afișează.
@Component({
selector: 'app-collapsible',
template: `
<button (click)="open.set(!open())">Toggle</button>
@if (open()) { <ng-content /> } <!-- ascunde DOM-ul, dar componentele există deja -->
`,
})
export class Collapsible {
readonly open = signal(false);
}Proiecție sau input?
// Proiecție vs input — cum alegi?
// Input: componenta controlează CUM arată datele (format fix, consistent)
<app-user-chip [user]="user()" />
// Proiecție: părintele controlează CE apare (markup liber, flexibil)
<app-dialog>
<h2 dialog-title>Ștergi proiectul?</h2>
<p>Acțiunea nu poate fi anulată.</p>
<button dialog-actions (click)="confirm()">Șterge</button>
</app-dialog>- Componente-container: card, dialog, drawer, layout, panou.
- Părintele are nevoie de markup liber (link-uri, iconițe, butoane proprii).
- Vrei să eviți zeci de input-uri de tip
titleIcon,titleColor,showTitleBadge...
- Datele trebuie afișate mereu la fel (consistență vizuală) → input-uri.
- Componenta trebuie să proceseze datele (sortare, formatare) → input-uri.
- Conținutul trebuie repetat sau creat condiționat → ng-template.
Cum afli dacă un slot e gol
Un card cu subsol trebuie uneori să ascundă și chenarul subsolului când nu primește nimic. Ai două variante. Dacă slotul primește o componentă sau o directivă, contentChild() îți spune dacă există. Dacă e doar markup, CSS-ul :empty pe elementul care înconjoară slotul ajunge.
@Component({
selector: 'app-card',
template: `
<div class="body"><ng-content /></div>
@if (footer()) {
<!-- condiția e pe ramă; conținutul subsolului e creat oricum -->
<footer><ng-content select="app-card-footer" /></footer>
}
`,
})
export class Card {
protected readonly footer = contentChild(CardFooter); // există un <app-card-footer> în conținut?
}
/* sau, pentru markup simplu: */
.actions:empty {
display: none;
}Cine stilizează conținutul proiectat
Conținutul proiectat aparține părintelui: e creat în template-ul lui, deci primește stilurile părintelui, nu pe ale componentei care îl afișează. Stilurile componentei se aplică doar elementelor ei. Componenta stilizează deci „rama” (spațiere, chenar, poziție), iar părintele stilizează ce pune înăuntru. Dacă trebuie totuși să atingi conținutul proiectat, folosește variabile CSS pe :host (vezi Stilurile componentelor).
/* card.css — componenta stilizează rama */
:host {
display: block;
padding: var(--card-padding, 1rem);
border: 1px solid var(--border);
}
p {
color: red; /* ✗ nu atinge <p>-ul primit prin proiecție */
}
/* page.css — părintele stilizează ce pune în card */
app-card p {
color: var(--text-2);
}Capcane frecvente
- select pe elemente din @if/@for ale părintelui: funcționează doar dacă elementul cu atributul e „rădăcina” blocului; altfel ajunge în slotul implicit. Folosește
ngProjectAspe unng-container. - Același
<ng-content>de două ori: conținutul apare doar în ultimul. Pentru repetare folosește template-uri. - Stilizarea conținutului proiectat: CSS-ul componentei nu atinge elementele părintelui (încapsulare). Stilizează wrapper-ul sau folosește variabile CSS.
Fișă rapidă
<ng-content /> <!-- slotul implicit: tot ce nu prinde alt select -->
<ng-content select="[card-title]" /> <!-- după atribut -->
<ng-content select="app-card-footer" /> <!-- după tag -->
<ng-content select="h2, h3" /> <!-- listă de selectori -->
<ng-content select="[card-title]">Fără titlu</ng-content> <!-- conținut implicit (v18+) -->
<h2 ngProjectAs="[card-title]">...</h2> <!-- pare alt element pentru select -->
<!-- conținutul proiectat e creat MEREU, chiar dacă slotul e ascuns → creare la cerere: ng-template -->
<!-- stilurile conținutului proiectat vin de la părinte -->