Angular2 – Componente en elemento creado dinámicamente

Uso google maps javascript api y tengo que mostrar un componente angular en InfoWindow.

En mi proyecto, cargo la API de Google Jsonp con el servicio de Jsonp . Tengo el objeto google.maps.Map disponible. Más adelante, en un componente, creo algunos marcadores y les adjunto un oyente clic:

TypeScript :

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

Y luego, en el controlador de click , quiero abrir una ventana de información que contiene un componente 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(''); this.placeInfoWindow.open(this.map, marker); } 

Lo que terminé haciendo fue un poco de JS vainilla:

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

El problema, como aprendí, es que la única forma viable de crear componentes dynamics es usar Angulars ViewContainerRef :

  • Cómo colocar un componente dynamic en un contenedor

Pero no hay documentos ni ejemplos que describan cómo crear un ViewContainerRef partir de un elemento creado dinámicamente.


¿Es posible forzar el marco para procesar el DOM de alguna manera? Como está en muchos hilos, se afirma: “Angular no procesa innerHTML o appendChild “. ¿Es esto un callejón sin salida completo?

Segundo: ¿es posible usar una implementación Renderer ? (No estoy familiarizado con esto), vi este Canvas Renderer Experiment y teóricamente, creo que funcionaría también con el mapa de Google, ya que podemos extrapolar que el mapa es solo un tipo especial de canvas. ¿Todavía está disponible en el último lanzamiento o cambió? DomRenderer no está en los documentos, sin embargo, uno puede encontrarlo en las fonts.

La regla principal aquí es crear un componente dinámicamente que necesita para obtener su fábrica.

1) Agregue componente dynamic a la matriz entryComponents además de incluir en declarations :

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

Esa es una sugerencia para el comstackdor angular para producir ngfactory para el componente incluso si no usamos nuestro componente directamente dentro de alguna de las plantillas.

2) Ahora necesitamos inyectar ComponentFactoryResolver a nuestro componente / servicio donde queremos obtener ngfactory. Puedes pensar en ComponentFactoryResolver como un almacenamiento de fábricas de componentes

app.component.ts

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

3) Es hora de obtener fábrica de AppInfoWindowComponent :

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

4) Tener fábrica podemos usarlo libremente como queramos. Aquí hay algunos casos:

  • ViewContainerRef.createComponent(componentFactory,...) inserta el componente al lado de viewContainer.

  • ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?) Simplemente crea un componente y este componente se puede insertar en un elemento que coincida con rootSelectorOrNode

Tenga en cuenta que podemos proporcionar un nodo o selector en el tercer parámetro de la función ComponentFactory.create . Puede ser útil en muchos casos. En este ejemplo, simplemente crearé un componente y luego insertaré en algún elemento.

onMarkerClick método onMarkerClick podría verse así:

 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) El componente desafortunadamente creado dinámicamente no forma parte del árbol de detección de cambios, por lo tanto, también debemos tener cuidado con la detección de cambios. Se puede hacer utilizando ApplicationRef.attachView(compRef.hostView) como se ha escrito en el ejemplo anterior o podemos hacerlo explícitamente con ngDoCheck ( ejemplo ) del componente donde estamos creando un componente dynamic ( AppComponent en mi caso)

app.component.ts

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

Este enfoque es mejor porque solo actualizará el componente dynamic si se actualiza el componente actual. Por otro lado, ApplicationRef.attachView(compRef.hostView) agrega detector de cambio a la raíz del árbol detector de cambios y, por lo tanto, se ApplicationRef.attachView(compRef.hostView) en cada tic de detección de cambios.

Ejemplo de Plunker


Consejos:

Debido a que addListener está ejecutando fuera de la zona angular2, necesitamos ejecutar explícitamente nuestro código dentro de la zona angular2:

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