Angular2 – Componente no elemento criado dinamicamente

Eu uso o google maps javascript api e tenho que exibir um componente Angular na InfoWindow.

No meu projeto eu carrego a API do google map com o serviço Jsonp . Então eu tenho o object google.maps.Map disponível. Posteriormente, em um componente, crio alguns marcadores e atribuo a eles um ouvinte de clique:

TypeScript :

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

E, em seguida, no manipulador de click eu quero abrir uma janela de informações que contém um 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); } 

O que acabei fazendo foi um pouco de baunilha 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); } 

O problema, como aprendi, é que a única maneira viável de criar componentes dynamics é usar o Angular ViewContainerRef :

  • Como colocar um componente dynamic em um contêiner

Mas não há documentos ou exemplos, descrevendo como criar um ViewContainerRef de um elemento criado dinamicamente.


É possível forçar o framework a processar o DOM de alguma forma? Como é em muitos segmentos é afirmado: “Angular não processa innerHTML ou appendChild “. Isso é um beco sem saída completo?

Segundo: É possível usar uma implementação do Renderer ? (Não estou familiarizado com isso), eu vi este Canvas Renderer Experiment e teoricamente, eu acho que funcionaria também com o mapa do Google, uma vez que podemos extrapolar que o mapa é apenas um tipo especial de canvas. Ainda está disponível na última versão ou foi alterado? DomRenderer não está nos documentos, mas pode ser encontrado nas fonts.

A principal regra aqui é criar componentes dinamicamente para que você obtenha sua fábrica.

1) Adicionar componente dynamic ao array entryComponents além de include em declarations :

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

Isso é uma dica para o compilador angular produzir ngfactory para o componente mesmo se não usarmos nosso componente diretamente dentro de algum template.

2) Agora precisamos injetar o ComponentFactoryResolver em nosso componente / serviço onde queremos obter o ngfactory. Você pode pensar em ComponentFactoryResolver como um armazenamento de fábricas de componentes

app.component.ts

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

3) Chegou a hora de obter a fábrica AppInfoWindowComponent :

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

4) Tendo fábrica podemos livremente usá-lo como queremos. Aqui estão alguns casos:

  • ViewContainerRef.createComponent(componentFactory,...) insere o componente ao lado de viewContainer.

  • ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?) Apenas cria um componente e este componente pode ser inserido no elemento que combina rootSelectorOrNode

Note que podemos fornecer nó ou seletor no terceiro parâmetro da function ComponentFactory.create . Pode ser útil em muitos casos. Neste exemplo, vou simplesmente criar um componente e depois inserir em algum elemento.

onMarkerClick método onMarkerClick pode se parecer com:

 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) Infelizmente, o componente criado dinamicamente não faz parte da tree de detecção de alterações, portanto, também precisamos ter cuidado com a detecção de alterações. Isso pode ser feito usando ApplicationRef.attachView(compRef.hostView) como foi escrito no exemplo acima ou podemos explicitamente com ngDoCheck ( exemplo ) do componente onde estamos criando um componente dynamic ( AppComponent no meu caso)

app.component.ts

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

Essa abordagem é melhor porque só atualizará o componente dynamic se o componente atual for atualizado. Por outro lado, o ApplicationRef.attachView(compRef.hostView) adiciona o detector de alterações à raiz da tree do detector de alterações e, portanto, ele será chamado em cada escala de detecção de alterações.

Exemplo de Plunker


Dicas:

Como addListener está sendo executado fora da zona angular2, precisamos executar nosso código dentro da zona angular2:

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