如何在Leaflet标记的弹出窗口中生成Angular 4组件?

12

我长期使用Angular 1.x,现在正在使用Angular 4制作一个新应用程序。虽然我仍不掌握大部分概念,但终于有了一些非常好的工作成果。然而,我遇到了一个问题:我需要在Leaflet中使用标记的弹出窗口内显示一个Angular 4组件(尽管在1.x中我只是使用指令)。

现在,在Angular 1.x中,我可以使用$compile针对包含指令的模板(`<component>{{ text }}</component>`)以及其中的按钮等内容进行编译,并且它会起作用。但是Angular 4通过其AoT功能进行编译,似乎在运行时进行编译非常困难,而且没有简单的解决方案。

我在这里提出了一个问题(链接),作者说我可以使用指令。我不确定这是否是正确的方法,甚至不知道如何将自己的代码与他提出的解决方案混合使用...因此,我制作了一个基于npm的小项目,其中已经设置了Angular 4和Leaflet的环境,以防您知道如何帮助我或尝试一下(非常感激!)。我已经头痛了一个星期,尝试了许多替代方案但没有成功:(

这是我在GitHub上的存储库链接:https://github.com/darkguy2008/leaflet-angular4-issue

想法是在标记内生成PopupComponent(或任何类似的组件),您可以在src/app/services/map.service.ts的第38行找到该代码。

提前感谢!:)

编辑

我设法解决了它 :) 请查看标记答案以获取详细信息,或查看此diff。有一些注意事项,Angular 4和Leaflet的过程有点不同,而且不需要进行太多更改:https://github.com/darkguy2008/leaflet-angular4-issue/commit/b5e3881ffc9889645f2ae7e65f4eed4d4db6779b

我还将这个解决方案制作成了一个自定义编译服务,详见这里,并上传到同一个GitHub存储库中。感谢@yurzui! :)


你尝试过 https://dev59.com/M1gR5IYBdhLWcg3wvPiM 吗? - ghybs
你好@ghybs,我几天前尝试过,但没有太大的成功。猜猜怎么着,我试图将解决方案添加到服务中。它似乎不适用于服务,而是适用于组件。我已经向GitHub存储库提交了解决方案...但我认为自己回答问题并不公平,因为您提供了关键信息链接。您想回答问题以便我可以标记它,还是我自己回答问题?谢谢! - DARKGuy
太好了,@yurzui,你做得很好。我本来可以回答自己的问题并获得一些声望点数,为下一个读者澄清问题,但现在我只能编辑主要问题。真是太棒了。请注意,Leaflet不等同于Google Maps。 - DARKGuy
1
@DARKGuy 对不起,就这样做吧。 - yurzui
哦哈哈,我以为这个更改是不可逆的,谢谢@yurzui!我会做的:D - DARKGuy
2个回答

16

好的,感谢@ghybs的建议,我又尝试了一次那个链接,并成功解决了问题:D。 Leaflet与Google Maps有点不同(也更短),提出的解决方案可能会更小,更易于理解,因此这是我使用Leaflet的版本。

基本上,您需要将弹出窗口组件放入主应用程序模块的entryComponents字段中。关键内容在m.onclick()中,我们在那里创建一个组件,在div内呈现它,然后将该div的内容传递给leaflet弹出窗口容器元素。有点棘手,但它可以工作。

我花了一些时间将此解决方案转换为Angular 4的新$compile。在此处查看详细信息感谢@yurzui!:)

这是核心代码...其他内容(css、webpack等)与OP相同,简化为几个文件:https://github.com/darkguy2008/leaflet-angular4-issue,但只需要这个示例即可使其正常工作:

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: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <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);

1
非常感谢您的帮助。使用Angular2/4与MapboxGL。Angular需要一个该死的编译函数。 - ohjeeez
1
为什么不创建一个单独的函数来加载组件,直接将HTMLElement返回到marker.bindPopup函数中呢?我有点困惑为什么要将bindPopup设置为null,并在.on('click')内部处理。你不能只做这样的事情吗:m.bindPopup(this.loadDynamicComponent()); - Douglas Tober
1
我同意@DouglasTober的观点,不将bindPopup设置为null会导致更稳定的功能(当我首先将其设置为null时,出现了一些奇怪的结果)。但除此之外,感谢您的答案!它真的帮了我很大的忙! - Sim_on
1
希望你们还在。我正在使用这个解决方案来生成一个弹出窗口。对于单个弹出窗口,它的效果还可以。但是如果我想要做多个弹出窗口怎么办?另外,有人能提供一下代码吗?如果代码不在服务中而是在组件本身中会是什么样子?或者它必须在服务中吗? - James Mak
我也在尝试做同样的事情,但是还要有多个弹出窗口@JamesMak..如果您不断更改组件,有人知道如何使其正常工作吗?我遇到了问题,即先前的弹出窗口数据出现,即使我单击具有不同元数据的不同点。 - fairlyMinty

3
  1. 如果您还没有弹出内容的组件,请创建一个组件。我们假设它叫做MycustomPopupComponent
  2. 将您的组件添加到app.module.ts文件中的入口组件数组中。这是在动态创建组件时所需的步骤:
   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;
}

最后,在创建Leaflet弹出框时,将该函数作为参数传递给bindPopup。该函数还接受第二个带有选项的参数。
const marker = L.marker([latitude, longitude]).addTo(this.map);
marker.bindPopup(() => this.createCustomPopup()).openPopup();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接