Content projection, a powerful technique in Angular, allows developers to create highly reusable and customizable components. This approach enhances component reusability, flexibility, leading to cleaner, and more maintainable Angular applications.
In this article, we will delve into what content projection is, types, how to use it, with below real-world scenario for multi-slot content projection.
What is Content Projection
Content projection is a pattern enables us to insert or project content from a parent component into a child(s) component. This approach makes it possible to design flexible components that can accommodate a wide range of content, making them more reusable.
Content Projection types
Single-Slot Content Projection Single-slot content projection uses a single <ng-content>
directive, allowing the insertion of content in a specific place.
Multi-Slot Content Projection Multi-slot content projection allows projecting different sections of content into different parts of the template. Multi-slot content projection uses the select attribute (<ng-content select="[card-subtitle]"></ng-content>
) to specify which content need to go where.
Few key concepts to be aware
<ng-content>
in a component template defines a placeholder area(s). When a parent component uses this component, it pass content between the tags, customizing the child component's behavior.
<ng-container>
is an directive that allows to group elements in a template that doesn’t interfere with styles or layout and not rendered into Angular DOM.
<ng-template>
is a template element that Angular uses with structural directives. These template elements work in the presence of structural directives and help us to define a template that doesn’t render anything by itself, but conditionally renders them to the DOM.
Example of Single-Slot Content Projection
Step 1: Create child/reusable component
@Component({
selector: 'app-single-slot-projection',
template: `<mat-toolbar>
<ng-content> </ng-content>
</mat-toolbar>`,
styles: ``
})
export class SingleSlotProjectionComponent {
}
Step 2: Use Single-slot component in a Parent Component
All the content inside <app-single-slot-projection>
will be projected at <ng-content>
place of child component.
@Component({
selector: 'app-root',
template: `
<app-single-slot-projection>
<span>Content Projection Example</span>
</app-single-slot-projection>
`,
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'angular-multi-select-picker';
}
Example of Multi-Slot Content Projection
Let's look at real time application scenario. Consider you are working on dynamic reports generation application which allows user to select required fields in the report. To select required fields in report, we have a screen like above where all available column names will appear in left box, selected column names will appear in the right box. Actions will appear in the middle. You can select required columns from left menu and move it to right side. You will have an option to move all towards right and left.
As part of core team we decide to define standard sections and consuming application can decide what has to go inside of those sections.
Making it more reusable component with all conditional projections will be published soon in another post.
Step 1: Create child/reusable component
As part of this example i am considering 5 places for content project.
<mat-card appearance="outlined">
<mat-card-header class="mb-2">
<mat-card-title>
<ng-content select="[card-title]"></ng-content>
</mat-card-title>
<mat-card-subtitle class="mt-2">
<ng-content select="[card-subtitle]"></ng-content>
</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div class="list_builder">
<ng-content select="[source-items]"></ng-content>
<ng-content select="[action-items]"> </ng-content>
<ng-content select="[target-items]"></ng-content>
</div>
</mat-card-content>
</mat-card>
Step 2: Use Multi-slot component in a Parent Component
card-title: displays some static content.
card-subtitle: displays static content wit number of columns available in right side table.
source-items: displays all the available columns available for our requirement.
action-items: displays all actions that can be performed on left and right side tables.
target-items: displays all the selected columns to present in out report.
<main class="main">
<div class="content">
<!-- Single Slot Projection -->
<app-single-slot-projection>
<span>Content Projection Example</span>
</app-single-slot-projection>
<!-- Multi Slot Projection -->
<app-multi-select-picker>
<!-- card-title section -->
<ng-container card-title>Multi Select Picker</ng-container>
<!-- card-subtitle section -->
<ng-container card-subtitle>Selected Items Count: {{targetItems.length}}</ng-container>
<!-- source-items section -->
<ng-container source-items>
<mat-card class="source_items">
<mat-card-content>
<mat-list role="list">
@for (data of sourceItems; track $index) {
<mat-list-item role="listitem" class="selectable_item" [ngClass]="{selected_item: data.selected}"
(click)="itemSelected(data)">
{{data.value}}
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
</ng-container>
<!-- action-items section -->
<ng-container action-items>
<mat-card class="action_items">
<mat-card-content class="action_buttons_container">
<button mat-icon-button class="action_buttons" (click)="moveAll('right')">
<mat-icon aria-hidden="false" aria-label="Move All Right"
fontIcon="keyboard_double_arrow_right"></mat-icon>
</button>
<button mat-icon-button class="action_buttons" (click)="moveSelected('right')">
<mat-icon aria-hidden="false" aria-label="Move Right" fontIcon="chevron_right"></mat-icon>
</button>
<button mat-icon-button class="action_buttons" (click)="moveSelected('left')">
<mat-icon aria-hidden="false" aria-label="Move Left" fontIcon="chevron_left"></mat-icon>
</button>
<button mat-icon-button class="action_buttons" (click)="moveAll('left')">
<mat-icon aria-hidden="false" aria-label="Move All Left" fontIcon="keyboard_double_arrow_left"></mat-icon>
</button>
</mat-card-content>
</mat-card>
</ng-container>
<!-- target-items section -->
<ng-container target-items>
<mat-card class="target_items">
<mat-card-content>
<mat-list role="list">
@for (data of targetItems; track $index) {
<mat-list-item role="listitem" class="selectable_item" [ngClass]="{selected_item: data.selected}"
(click)="itemSelected(data)">
{{data.value}}
</mat-list-item>
}
</mat-list>
</mat-card-content>
</mat-card>
</ng-container>
</app-multi-select-picker>
</div>
</main>
Best Practices
Use consistent naming conventions for select attributes.
ngProjectAs
supports only static values and not the dynamic expressions.We need to avoid overcomplicating component with too many slots.
You can find code at GitHub
Please drop a comment if you have any question.