Angular — Using component injection to communicate between parent and dynamic child components

The parent with dynamic children pattern is a common pattern in Angular for developing interdependent components with advanced features. Here I would like to explain with an example using Google map:

<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. Here is a guide about how you can use component injection to implement this kind of components.

What is component injection?

This is a very useful feature powered by the Angular dependency injection engine. It allows developers to inject the reference of any ancestor in the constructor through the engine.

@Component(...)
export class DropDownComponent {
constructor(
parent: DropDownGroupComponent
) {
...
}
...
}

With the help of this feature, child components can access the methods and state of their parents dynamically.

Example with Google map API

Here is a simple implementation of Google map API as an example. This is the parent <google-map> component:

@Component({
selector: 'google-map',
template: `
<div #mapContainer style="height: 500px"></div>
<div #content><ng-content></ng-content></div>
`
})
export class GoogleMapComponent {
@ViewChild('mapContainer', { static: false })
mapContainer: ElementRef;
@ViewChild("content", { static: false })
contentWrapper: ElementRef;
@Input() key: string; _isReady = false;
_pending = [];
google;
map;
ngAfterViewInit() {
GoogleMapsApiLoader({
apiKey: this.key
}).then(googleMapApi => {
const mapContainer = this.mapContainer.nativeElement;
const map = new googleMapApi.maps.Map(mapContainer, {
zoom: 0,
center: {lat: 0, lng: 0}
});
this.onload(googleMapApi, map);
})
}
onload(google, map) {
this.google = google;
this.map = map;
this._isReady = true;
this._pending.forEach(resolve => { resolve() });
}
isReady(): Promise<any> {
return new Promise(resolve => {
if (this._isReady) {
resolve();
}
else {
this._pending.push(() => { resolve() });
}
})
}
}

This component initializes Google map API and also renders the map in the view. As the API is loaded asynchronously, the isReady method is added such that child components can rely on it to wait for the initialization process to complete before taking any action.

@Component({
selector: 'google-map-marker',
template: ''
})
export class GoogleMapMarkerComponent {
@Input() lat: number;
@Input() lng: number;
_markerObj; constructor(parent: GoogleMapComponent) {
parent.isReady().then(() => {
const google = parent.google;
const map = parent.map;
const Marker = google.maps.Marker;
this._markerObj = new Marker({
position: { lat: this.lat, lng: this.lng },
map: map
});
});
}
ngOnDestroy() {
if (this._markerObj)
this._markerObj.setMap(null);
}
}

The <google-map-marker> component is more straight forward. It simply injects the GoogleMapComponent when it is created and adds the marker to it.

Bonus: Use the “optional” annotation for more flexible use cases

Component injection will declare a mandatory dependency by default. It means the child component will no longer be able to use alone.

@Component(...)
export class DropDownComponent {
constructor(
parent: DropDownGroupComponent
) {
...
}
...
}
@Component({
selector: 'app-root',
template: `
<drop-down-component></drop-down-component>
`
})
export class RootComponent {
...
}

This will throw an injection exception as no DropDownGroupComponent ancestor can be found. In order to solve this, the optional annotation can be added to declare an optional dependency.

@Component(...)
export class DropDownComponent {
constructor(
@Optional() parent: DropDownGroupComponent
) {
...
}
...
}

Now there won’t be exception anymore and parent will be null when there is no DropDownGroupComponent ancestor.

Alternatives of using component injection

Component injection is the simplest built in approach for developing interdependent components. Yet, the major weakness of component injection is the strong coupling between the parent and children. It will be quite difficult to maintain when there is complicated logic. Here are two more approaches that work better for more complex components:

Thanks for reading! Hope you find this article helpful. Any comments would be highly appreciated. :D

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store