Documentation Index Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Content Projection Overview
Estimated Time : 2 hours | Difficulty : Intermediate | Prerequisites : Components, Templates
Content projection allows you to create flexible, reusable components by inserting content from a parent component into designated slots in the child component. The analogy is a picture frame: the frame (child component) provides the structure and styling, but the picture (projected content) comes from whoever uses the frame. This is the fundamental pattern behind every reusable layout component — cards, modals, tabs, accordions, and data tables.
Why this matters : Without content projection, you end up with one of two bad outcomes: either your components are too rigid (hard-coded templates that cannot be customized) or too flexible (accepting dozens of inputs to cover every variation). Content projection hits the sweet spot — the component controls the layout and behavior, while the consumer controls the content.
┌─────────────────────────────────────────────────────────────────────────┐
│ Content Projection Types │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Single-slot Projection │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ <app-card> │ ───► │ Card Component │ │
│ │ <p>Content</p> │ │ ┌──────────────┐ │ │
│ │ </app-card> │ │ │ <ng-content>│ │ │
│ └────────────────────┘ │ └──────────────┘ │ │
│ └────────────────────┘ │
│ │
│ 2. Multi-slot Projection (Named slots) │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ <app-modal> │ │ Modal Component │ │
│ │ <h2 slot="header"> │ ──► │ ┌────────────────┐ │ │
│ │ <div slot="body"> │ │ │ header slot │ │ │
│ │ <button slot="footer│ │ ├────────────────┤ │ │
│ │ </app-modal> │ │ │ body slot │ │ │
│ └────────────────────────┘ │ ├────────────────┤ │ │
│ │ │ footer slot │ │ │
│ │ └────────────────┘ │ │
│ └────────────────────────┘ │
│ │
│ 3. Conditional Projection │
│ Content shown/hidden based on component logic │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Single-Slot Projection
// card.component.ts
@ Component ({
selector: 'app-card' ,
standalone: true ,
template: `
<div class="card" [class.elevated]="elevated()">
<ng-content />
</div>
` ,
styles: `
.card {
padding: 1.5rem;
border-radius: 8px;
background: white;
border: 1px solid #e2e8f0;
}
.card.elevated {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
`
})
export class CardComponent {
elevated = input ( false );
}
// Usage
@ Component ({
template: `
<app-card [elevated]="true">
<h2>Card Title</h2>
<p>This content is projected into the card.</p>
<button>Action</button>
</app-card>
`
})
export class PageComponent {}
Multi-Slot Projection
Using select Attribute
Multi-slot projection uses CSS-like selectors on <ng-content> to route projected content into the right slot. The select attribute accepts element names, CSS classes, and attribute selectors. Any projected content that does not match a select goes into the default (un-selected) <ng-content> slot. This is analogous to named slots in Vue or React’s children vs named props pattern.
// modal.component.ts
@ Component ({
selector: 'app-modal' ,
standalone: true ,
template: `
<div class="modal-overlay" (click)="close()">
<div class="modal" (click)="$event.stopPropagation()">
<header class="modal-header">
<ng-content select="[modal-header]" />
<button class="close-btn" (click)="close()">×</button>
</header>
<main class="modal-body">
<ng-content select="[modal-body]" />
</main>
<footer class="modal-footer">
<ng-content select="[modal-footer]" />
</footer>
</div>
</div>
` ,
styleUrl: './modal.component.scss'
})
export class ModalComponent {
closed = output < void >();
close () {
this . closed . emit ();
}
}
// Usage
@ Component ({
template: `
@if (showModal()) {
<app-modal (closed)="showModal.set(false)">
<h2 modal-header>Confirm Action</h2>
<div modal-body>
<p>Are you sure you want to proceed?</p>
</div>
<div modal-footer>
<button (click)="cancel()">Cancel</button>
<button class="primary" (click)="confirm()">Confirm</button>
</div>
</app-modal>
}
`
})
export class ConfirmDialogComponent {
showModal = signal ( false );
}
Using CSS Selectors
// data-table.component.ts
@ Component ({
selector: 'app-data-table' ,
standalone: true ,
template: `
<table class="data-table">
<thead>
<tr>
<!-- Select by element name -->
<ng-content select="th" />
</tr>
</thead>
<tbody>
<!-- Select by CSS class -->
<ng-content select=".table-row" />
</tbody>
<tfoot>
<!-- Select by attribute -->
<ng-content select="[table-footer]" />
</tfoot>
</table>
<!-- Default slot (unmatched content) -->
<ng-content />
`
})
export class DataTableComponent {}
// Usage
@ Component ({
template: `
<app-data-table>
<th>Name</th>
<th>Email</th>
<th>Actions</th>
@for (user of users(); track user.id) {
<tr class="table-row">
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td><button>Edit</button></td>
</tr>
}
<tr table-footer>
<td colspan="3">Total: {{ users().length }} users</td>
</tr>
</app-data-table>
`
})
export class UserTableComponent {
users = input . required < User []>();
}
ng-template with Content Projection
ng-template combined with ngTemplateOutlet is the Angular equivalent of React’s “render props” pattern. The parent provides a template (the “how to render each item”) and the child provides the data and controls when to render it. This inverts the typical control flow: the child component decides when and where to render, while the parent decides what to render. It is the ultimate tool for building highly flexible components like tabs, tables, and lists.
Practical tip : When a component accepts a TemplateRef input, always provide a sensible default template using a local ng-template with #defaultTemplate. This way, the component works out of the box without requiring the consumer to always pass a custom template.
// tabs.component.ts
@ Component ({
selector: 'app-tabs' ,
standalone: true ,
template: `
<div class="tabs">
<div class="tab-headers">
@for (tab of tabs(); track tab.label; let i = $index) {
<button
class="tab-header"
[class.active]="activeIndex() === i"
(click)="activeIndex.set(i)"
>
{{ tab.label }}
</button>
}
</div>
<div class="tab-content">
<ng-container
[ngTemplateOutlet]="tabs()[activeIndex()].content"
/>
</div>
</div>
`
})
export class TabsComponent {
tabs = input . required < Tab []>();
activeIndex = signal ( 0 );
}
interface Tab {
label : string ;
content : TemplateRef < any >;
}
// tab.component.ts
@ Directive ({
selector: '[appTab]' ,
standalone: true
})
export class TabDirective {
label = input . required < string >({ alias: 'appTab' });
constructor ( public templateRef : TemplateRef < any >) {}
}
// Usage
@ Component ({
template: `
<app-tabs [tabs]="tabList()">
</app-tabs>
<ng-template appTab="Profile" #profile>
<app-user-profile [user]="currentUser()" />
</ng-template>
<ng-template appTab="Settings" #settings>
<app-settings />
</ng-template>
<ng-template appTab="Activity" #activity>
<app-activity-log [userId]="currentUser().id" />
</ng-template>
`
})
export class UserDashboardComponent {
@ ViewChildren ( TabDirective ) tabDirectives !: QueryList < TabDirective >;
currentUser = input . required < User >();
tabList = computed (() =>
this . tabDirectives . map ( dir => ({
label: dir . label (),
content: dir . templateRef
}))
);
}
ContentChild & ContentChildren
@ContentChild and @ContentChildren let a component inspect and interact with the content projected into it. Think of a playlist app: the playlist component (parent) does not just display songs (projected children) — it also needs to know about them to handle “play next” logic, enforce “only one song playing at a time,” or reorder them. @ContentChildren gives the parent a QueryList of projected child components, enabling exactly this kind of coordination.
// accordion.component.ts
@ Component ({
selector: 'app-accordion' ,
standalone: true ,
template: `
<div class="accordion">
<ng-content />
</div>
`
})
export class AccordionComponent implements AfterContentInit {
@ ContentChildren ( AccordionItemComponent ) items !: QueryList < AccordionItemComponent >;
allowMultiple = input ( false );
ngAfterContentInit () {
this . items . forEach ( item => {
item . toggled . subscribe (( isOpen ) => {
if ( isOpen && ! this . allowMultiple ()) {
this . closeOthers ( item );
}
});
});
}
private closeOthers ( openedItem : AccordionItemComponent ) {
this . items . forEach ( item => {
if ( item !== openedItem ) {
item . close ();
}
});
}
}
// accordion-item.component.ts
@ Component ({
selector: 'app-accordion-item' ,
standalone: true ,
template: `
<div class="accordion-item" [class.open]="isOpen()">
<button class="accordion-header" (click)="toggle()">
<ng-content select="[accordion-header]" />
<span class="icon">{{ isOpen() ? '−' : '+' }}</span>
</button>
@if (isOpen()) {
<div class="accordion-body" @slideInOut>
<ng-content select="[accordion-body]" />
</div>
}
</div>
` ,
animations: [
trigger ( 'slideInOut' , [
transition ( ':enter' , [
style ({ height: 0 , opacity: 0 }),
animate ( '200ms ease-out' , style ({ height: '*' , opacity: 1 }))
]),
transition ( ':leave' , [
animate ( '200ms ease-in' , style ({ height: 0 , opacity: 0 }))
])
])
]
})
export class AccordionItemComponent {
isOpen = signal ( false );
toggled = output < boolean >();
toggle () {
this . isOpen . update ( v => ! v );
this . toggled . emit ( this . isOpen ());
}
close () {
this . isOpen . set ( false );
}
}
// Usage
@ Component ({
template: `
<app-accordion>
<app-accordion-item>
<h3 accordion-header>Section 1</h3>
<div accordion-body>
<p>Content for section 1</p>
</div>
</app-accordion-item>
<app-accordion-item>
<h3 accordion-header>Section 2</h3>
<div accordion-body>
<p>Content for section 2</p>
</div>
</app-accordion-item>
</app-accordion>
`
})
export class FAQComponent {}
Conditional Content Projection
// empty-state.component.ts
@ Component ({
selector: 'app-list-with-empty' ,
standalone: true ,
template: `
@if (hasContent()) {
<ng-content />
} @else {
<div class="empty-state">
<ng-content select="[empty-state]" />
</div>
}
`
})
export class ListWithEmptyComponent {
hasContent = input ( true );
}
// Checking if content was provided
@ Component ({
selector: 'app-card' ,
standalone: true ,
template: `
<div class="card">
@if (hasHeader()) {
<header class="card-header">
<ng-content select="[card-header]" />
</header>
}
<main class="card-body">
<ng-content />
</main>
@if (hasFooter()) {
<footer class="card-footer">
<ng-content select="[card-footer]" />
</footer>
}
</div>
`
})
export class CardComponent {
@ ContentChild ( 'cardHeader' ) headerContent ?: ElementRef ;
@ ContentChild ( 'cardFooter' ) footerContent ?: ElementRef ;
hasHeader = computed (() => !! this . headerContent );
hasFooter = computed (() => !! this . footerContent );
}
ngProjectAs
ngProjectAs solves a subtle problem: what if you want to project a component or ng-container into a named slot, but its tag name or attributes do not match the slot’s select selector? ngProjectAs lets you tell Angular “treat this element as if it had the specified selector.” It is like giving a backstage pass to someone whose name is not on the guest list — you are vouching that they belong in that slot.
// Use ngProjectAs to match a different selector
@ Component ({
template: `
<app-modal>
<!-- This div will be projected as if it had [modal-header] attribute -->
<div ngProjectAs="[modal-header]">
<h2>Dynamic Title</h2>
<span class="badge">New</span>
</div>
<app-form-content ngProjectAs="[modal-body]" />
<ng-container ngProjectAs="[modal-footer]">
<button>Cancel</button>
<button class="primary">Save</button>
</ng-container>
</app-modal>
`
})
export class DynamicModalComponent {}
Context-Aware Projection
// list.component.ts
@ Component ({
selector: 'app-list' ,
standalone: true ,
template: `
<ul class="list">
@for (item of items(); track trackFn()(item); let i = $index) {
<li class="list-item">
<ng-container
[ngTemplateOutlet]="itemTemplate()"
[ngTemplateOutletContext]="{ $implicit: item, index: i }"
/>
</li>
}
</ul>
`
})
export class ListComponent < T > {
items = input . required < T []>();
itemTemplate = input . required < TemplateRef <{ $implicit : T ; index : number }>>();
trackFn = input <( item : T ) => any >(() => ( item : any ) => item );
}
// Usage
@ Component ({
template: `
<app-list
[items]="users()"
[itemTemplate]="userTemplate"
[trackFn]="trackById"
/>
<ng-template #userTemplate let-user let-i="index">
<div class="user-card">
<span class="index">{{ i + 1 }}</span>
<img [src]="user.avatar" [alt]="user.name" />
<div class="user-info">
<h4>{{ user.name }}</h4>
<p>{{ user.email }}</p>
</div>
</div>
</ng-template>
`
})
export class UserListComponent {
users = signal < User []>([]);
trackById = ( user : User ) => user . id ;
}
Advanced: Render Props Pattern
// data-fetcher.component.ts
@ Component ({
selector: 'app-data-fetcher' ,
standalone: true ,
template: `
@if (loading()) {
<ng-container
[ngTemplateOutlet]="loadingTemplate() || defaultLoading"
/>
} @else if (error()) {
<ng-container
[ngTemplateOutlet]="errorTemplate() || defaultError"
[ngTemplateOutletContext]="{ $implicit: error() }"
/>
} @else {
<ng-container
[ngTemplateOutlet]="dataTemplate()"
[ngTemplateOutletContext]="{ $implicit: data() }"
/>
}
<ng-template #defaultLoading>
<div class="loading">Loading...</div>
</ng-template>
<ng-template #defaultError let-err>
<div class="error">Error: {{ err.message }}</div>
</ng-template>
`
})
export class DataFetcherComponent < T > {
fetchFn = input . required <() => Observable < T >>();
dataTemplate = input . required < TemplateRef <{ $implicit : T }>>();
loadingTemplate = input < TemplateRef < void >>();
errorTemplate = input < TemplateRef <{ $implicit : Error }>>();
data = signal < T | null >( null );
loading = signal ( true );
error = signal < Error | null >( null );
constructor () {
effect (() => {
this . loading . set ( true );
this . error . set ( null );
this . fetchFn ()(). subscribe ({
next : ( data ) => {
this . data . set ( data );
this . loading . set ( false );
},
error : ( err ) => {
this . error . set ( err );
this . loading . set ( false );
}
});
});
}
}
// Usage
@ Component ({
template: `
<app-data-fetcher
[fetchFn]="fetchProducts"
[dataTemplate]="productsTemplate"
[loadingTemplate]="customLoading"
/>
<ng-template #customLoading>
<app-skeleton-loader [count]="6" />
</ng-template>
<ng-template #productsTemplate let-products>
<div class="products-grid">
@for (product of products; track product.id) {
<app-product-card [product]="product" />
}
</div>
</ng-template>
`
})
export class ProductsPageComponent {
private productService = inject ( ProductService );
fetchProducts = () => this . productService . getProducts ();
}
Best Practices
Name Slots Clearly Use descriptive attribute names for multi-slot projection
Provide Defaults Use fallback content when projection is optional
Document Slots Document expected content in component API
Type Templates Use generics for type-safe template contexts
Next: Dynamic Components Learn to create and manage dynamic components