Angular — Using NgTemplateOutlet to communicate between parent and dynamic child components

Liu Ting Chun
Feb 16 · 4 min read

The parent with dynamic children pattern is a common pattern in Angular that you may use when working on some complicated features, such as third party library integration. 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. A common solution to this use case is to create a component-level scoped service to communicate between parent and children. Yet, I would like to introduce a more light-weighted alternative using NgTemplateOutlet.


What is NgTemplateOutlet?

NgTemplateOutliet is an Angular built-in directive that takes a template reference and render that template with a given context. In simple, you can imagine it as a rendering function. You pass your parameters (context) to the function, then it renders the content (template reference) based on your given context. Here is a simple example:

<ng-template #test let-word="word">
Hi, I received {{word}}
</ng-template>
<ng-container *ngTemplateOutlet="test; context: { word: 'Hello' }"></ng-container><br><ng-container *ngTemplateOutlet="test; context: { word: 'World' }"></ng-container>

And in your view:

Hi, I received Hello
Hi, I received World

Passing data through NgTemplateOutlet

The logic is not straight forward, but it isn’t as difficult as you may think. When using NgTemplateOutlet to pass data between parent and children, we wrap the children with <ng-template> with context parameters. This whole <ng-template> is then rendered in your parent component with NgTemplateOutlet, supplying the data needed to be passed to children via the context. Here I would like to use the NgTemplateOutlet approach to implement an integration of the Google map API as a more practical example. Here is what your implementation may look like with the <ng-template> wrapper:

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

Example with Google map API

First, we are going to implement the <google-map> component. The logic here is quite simple. We initialize the Google map API on component creation. After that, we render the map and expose our API objects to our children via NgTemplateOutlet.

import { 
Component,
ContentChild,
ElementRef,
Input,
TemplateRef,
ViewChild } from '@angular/core';
import GoogleMapsApiLoader from "google-maps-api-loader";
@Component({
selector: 'google-map',
template: `
<div #mapContainer style="height: 500px"></div>
<ng-container *ngIf="google && map">
<ng-container *ngTemplateOutlet="templateRef; context: {
google: google,
map: map
}"></ng-container>
</ng-container>
`
})
export class GoogleMapComponent {
@ViewChild('mapContainer') mapContainer: ElementRef;
@ContentChild(TemplateRef) templateRef: TemplateRef<any>;
@Input() key: string; 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}
});
})
}
}

Next, for the <google-map-marker> component, we are going to accept the API objects as properties. We check if it exists on component creation. If yes, we add the marker via these objects.

import { Component, Input } from '@angular/core';@Component({
selector: 'google-map-marker',
template: ''
})
export class GoogleMapMarkerComponent {
@Input() google: any;
@Input() map: any;
@Input() lat: number;
@Input() lng: number;
markerObj; ngOnInit() {
if (this.google) {
const Marker = this.google.maps.Marker;
this.markerObj = new Marker({
position: { lat: this.lat, lng: this.lng },
map: this.map
});
}
}
ngOnDestroy() {
if (this.markerObj)
this.markerObj.setMap(null);
}
}

In case you want to test it, you may use this piece of code:

import { Component } from '@angular/core';@Component({
template: `
<button (click)="generateMarker()">Generate Markers</button>
<google-map [key]="key">
<ng-template let-google="google" let-map="map">
<google-map-marker *ngFor="let marker of markers"
[google]="google"
[map]="map"
[lat]="marker.lat"
[lng]="marker.lng">
</google-map-marker>
</ng-template>
</google-map>
`
})
export class GoogleMapWithTemplateOutletPage {
key = "{{your_key}}";
markers = [];
generateMarker() {
let markers = [];
for(let i = 0; i < 3; i++) {
markers.push({
lat: this.random(-90, 90),
lng: this.random(-180, 180)
})
}
this.markers = markers;
}
random(from, to) {
return Math.random() * (to - from) + from;
}
}

Pros and cons comparing with scoped service

I would like to clarify that I don’t mean NgTemplateOutlet is always a better option than using a shared and scoped service. It has its edges, but also drawbacks. You should consider which approach to use based on your specific use case. Here is a comparison about two approaches:

  1. All logic are self-contained inside the components. You don’t need to maintain extra services, which means less code.
  2. It forces the data flow to be one directional, which is very good for code tracing and adding new features.
  3. You have full control on when to initialize your dynamic children.
  1. You always need to warp your children with <ng-template> to expose the data, which is not intuitive for other developers working together.
  2. No way you can nicely support bidirectional data flow with this approach. Hence, not an option in use cases that need bidirectional data flow.
  3. You cannot manage shared logic with this approach. Not an option again in use cases that your component has lots of shared codes.

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.

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