How to spawn Angular 4 component inside a Leaflet marker's popup?
Alright, so thanks to @ghybs's suggestion I gave that link another try and managed to solve the issue :D. Leaflet is a bit different from Google Maps (it's also shorter) and the proposed solution there could be a bit smaller and easier to understand, so here's my version using Leaflet.
Basically, you need to put your popup component in the main app module's entryComponents
field. The key stuff is in m.onclick()
, there, we create a component, render it inside a div
and then we pass that div
's content to the leaflet popup container element. A bit tricky, but it works.
I got some time and converted this solution to a new $compile for Angular 4. Check the detailed info here. Thanks @yurzui! :)
This is the core code... The other stuff (css, webpack, etc.) is in the same repo as the OP, simplified into few files: https://github.com/darkguy2008/leaflet-angular4-issue but you just need this example to make it work:
import 'leaflet';
import './main.scss';
import "reflect-metadata";
import "zone.js/dist/zone";
import "zone.js/dist/long-stack-trace-zone";
import { BrowserModule } from "@angular/platform-browser";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
import { Component, NgModule, ComponentRef, Injector, ApplicationRef, ComponentFactoryResolver, Injectable, NgZone } from "@angular/core";
// ###########################################
// App component
// ###########################################
@Component({
selector: "app",
template: `<section class="app"><map></map></section>`
})
class AppComponent { }
// ###########################################
// Popup component
// ###########################################
@Component({
selector: "popup",
template: `<section class="popup">Popup Component! :D {{ param }}</section>`
})
class PopupComponent { }
// ###########################################
// Leaflet map service
// ###########################################
@Injectable()
class MapService {
map: any;
baseMaps: any;
markersLayer: any;
public injector: Injector;
public appRef: ApplicationRef;
public resolver: ComponentFactoryResolver;
public compRef: any;
public component: any;
counter: number;
init(selector) {
this.baseMaps = {
CartoDB: L.tileLayer("http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", {
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="http://cartodb.com/attributions">CartoDB</a>'
})
};
L.Icon.Default.imagePath = '.';
L.Icon.Default.mergeOptions({
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
this.map = L.map(selector);
this.baseMaps.CartoDB.addTo(this.map);
this.map.setView([51.505, -0.09], 13);
this.markersLayer = new L.FeatureGroup(null);
this.markersLayer.clearLayers();
this.markersLayer.addTo(this.map);
}
addMarker() {
var m = L.marker([51.510, -0.09]);
m.bindTooltip('Angular 4 marker (PopupComponent)');
m.bindPopup(null);
m.on('click', (e) => {
if (this.compRef) this.compRef.destroy();
const compFactory = this.resolver.resolveComponentFactory(this.component);
this.compRef = compFactory.create(this.injector);
this.compRef.instance.param = 0;
setInterval(() => this.compRef.instance.param++, 1000);
this.appRef.attachView(this.compRef.hostView);
this.compRef.onDestroy(() => {
this.appRef.detachView(this.compRef.hostView);
});
let div = document.createElement('div');
div.appendChild(this.compRef.location.nativeElement);
m.setPopupContent(div);
});
this.markersLayer.addLayer(m);
return m;
}
}
// ###########################################
// Map component. These imports must be made
// here, they can't be in a service as they
// seem to depend on being loaded inside a
// component.
// ###########################################
@Component({
selector: "map",
template: `<section class="map"><div id="map"></div></section>`,
})
class MapComponent {
marker: any;
compRef: ComponentRef<PopupComponent>;
constructor(
private mapService: MapService,
private injector: Injector,
private appRef: ApplicationRef,
private resolver: ComponentFactoryResolver
) { }
ngOnInit() {
this.mapService.init('map');
this.mapService.component = PopupComponent;
this.mapService.appRef = this.appRef;
this.mapService.compRef = this.compRef;
this.mapService.injector = this.injector;
this.mapService.resolver = this.resolver;
this.marker = this.mapService.addMarker();
}
}
// ###########################################
// Main module
// ###########################################
@NgModule({
imports: [
BrowserModule
],
providers: [
MapService
],
declarations: [
AppComponent,
MapComponent,
PopupComponent
],
entryComponents: [
PopupComponent
],
bootstrap: [AppComponent]
})
class AppModule { }
platformBrowserDynamic().bootstrapModule(AppModule);
- You will need to create a component for the popup content in case you don't have it already. Lets assume it is called
MycustomPopupComponent
. - Add your component to the
app.module.ts
in the entry components array. This is needed when creating components dynamically:
entryComponents: [
...,
MycustomPopupComponent
],
- In your screen, add these two dependencies to the constructor:
constructor(
...
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {
- Now in that screen we can define a function that creates the component dynamically.
private createCustomPopup() {
const factory = this.componentFactoryResolver.resolveComponentFactory(MycustomPopupComponent);
const component = factory.create(this.injector);
//Set the component inputs manually
component.instance.someinput1 = "example";
component.instance.someinput2 = "example";
//Subscribe to the components outputs manually (if any)
component.instance.someoutput.subscribe(() => console.log("output handler fired"));
//Manually invoke change detection, automatic wont work, but this is Ok if the component doesn't change
component.changeDetectorRef.detectChanges();
return component.location.nativeElement;
}
- Finally, when creating the Leaflet popup, pass that function as parameter to bindPopup. That function also accepts a second parameter with options.:
const marker = L.marker([latitude, longitude]).addTo(this.map);
marker.bindPopup(() => this.createCustomPopup()).openPopup();