I use google maps javascript api and I have to display an Angular component into the InfoWindow.
In my project I load the google map api with the Jsonp
service. Than I have the google.maps.Map
object available. Later in a component I create some markers and attach to them a click listener :
TypeScript :
let marker = new google.maps.Marker(opts);
marker.setValues({placeId: item[0]});
marker.addListener('click', (ev: google.maps.MouseEvent) => this.onMarkerClick(marker, ev));
And then in the click
handler I want to open an Info Window that contains an Angular Component:
TypeScript :
private onMarkerClick(marker: google.maps.Marker, ev: google.maps.MouseEvent) {
var div = document.createElement();
this.placeInfoWindow.setContent(div);
// Magic should happen here somehow
// this.placeInfoWindow.setContent('<app-info-view-element></app-info-view-element>');
this.placeInfoWindow.open(this.map, marker);
}
What I ended up doing was some vanilla JS:
TypeScript :
private onMarkerClick(marker: google.maps.Marker, ev: google.maps.MouseEvent) {
let div = document.createElement('div');
div.className = 'map-info-window-container';
div.style.height = '140px';
div.style.width = '240px';
this.placeInfoWindow.setContent(div);
this.placeInfoWindow.open(this.map, marker);
this.placesService.getPlace(marker.get('id')).subscribe(res => {
this.decorateInfoWindow(div, res.name, marker);
}, error => {
this.decorateInfoWindow(div, ':( Failed to load details: ', marker);
});
}
private decorateInfoWindow(containerEl: HTMLElement, title?:string, marker?:google.maps.Marker) {
let h3 = document.createElement('h3');
h3.innerText = title;
containerEl.appendChild(h3);
let buttonBar = document.createElement('div');
let editButton = document.createElement('button')
editButton.innerText = "Edit";
editButton.addEventListener('click', ev => {
this.editPlace(marker);
});
buttonBar.appendChild(editButton);
containerEl.appendChild(buttonBar);
}
The problem, as I learned, Is that the only viable way to create dynamic components is to use Angulars ViewContainerRef
:
But there are no docs or examples, describing how to create a ViewContainerRef
from a dynamically created element.
Is it posible to force the framework to process the DOM in some way ? As it is in a lot of threads is stated : "Angular does not process innerHTML
or appendChild
". Is this a complete dead end ?
Second: Is it possible using a Renderer
implementation ? (Not Familiar with it), I saw this Canvas Renderer Experiment and theoretically, I guess it would work also with the Google map, since we can extrapolate that the map is just a special kind of canvas. Is it still available in the last release or it changed ? DomRenderer
is not in the docs, however one can find it in the sources.
The main rule here is to create component dynamically you need to get its factory.
1) Add dynamic component to entryComponents
array besides including into declarations
:
@NgModule({
...
declarations: [
AppInfoWindowComponent,
...
],
entryComponents: [
AppInfoWindowComponent,
...
],
})
That is a hint for angular compiler to produce ngfactory for the component even if we don't use our component directly within some of template.
2) Now we need to inject ComponentFactoryResolver
to our component/service where we want to get ngfactory. You can think about ComponentFactoryResolver
like a storage of component factories
app.component.ts
import { ComponentFactoryResolver } from '@angular/core'
...
constructor(private resolver: ComponentFactoryResolver) {}
3) It's time to get AppInfoWindowComponent
factory:
const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
this.compRef = compFactory.create(this.injector);
4) Having factory we can freely use it how we want. Here are some cases:
ViewContainerRef.createComponent(componentFactory,...)
inserts component next to viewContainer.
ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?)
just creates component and this component can be inserted into element that matches rootSelectorOrNode
Note that we can provide node or selector in the third parameter of ComponentFactory.create
function. It can be helpful in many cases. In this example i will simply create component and then insert into some element.
onMarkerClick
method might look like:
onMarkerClick(marker, e) {
if(this.compRef) this.compRef.destroy();
// creation component, AppInfoWindowComponent should be declared in entryComponents
const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
this.compRef = compFactory.create(this.injector);
// example of parent-child communication
this.compRef.instance.param = "test";
const subscription = this.compRef.instance.onCounterIncremented.subscribe(x => { this.counter = x; });
let div = document.createElement('div');
div.appendChild(this.compRef.location.nativeElement);
this.placeInfoWindow.setContent(div);
this.placeInfoWindow.open(this.map, marker);
// 5) it's necessary for change detection within AppInfoWindowComponent
// tips: consider ngDoCheck for better performance
this.appRef.attachView(this.compRef.hostView);
this.compRef.onDestroy(() => {
this.appRef.detachView(this.compRef.hostView);
subscription.unsubscribe();
});
}
5) Unfortunatelly dynamically created component is not part of change detection tree therefore we also need to take care about change detection. It can be done by using ApplicationRef.attachView(compRef.hostView)
as has been written in example above or we can do it explicity with ngDoCheck
(example) of component where we're creating dynamic component(AppComponent
in my case)
app.component.ts
ngDoCheck() {
if(this.compRef) {
this.compRef.changeDetectorRef.detectChanges()
}
}
This approach is better because it will only update dynamic component if current component is updated. On the other hand ApplicationRef.attachView(compRef.hostView)
adds change detector to the root of change detector tree and therefore it will be called on every change detection tick.
Plunker Example
Tips:
Because addListener
is running outside angular2 zone we need to explicity run our code inside angular2 zone:
marker.addListener('click', (e) => {
this.zone.run(() => this.onMarkerClick(marker, e));
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With