I've been a long Angular 1.x user and now I'm working on make a new app using Angular 4. I still don't grasp most of the concepts but I finally have something working really nice. However, I'm having an issue where I need to display an Angular 4 component (although in 1.x I just used directives) inside a Marker's popup using Leaflet.
Now, in Angular 1.x I could just use $compile against a template with the directive inside it (`<component>{{ text }}</component>`
) with buttons and such and it would work, but Angular 4 is totally different with its AoT thing and compiling at runtime seems to be really hard and there's no easy solution for it.
I asked a question here and the author says I could use a directive. I'm not sure if this is the correct approach or even how to mix my own code with his proposed solution... so I made a small npm-based project with Angular 4 and Leaflet already set up in case you know how to help me or want to give it a try (I greatly appreciate it!). I've been banging my head around this for maybe a week and I'm really tired of trying a lot of alternatives without success :(
Here's the link of my repo in GitHub: https://github.com/darkguy2008/leaflet-angular4-issue
The idea is to spawn PopupComponent (or anything similar to it) inside a Marker, code which you can find in src/app/services/map.service.ts, line 38.
Thanks in advance! :)
EDIT
I managed to solve it :) see the marked answer for details, or this diff. There are a few caveats and the procedure for Angular 4 and Leaflet is a bit different and it doesn't require as much changes: https://github.com/darkguy2008/leaflet-angular4-issue/commit/b5e3881ffc9889645f2ae7e65f4eed4d4db6779b
I've also made a Custom Compile Service out of this solution explained here and uploaded to the same GitHub repo. Thanks @yurzui! :)
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);
MycustomPopupComponent
.app.module.ts
in the entry components array. This is needed when creating components dynamically: entryComponents: [
...,
MycustomPopupComponent
],
constructor(
...
private componentFactoryResolver: ComponentFactoryResolver,
private injector: Injector
) {
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;
}
const marker = L.marker([latitude, longitude]).addTo(this.map);
marker.bindPopup(() => this.createCustomPopup()).openPopup();
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