Angular2 - 分成动态创建的元素 [英] Angular2 - Component into dynamically created element

查看:112
本文介绍了Angular2 - 分成动态创建的元素的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用google map javascript api,并且必须在InfoWindow中显示一个Angular组件。

在我的项目中,我使用<$ c加载google map api $ c> Jsonp 服务。比我有 google.maps.Map 对象可用。稍后在一个组件中,我创建了一些标记并附加给他们一个点击监听器:
$ b TypeScript

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

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

TypeScript

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

var div = document.createElement();
this.placeInfoWindow.setContent(div);
//魔术应该以某种方式发生
// this.placeInfoWindow.setContent('< app-info-view-element>< / app-info-view-element>');
this.placeInfoWindow.open(this.map,marker);
}

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


$ b 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);
},错误=> {
this.decorateInfoWindow(div,':(无法加载细节:',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



$ ul
  • 如何在容器中放置一个动态组件



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




    强制框架以某种方式处理DOM是否可行?正如很多线程所述:Angular不处理 innerHTML appendChild 。这是一个完全死胡同吗?第二:是否可以使用 Renderer 实现? (不熟悉它),我看到这个画布渲染实验,理论上,我猜这也适用于Google地图,因为我们可以推断地图只是一种特殊的画布。它在上一个版本中仍然可用,或者它已更改? DomRenderer 不在文档中,但是可以在源代码中找到它。

    解决方案

    这里的主要规则是动态创建组件,您需要获得它的工厂。
    $ b 1)将动态组件添加到 entryComponents 数组,除了包含到声明中

      @NgModule({
    ...
    声明:[
    AppInfoWindowComponent,
    .. 。
    ],
    entryComponents:[
    AppInfoWindowComponent,
    ...
    ],
    })

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


    $ b $ 2)现在我们需要向我们想要获取ngfactory的组件/服务注入 ComponentFactoryResolver 。您可以考虑 ComponentFactoryResolver 类似于组件工厂的存储



    app.component.ts
    $ b

    从'@ angular / core'导入{ComponentFactoryResolver}
    ...
    构造函数(private resolver:ComponentFactoryResolver){}



    <3>现在是时候获得 AppInfoWindowComponent factory:
    $ b

      const compFactory = this。 resolver.resolveComponentFactory(AppInfoWindowComponent); 
    this.compRef = compFactory.create(this.injector);



    <4>有了工厂,我们可以随心所欲地使用它。以下是一些情况:


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


    • ComponentFactory.create(injector,projectableNodes?,rootSelectorOrNode?)只是创建组件,并且可以将此组件插入匹配 rootSelectorOrNode



    $的元素b $ b

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

    onMarkerClick 方法可能如下所示:

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

    //创建组件,AppInfoWindowComponent应该在entryComponents中声明$ b $ const const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
    this.compRef = compFactory.create(this.injector);

    //父母 - 子女沟通的例子
    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)在AppInfoWindowComponent
    中有必要进行变化检测//提示:考虑ngDoCheck以获得更好的性能
    this.appRef.attachView(this.compRef.hostView);
    this.compRef.onDestroy(()=> {
    this.appRef.detachView(this.compRef.hostView);
    subscription.unsubscribe();
    });





    5)不幸动态创建的组件不是变化检测树的一部分,因此我们也需要关心变化检测。它可以通过使用 ApplicationRef.attachView(compRef.hostView)来完成,正如上面的例子中所写,或者我们可以用 ngDoCheck 示例),我们正在创建动态组件( AppComponent 在我的情况下)



    app.component.ts

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

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



    Plunker示例



    提示:



    因为 addListener 在angular2区域外运行,所以我们需要在angular2区域内运行我们的代码:

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


    I use google maps javascript api and I have to display an Angular component into the InfoWindow.

    In my project I load the google map api with the Jsonp service. Than I have the google.maps.Map object available. Later in a component I create some markers and attach to them a click listener :

    TypeScript :

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

    And then in the click handler I want to open an Info Window that contains an Angular Component:

    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);
    }
    

    What I ended up doing was some vanilla 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);
      }
    


    The problem, as I learned, Is that the only viable way to create dynamic components is to use Angulars ViewContainerRef:

    But there are no docs or examples, describing how to create a ViewContainerRef from a dynamically created element.


    Is it posible to force the framework to process the DOM in some way ? As it is in a lot of threads is stated : "Angular does not process innerHTML or appendChild". Is this a complete dead end ?

    Second: Is it possible using a Renderer implementation ? (Not Familiar with it), I saw this Canvas Renderer Experiment and theoretically, I guess it would work also with the Google map, since we can extrapolate that the map is just a special kind of canvas. Is it still available in the last release or it changed ? DomRenderer is not in the docs, however one can find it in the sources.

    解决方案

    The main rule here is to create component dynamically you need to get its factory.

    1) Add dynamic component to entryComponents array besides including into declarations:

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

    That is a hint for angular compiler to produce ngfactory for the component even if we don't use our component directly within some of template.

    2) Now we need to inject ComponentFactoryResolver to our component/service where we want to get ngfactory. You can think about ComponentFactoryResolver like a storage of component factories

    app.component.ts

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

    3) It's time to get AppInfoWindowComponent factory:

    const compFactory = this.resolver.resolveComponentFactory(AppInfoWindowComponent);
    this.compRef = compFactory.create(this.injector);
    

    4) Having factory we can freely use it how we want. Here are some cases:

    • ViewContainerRef.createComponent(componentFactory,...) inserts component next to viewContainer.

    • ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?) just creates component and this component can be inserted into element that matches rootSelectorOrNode

    Note that we can provide node or selector in the third parameter of ComponentFactory.create function. It can be helpful in many cases. In this example i will simply create component and then insert into some element.

    onMarkerClick method might look like:

    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) Unfortunatelly dynamically created component is not part of change detection tree therefore we also need to take care about change detection. It can be done by using ApplicationRef.attachView(compRef.hostView) as has been written in example above or we can do it explicity with ngDoCheck(example) of component where we're creating dynamic component(AppComponent in my case)

    app.component.ts

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

    This approach is better because it will only update dynamic component if current component is updated. On the other hand ApplicationRef.attachView(compRef.hostView) adds change detector to the root of change detector tree and therefore it will be called on every change detection tick.

    Plunker Example


    Tips:

    Because addListener is running outside angular2 zone we need to explicity run our code inside angular2 zone:

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

    这篇关于Angular2 - 分成动态创建的元素的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆