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.
- 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. - 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