La projection de contenu signifie qu’un composant affiche du contenu reçu de son parent, écrit entre ses balises. C’est le mécanisme derrière tout composant « conteneur » : carte, boîte de dialogue, layout, panneau, onglet. Le composant définit où le contenu apparaît avec <ng-content> ; le parent décide de ce qui apparaît.
Plusieurs slots avec select
@Component({
selector: 'app-card',
template: `
<header>
<ng-content select="[card-title]">Sans titre</ng-content> <!-- repli (v18+) -->
<ng-content select="[card-actions]" />
</header>
<div class="body">
<ng-content /> <!-- le slot par défaut : le reste -->
</div>
<footer>
<ng-content select="app-card-footer" /> <!-- après la balise -->
</footer>
`,
})
export class Card {}3 produits · 1 249 RON · livraison demain.
Carte sans titre et sans pied de carte : les slots vides utilisent le contenu de repli ou disparaissent.
ngProjectAsUtile quand l’élément enveloppant ne peut pas porter l’attribut de sélection.
Ce que select accepte
<ng-content select="app-card-footer" /> <!-- balise -->
<ng-content select="[card-title]" /> <!-- attribut -->
<ng-content select=".highlight" /> <!-- classe -->
<ng-content select="h2, h3" /> <!-- liste de sélecteurs -->
<ng-content select="button:not(.secondary)" /> <!-- :not() -->
<ng-content /> <!-- tout ce que les autres n’ont pas capté -->ngProjectAs
<!-- Vous voulez projeter plusieurs éléments dans un seul slot, sans élément enveloppant dans le DOM -->
<app-card>
<ng-container ngProjectAs="[card-title]">
<app-icon name="box" /> Commande n° 1042
</ng-container>
</app-card>Le piège important : le contenu projeté est créé quand même
Les composants projetés sont instanciés par le parent, dans le cadre du template du parent. Si l’enfant place <ng-content> dans un @if, seul le DOM est masqué — les composants existent déjà, leurs constructeurs ont été exécutés, les requêtes ont démarré. Et <ng-content> ne peut pas être « projeté » deux fois. La solution : recevoir un <ng-template> et l’instancier quand vous en avez vraiment besoin.
- ExpensiveWidget #3 CRÉÉ
// ✓ Reçoit un template et ne l’instancie que si nécessaire
@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);
}
// utilisation :
// <app-collapsible>
// <ng-template><app-heavy-report /></ng-template>
// </app-collapsible>// ✗ Le contenu projeté est créé par le PARENT, que l’enfant l’affiche ou non.
@Component({
selector: 'app-collapsible',
template: `
<button (click)="open.set(!open())">Toggle</button>
@if (open()) { <ng-content /> } <!-- masque le DOM, mais les composants existent déjà -->
`,
})
export class Collapsible {
readonly open = signal(false);
}Projection ou input ?
// Projection ou input — comment choisir ?
// Input : le composant contrôle COMMENT les données s’affichent (format fixe et cohérent)
<app-user-chip [user]="user()" />
// Projection : le parent contrôle CE QUI apparaît (balisage libre, flexible)
<app-dialog>
<h2 dialog-title>Supprimer le projet ?</h2>
<p>Cette action est irréversible.</p>
<button dialog-actions (click)="confirm()">Supprimer</button>
</app-dialog>- Composants conteneurs : carte, boîte de dialogue, drawer, layout, panneau.
- Le parent a besoin d’un balisage libre (liens, icônes, ses propres boutons).
- Vous voulez éviter des dizaines d’inputs comme
titleIcon,titleColor,showTitleBadge...
- Les données doivent toujours avoir le même aspect (cohérence visuelle) → inputs.
- Le composant doit traiter les données (tri, mise en forme) → inputs.
- Le contenu doit être répété ou créé de façon conditionnelle → ng-template.
Savoir si un slot est vide
Une carte avec pied doit parfois masquer aussi la bordure du pied quand rien n’est transmis. Deux options. Si le slot reçoit un composant ou une directive, contentChild() vous dit s’il existe. Si c’est du simple markup, le CSS :empty sur l’élément qui entoure le slot suffit.
@Component({
selector: 'app-card',
template: `
<div class="body"><ng-content /></div>
@if (footer()) {
<!-- la condition porte sur le cadre ; le contenu du pied est créé quand même -->
<footer><ng-content select="app-card-footer" /></footer>
}
`,
})
export class Card {
protected readonly footer = contentChild(CardFooter); // y a-t-il un <app-card-footer> dans le contenu ?
}
/* ou, pour du simple markup : */
.actions:empty {
display: none;
}Qui stylise le contenu projeté
Le contenu projeté appartient au parent : il est créé dans son template, il reçoit donc les styles du parent, pas ceux du composant qui l’affiche. Les styles du composant ne s’appliquent qu’à ses propres éléments. Le composant stylise donc le « cadre » (espacement, bordure, position), et le parent ce qu’il met dedans. S’il faut quand même atteindre le contenu projeté, utilisez des variables CSS sur :host (voir Styles des composants).
/* card.css — le composant stylise le cadre */
:host {
display: block;
padding: var(--card-padding, 1rem);
border: 1px solid var(--border);
}
p {
color: red; /* ✗ n’atteint pas le <p> reçu par projection */
}
/* page.css — le parent stylise ce qu’il met dans la carte */
app-card p {
color: var(--text-2);
}Pièges fréquents
- select sur des éléments à l’intérieur d’un @if/@for du parent : cela ne fonctionne que si l’élément portant l’attribut est la « racine » du bloc ; sinon il atterrit dans le slot par défaut. Utilisez
ngProjectAssur unng-container. - Le même
<ng-content>deux fois : le contenu n’apparaît que dans le dernier. Pour répéter, utilisez des templates. - Styliser le contenu projeté : le CSS du composant n’atteint pas les éléments du parent (encapsulation). Stylez l’élément enveloppant ou utilisez des variables CSS.
Aide-mémoire
<ng-content /> <!-- le slot par défaut : tout ce qu’aucun autre select n’attrape -->
<ng-content select="[card-title]" /> <!-- par attribut -->
<ng-content select="app-card-footer" /> <!-- par balise -->
<ng-content select="h2, h3" /> <!-- une liste de sélecteurs -->
<ng-content select="[card-title]">Sans titre</ng-content> <!-- contenu par défaut (v18+) -->
<h2 ngProjectAs="[card-title]">...</h2> <!-- ressemble à un autre élément pour select -->
<!-- le contenu projeté est TOUJOURS créé, même si le slot est caché → création à la demande : ng-template -->
<!-- les styles du contenu projeté viennent du parent -->