Conditional Content Projection in Angular
Introduction
In this article, I will discuss rendering certain elements inside a component based on those injected by its parent component. This has caused me significant trouble in my project due to the lack of real-world examples in the examples online, especially when using standalone components.
Use case
I have a shared component that I need to use on both my Ionic app and my web app. The issue is that I want to use two different types of icons for the back button. For Ionic, I will use `<ion-icon name="arrow-back-outline"></ion-icon>`, and for the web, `<mat-icon back>arrow_back</mat-icon>`. One more thing: sometimes I don’t want to display the back button at all, so there's no need to inject it.
You may ask: where is the problem? The issue is that the icon is optional, and if it is not defined, I don’t want to display the button at all, yet the button is defined inside the component itself.
Very Important to Note: If you’re using standalone components, do not forget to import the directive you are using to tag the content.
Page 1
<app-header>
<ng-container title>Page 1</ng-container>
</app-header>
Page 2
领英推荐
<app-header>
<ng-container title>Page 2</ng-container>
<ng-container back>
<ion-icon name="arrow-back-outline"></ion-icon>
</ng-container>
</app-header>
Implementation
What I need is a way to read the component injected from outside its scope. This step is tricky because you must follow Angular’s selection rules. You can either specify the class of the component if the element you want to inject is a component, or specify a directive with the same selector as the one used on your ng-content inside the component.
I tried to by pass on creating a directive just to check if the element exists, but no luck on my end.
Note: You can use either @ContentChild or @ContentChildren.
For @ContentChild and @ContentChildren to identify which components or elements to select, they must be either an Angular component (with a defined selector) or an element with an applied directive.
back-icon-directive
@Directive({
selector: '[back]',
})
export class BackIconDirective {}
app-header
@Component({
selector: 'app-header',
imports: [CommonModule, MatIconButton],
templateUrl: './authentication-title.component.html',
styleUrl: './authentication-title.component.css',
})
export class AuthenticationTitleComponent implements AfterContentInit {
@ContentChild(BackIconDirective, { read: ElementRef })
projectedContent?: ElementRef;
// You can also do this if it fits your case
// @ContentChildren(BackIconDirective, { read: ElementRef })
// projectedContent!: QueryList<ElementRef>;
backIconVisible = signal(false);
ngAfterContentInit(): void {
this.backIconVisible.set(!!this.projectedContent);
}
}
<div>
@if(backIconVisible()) {
<button mat-icon-button>
<ng-content select="[back]"></ng-content>
</button>
}
<span>
<ng-content />
</span>
</div>