Angular2 - 组成动态创建的元素

时间:2016-12-01 23:22:43

标签: google-maps angular

我使用谷歌地图javascript api,我必须在InfoWindow中显示一个Angular组件。

在我的项目中,我使用Jsonp服务加载了Google地图API。比我有google.maps.Map对象可用。稍后在一个组件中,我创建了一些标记,并在其上附加了一个点击监听器:

TypeScript

let marker = new google.maps.Marker(opts);
marker.setValues({placeId: item[0]});
marker.addListener('click', (ev: google.maps.MouseEvent) => this.onMarkerClick(marker, ev));

然后在click处理程序中,我想打开一个包含Angular组件的信息窗口:

TypeScript

private onMarkerClick(marker: google.maps.Marker, ev: google.maps.MouseEvent) {

    var div = document.createElement();
    this.placeInfoWindow.setContent(div);
    // Magic should happen here somehow
    // this.placeInfoWindow.setContent('<app-info-view-element></app-info-view-element>');
    this.placeInfoWindow.open(this.map, marker);
}

我最终做的是一些香草JS:

TypeScript

 private onMarkerClick(marker: google.maps.Marker, ev: google.maps.MouseEvent) {


    let div = document.createElement('div');
    div.className = 'map-info-window-container';
    div.style.height = '140px';
    div.style.width = '240px';

    this.placeInfoWindow.setContent(div);

    this.placeInfoWindow.open(this.map, marker);
    this.placesService.getPlace(marker.get('id')).subscribe(res => {
      this.decorateInfoWindow(div, res.name, marker);
    }, error => {
      this.decorateInfoWindow(div, ':( Failed to load details: ', marker);
    });

  }



private decorateInfoWindow(containerEl: HTMLElement, title?:string, marker?:google.maps.Marker) {
    let h3 = document.createElement('h3');
    h3.innerText = title;
    containerEl.appendChild(h3);

    let buttonBar = document.createElement('div');
    let editButton = document.createElement('button')
    editButton.innerText = "Edit";
    editButton.addEventListener('click', ev => {
      this.editPlace(marker);
    });
    buttonBar.appendChild(editButton);
    containerEl.appendChild(buttonBar);
  }

问题,正如我所知,创建动态组件的唯一可行方法是使用Angulars ViewContainerRef

但是没有文档或示例,描述了如何从动态创建的元素创建ViewContainerRef

强制框架以某种方式处理DOM是否可行?正如在很多线程中所述:“Angular不处理innerHTMLappendChild”。这完全是死路一条吗?

第二:是否可以使用Renderer实施? (不熟悉它),我看到了这个Canvas Renderer Experiment,理论上,我猜它也可以用于Google地图,因为我们可以推断地图只是一种特殊的画布。它在上一个版本中是否仍然可用或更改? DomRenderer不在文档中,但是可以在源代码中找到它。

1 个答案:

答案 0 :(得分:13)

这里的主要规则是动态创建组件,您需要将其出厂。

1)将动态组件添加到entryComponents数组,除了包括declarations

@NgModule({
  ...
  declarations: [ 
    AppInfoWindowComponent,
    ...
  ],
  entryComponents: [
    AppInfoWindowComponent,
    ...
  ],
})

即使我们不在某些模板中直接使用我们的组件,这也是角度编译器为组件生成ngfactory的提示。

2)现在我们需要将ComponentFactoryResolver注入到我们想要获得ngfactory的组件/服务中。您可以将ComponentFactoryResolver视为组件工厂的存储

<强> app.component.ts

import { ComponentFactoryResolver } from '@angular/core'
...
constructor(private resolver: ComponentFactoryResolver) {}

3)是时候让AppInfoWindowComponent工厂了:

const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
this.compRef = compFactory.create(this.injector);
4)拥有工厂,我们可以随心所欲地使用它。以下是一些案例:

  • ViewContainerRef.createComponent(componentFactory,...)在viewContainer旁边插入组件。

  • ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?)只需创建组件,此组件即可插入与rootSelectorOrNode匹配的元素

请注意,我们可以在ComponentFactory.create函数的第三个参数中提供节点或选择器。在许多情况下它可能会有所帮助。在这个例子中,我将简单地创建组件,然后插入到一些元素中。

onMarkerClick方法可能如下所示:

onMarkerClick(marker, e) {
  if(this.compRef) this.compRef.destroy();

  // creation component, AppInfoWindowComponent should be declared in entryComponents
  const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
  this.compRef = compFactory.create(this.injector);

  // example of parent-child communication
  this.compRef.instance.param = "test";
  const subscription = this.compRef.instance.onCounterIncremented.subscribe(x => { this.counter = x; });  

  let div = document.createElement('div');
  div.appendChild(this.compRef.location.nativeElement);

  this.placeInfoWindow.setContent(div);
  this.placeInfoWindow.open(this.map, marker);

  // 5) it's necessary for change detection within AppInfoWindowComponent
  // tips: consider ngDoCheck for better performance
  this.appRef.attachView(this.compRef.hostView);
  this.compRef.onDestroy(() => {
    this.appRef.detachView(this.compRef.hostView);
    subscription.unsubscribe();
  });
}

5)不幸的动态创建组件不是变更检测树的一部分,因此我们还需要注意变更检测。可以使用ApplicationRef.attachView(compRef.hostView)完成,如上面的示例所示,或者我们可以使用ngDoCheckexample)明确表达我们正在创建动态组件的组件(AppComponent {1}}在我的情况下)

<强> app.component.ts

ngDoCheck() {
  if(this.compRef) {
    this.compRef.changeDetectorRef.detectChanges()
  }
}

这种方法更好,因为它只会在更新当前组件时更新动态组件。另一方面,ApplicationRef.attachView(compRef.hostView)将变化检测器添加到变化检测器树的根,因此每次变化检测时都会调用它。

<强> Plunker Example

提示:

因为addListener在angular2区域之外运行,我们需要明确地在angular2区域内运行我们的代码:

marker.addListener('click', (e) => { 
  this.zone.run(() => this.onMarkerClick(marker, e));
});