When working in Angular, it’s pretty straightforward to create components and nest those components inside of other components. However, it is a bit more complicated to design customizable components that will display content based on the components passed to them (content projection).
For example, say I wanted to draw a smiley face. The simplest way would be to create a face component and then create components for each part of the face: eyes, nose, mouth. Then, I could put the eyes, nose, and mouth components in the face component and end up with a complete face.
The HTML for the face component would look something like this:
<div class="face">
<h1> My Face </h1>
<app-normal-eyes class="face-eyes"></app-normal-eyes>
<app-normal-nose class="face-nose"></app-normal-nose>
<app-smiley-mouth class="face-mouth"></app-smiley-mouth>
</div>
And, if the face component had selector app-face, I could display the face component by simply saying <app-face></app-face>
in a parent component.
The face might look something like this:
Content Projection
Let’s say I wanted to make a bunch of faces, and I wanted them all to use a different combination of eyes, noses, and mouths. I would start building a bunch of face part components:
I want to be able to pass these custom components to my face component, and to have my face component display them as-is. This is where content projection comes in.
To use content projection, I’ll start by passing the custom components as content to the face component.
<app-face>
<app-normal-eyes class="face-eyes"></app-normal-eyes>
<app-normal-nose class="face-nose"></app-normal-nose>
<app-smiley-mouth class="face-mouth"></app-smiley-mouth>
</app-face>
Then, in the face component, I’ll project the custom components by using <ng-content></ng-content>
:
<div class="face">
<h1> MY FACE </h1>
<ng-content></ng-content>
</div>
Any custom components passed in to the face component will now be displayed.
Multi-Content Projection
What if I pass in my custom components out of order, like this?
<app-face>
<app-smiley-mouth class="face-mouth"></app-smiley-mouth>
<app-normal-nose class="face-nose"></app-normal-nose>
<app-normal-eyes appEyeClick class="face-eyes"></app-normal-eyes>
</app-face>
The face component will be displayed like this:
Instead of trusting the developer to always enter the components in the right order, the face component should specify which custom component goes where.
Ng-content has a select attribute, which can select components by class name, CSS, tag, etc. It is possible to have multiple ng-content tags, each projecting an element with a matching class name. Architecting my face component with multiple content tags would allow me to pass the components in any order and have them displayed in order by class name.
The code looks like this:
<div class="face">
<h1> MY FACE </h1>
<ng-content select=".face-eyes"></ng-content>
<ng-content select=".face-nose"></ng-content>
<ng-content select=".face-mouth"></ng-content>
</div>
And, even though the components were passed in out of order, the eyes still show up on the top of the face. Good!
Responding to Events from Content Projected inside of Ng-Content
What if I wanted my face component to know whenever the eyes, nose, or mouth were clicked? For example, I might want to make counters that increment each time a component is clicked.
An event emitter on the projected content would not solve this problem. If, say, the eye component had an event emitter, the output event handler (specifying which method to call when an event is emitted) would have to be specified not in the app-face component, but in the parent of the app-face component. Because the components being projected in ng-content are being created in the parent, the event emitter handler must also be hooked up in the parent. So, the face component still wouldn’t have a way to tell if a projected component had been clicked.
In order to get events from the projected content directly in the face component, I can listen for click events with HostListener. However, I want to be able to differentiate between a click to the eye component and a click to the mouth component, so I cannot simply listen for all click events.
To solve this, I created a click listener directive for each of the components (the eyes, nose, and mouth). The directive listens for clicks and emits an event on click:
@Directive({
selector: '[appEyeClick]'
})
export class EyeClickDirective {
constructor() { }
@Output() clickEyes: EventEmitter = new EventEmitter();
@HostListener('click', ['$event']) onClick($event) {
this.clickEyes.emit('eyeClick');
}
}
For the eye clicks, the face component uses the @ContentChild decorator to get the eye click listener directive from ng-content. It subscribes to the event emitter and increments the ‘eyeClick’ count by one each time it gets a click event.
export class FaceComponent implements OnInit {
eyeClicks = 0;
@ContentChild(EyeClickDirective)
eyeClick: EyeClickDirective;
constructor() { }
ngOnInit() {
this.eyeClick.clickEyes.asObservable().pipe(tap(() => this.eyeClicks++)).subscribe();
}
}
If there were a similar directive and subscription for each component on the face, it would be possible to get the total number of clicks for the eyes, nose, and mouth.
So, while responding to the events of projected components is possible, overall, it might have been simpler to store the clicked count in a service or centralized state.