Angular — Using ContentChildren and ngAfterContentChecked to implement component with dynamic children

Liu Ting Chun
Mar 8 · 4 min read

I have already written some articles about using NgTemplateOutlet or component-level service to implement component with dynamic child components, for example:

<google-map [key]="apiKey">
<google-map-marker *ngFor="let marker of markers"
[lat]="marker.lat"
[lng]="marker.lng">
</google-map-marker>
</google-map>

The <google-map> is the parent component, which initializes and renders the Google map. The <google-map-marker> is the child component that can be dynamically added to create markers on the map. If you don’t like both the NgTemplateOutlet or component-level service ways, here I have one more hacky alternative, which utilize @ContentChildren and ngAfterContentChecked.


What is ContentChildren?

@ContentChildren is a built-in Angular decorator for you to get you content children, which means your child components inside your <ng-content>. You can just imagine it as the vanilla getElementsByTagName, but works in a slightly different way.


What is ngAfterContentChecked?

ngAfterContentChecked is an Angular lifecycle hook that is called after the change detection has checked the content projected. Hence, if you put your view update logic here, it can be guaranteed that you are using the latest state of your children to update your view. Yet, this hook is actually very tricky. It refers to content checked but not content changed. It is triggered whenever there is a change detection, including the change detection triggered by your parent. When you put you update logic in this hook, you can easily lead to such a infinity loop: ngAfterContentChecked > update you view > some other functions trigger change detection during update > ngAfterContentChecked > … I will try to explain this more practically later in the implementation example.


Example with Google map API

I would like to use this approach to implement Google map API as a demo. The basic logic is to add a callback to the ngAfterContentChecked. Whenever it is triggered, we grab the latest @ContentChildren. After that, we update our parent component based on our children state. However, one problem here is that the method to add marker on the map also triggers the change detection, and hence the ngAfterContentChecked hook, which turns out an infinity loop. Thus, we need to check if there is actual change in the children before we update the view. If there is no change, then we don’t do any update.

import { 
Component,
ContentChildren,
ElementRef,
Input,
QueryList,
ViewChild } from '@angular/core';
import GoogleMapsApiLoader from "google-maps-api-loader";
@Component({
selector: 'google-map-marker',
template: ``
})
export class GoogleMapMarkerComponent {
@Input() lat: number;
@Input() lng: number;
}
@Component({
selector: 'google-map',
template: `
<div #mapContainer style="height: 500px"></div>
<div #content><ng-content></ng-content></div>
`
})
export class GoogleMapComponent {
@Input() key: string;
@ViewChild('mapContainer')
mapContainer: ElementRef;
@ViewChild("content")
contentWrapper: ElementRef;
content = null;
@ContentChildren(GoogleMapMarkerComponent)
markers: QueryList<GoogleMapMarkerComponent>;
markerObjs = [];

google;
map;
ngAfterViewInit() {
GoogleMapsApiLoader({
apiKey: this.key
}).then(googleMapApi => {
this.google = googleMapApi;
const mapContainer = this.mapContainer.nativeElement;
this.map = new this.google.maps.Map(mapContainer, {
zoom: 0,
center: {lat: 0, lng: 0}
});
this.contentChanged();
})
}
ngAfterContentChecked() {
if (this.contentWrapper) {
let current = this.contentWrapper.nativeElement.innerHTML;
if (this.content != current) {
this.content = current;
this.contentChanged();
}
}
}
contentChanged() {
this.updateMarker();
}
updateMarker() {
if (!this.google || !this.map)
return;
this.markerObjs.forEach(marker => {
marker.setMap(null);
});
const Marker = this.google.maps.Marker;
this.markerObjs = this.markers
.map(({ lat, lng }) => new Marker({
position: { lat, lng },
map: this.map
}));
}
}

The trick I used is to directly compare the HTML of the children to see if there is actual change. If there is change, all the markers will be re-rendered. This approach is convenient, but not the best as it always re-render everything. When working on some more complicated components, you should do a deep checking on all the states of your @ContentChildren to avoid unnecessary re-rendering.


Pros and Cons

This approach is completely different with NgTemplateOutlet or component-level service as all the logic are kept in the parent component. Here are my comparison:

  1. You don’t need to maintain extra services, which means less code.
  2. Unlike the NgTemplateOutlet approach, you don’t need to have any weird wrapper, like <ng-template>.
  1. Everything is in the parent component, no separation of concerns. Not suitable for complicated components.
  2. You have to work on the checking on state change yourself, which can be very cumbersome.
  3. Adding too much logic in ngAfterContentChecked can lead to potential performance impact.

Thanks for reading! Any comments would be highly appreciated. :D


Liu Ting Chun

Written by

Web developer from Hong Kong. Most interested in Angular and Vue. Currently working on a Nuxt.js + NestJS project.

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade