Will Shaver
Will Shaver

Reputation: 13099

Arbitrary location Component rendering in Angular 4 / Force ViewChild detection

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

Answers (1)

Will Shaver
Will Shaver

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

Related Questions