Angular — Using component-level scoped service to communicate between parent and children

Service is one of the fundamental concepts of Angular for managing shared state and logic. Do you know service can be in component-level? It is very useful for implementing 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. Here is a guide about how you can use component-level service to implement this kind of components.

How to create a component-level service?

There isn’t actually much difference comparing with traditional global service. Just simply provide the injectable in your component rather than in your module:

import { Component } from '@angular/core';
import { GoogleMapService } from './google-map-service';
@Component({
selector: 'google-map',
providers: [ GoogleMapService ],
template: ''
})
export class GoogleMapComponent {
constructor(
private googleMapService: GoogleMapService
) {

}
}

When you do this, Angular will instantiate a new service instance for each of your <google-map> component. When you request for a GoogleMapService in any of your children inside your <google-map> component, Angular will look for the closest service, which means the component-level service in this case.

Example with Google map API

Here I would like to provide a simple implementation of Google map API as an example. This is the <google-map> component, which initialize Google map API and store it in the service.

import { 
Component,
ElementRef,
Input,
TemplateRef,
ViewChild } from '@angular/core';
import GoogleMapsApiLoader from "google-maps-api-loader";
import { GoogleMapService } from './google-map-service';
@Component({
selector: 'google-map',
providers: [ GoogleMapService ],
template: `
<div #mapContainer style="height: 500px"></div>
`
})
export class GoogleMapComponent {
@ViewChild('mapContainer') mapContainer: ElementRef;
@Input() key: string; constructor(
private googleMapService: GoogleMapService
) {}
ngAfterViewInit() {
GoogleMapsApiLoader({
apiKey: this.key
}).then(googleMapApi => {
const google = googleMapApi;
const mapContainer = this.mapContainer.nativeElement;
const map = new google.maps.Map(mapContainer, {
zoom: 0,
center: {lat: 0, lng: 0}
});
this.googleMapService.onload(map, google);
})
}
}

For the <google-map-marker> component, we simply inject the GoogleMapService from our parent component and create a marker on it.

import { Component, Input } from '@angular/core';
import { GoogleMapService } from './google-map-service';
@Component({
selector: 'google-map-marker',
template: ''
})
export class GoogleMapMarkerComponent {
@Input() lat: number;
@Input() lng: number;
_markerObj; constructor(
private googleMapService: GoogleMapService
) {
googleMapService.isReady().then(() => {
const google = googleMapService.google;
const map = googleMapService.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);
}
}

Here is the Google map service:

import { Injectable } from '@angular/core';@Injectable()
export class GoogleMapService {
_isReady = false;
_pending = [];
map;
google;
onload(map, google) {
this.map = map;
this.google = google;
this._isReady = true;
this._pending.forEach(resolve => { resolve() });
}
isReady(): Promise<any> {
return new Promise(resolve => {
if (this._isReady) {
resolve();
}
else {
this._pending.push(() => { resolve() });
}
})
}
}

When to use component-level service?

It is not a good practice to always use component-level service to build your components as it adds unnecessary complexity, which increases your maintenance overhead.

  1. It is needed only when you are making a dynamic component that depends on content projection or <ng-content>. If you are not making this kind of components, you probably can always make it simpler with basic property binding.
  2. It is needed only when you need to keep shared states or shared codes. Something is probably going wrong if this is not your use case.

Alternative of using component-level service

It is not the only approach you can use to work on dynamic components. I have written another article about using NgTemplateOutlet to communicate between parent and children.

Thanks for reading! 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