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

Liu Ting Chun
Feb 21 · 4 min read

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.

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


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