Reputation: 13099
My Angular 4 application uses a 3rd party Library (mapbox) to render content on a page. That library has a popup dialog feature that allows for popups to be displayed when a user clicks a pin on the map: https://www.mapbox.com/mapbox-gl-js/example/popup-on-click/
The popup is called with something like the following code:
map.on('click', 'places', function (e) {
new mapboxgl.Popup()
.setLngLat(e.features[0].geometry.coordinates)
.setHTML('<div>this is a popup</div>')
.addTo(map);
});
I want to render an Angular 4+ component into the popup div. Things I've tried:
1) Binding a @ViewChild to that popup's div.
// in the component that holds the map component
@ViewChild("pop", { read: ViewContainerRef }) childContainer: ViewContainerRef;
//when the user clicks the popup
popup.setHTML("<div #pop></div>")
The childContainer
is never assigned. I've tried waiting seconds after the popup is open, or calling detectChanges()
on my component's ChangeDetectorRef
. I've tried opening the popup in an NgZone.runOutsideAngular()
and then running detectChanges()
manually.
If I manually add the div to my html template, the popup renders as expected to that div's location.
2) Rendering it using a factory to a div with that ID
//when the user clicks the popup
popup.setHTML("<div id="pop">HERE</div>")
const element = this.elRef.nativeElement.querySelector("#pop"); //this is found
const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
const component = factory.create(this.viewContainerRef.injector, null, element);
The contents of the div HERE are replaced with <!---->
. Clearly Angular is rendering something to that location, but not the actual component. Do I need a different injector?
3) Placing the component render element directly into the popup:
popup.setHTML("<popup-component></popup-component>")
It never turns itself into an actual component. Even with things like detectChanges()
or NgZone
runs.
4) Any other ideas? How do I render an Angular component to an arbitrary html location defined outside of Angular?
Upvotes: 2
Views: 518
Reputation: 13099
I was able to solve this using a standard .append
call. I don't love this solution, but it does appear to work and not have too many downsides, so I'm posting it here in case other folks find this question.
Add a hidden component to the same page as your map:
//map.component.html
<div id="map"></div>
<div style="display:none">
<info-popup #infopop [id]="selectedId" [renderTo]="popupLocation"></info-popup>
</div>
Bind the clicked popup by ID to a class property passed to the info-pop component above
//map.component.ts
popupLocation: any;
selectedId: any;
initializeMap(): void {
this.map.on("click", "pinLayer", (e: MapMouseFeaturesEvent) => {
this.selectedId = e.features[0].properties["id"]; // assumes a distinct id per pin
new mapboxgl.Popup({ closeButton: false })
.setLngLat(e.lngLat)
.setHTML("<div id='popupLocation'></div>")
.addTo(this.map);
this.popupLocation = this.elementRef.nativeElement.querySelector("#popupLocation);
});
}
Use that component with append:
//infopop.component.ts
export class InfoPopupComponent implements OnChanges {
@Input() id: string;
@Input() renderTo: any;
constructor(private elementRef: ElementRef) { }
ngOnChanges(changes: SimpleChanges): void {
if ("id" in changes) {
// load popup specific details and render
}
if (this.renderTo) {
this.renderTo.append(this.elementRef.nativeElement);
}
}
}
This worked with bound [click] events on the infopop.component.html passing through correctly.
Upvotes: 3