Angular2 - Component in a dynamically created element

I am using google maps javascript api and I need to display Angular component in InfoWindow.

In my project, I download a google map api using the Jsonp service. How the google.maps.Map object is accessible. Later in the component, I create some markers and attach a click listener to them:

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 the info window containing the 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 with is some kind of 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 found out, is that the only viable way to create dynamic components is to use Angulars ViewContainerRef :

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


Is it possible to get a structure to handle the DOM in some way? As pointed out in many threads: "Angular does not process innerHTML or appendChild ." Is this a complete dead end?

Second: is it possible to use a Renderer implementation? (Not familiar with this), I saw this Canvas Renderer experiment, and theoretically, I think it will work with a Google map, since we can extrapolate that a map is just a special kind of canvas. Is it available in the latest version or has it changed? DomRenderer not in the documents, however it can be found in the sources.

+6
source share
1 answer

The main rule: dynamically create a component to get its factory.

1) Add a dynamic component to the entryComponents array, except for inclusion in declarations :

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

This is a tip for the angular compiler to create an ngfactory for a component, even if we are not using our component directly inside some template.

2) Now we need to introduce the ComponentFactoryResolver in our component / service, where we want to get ngfactory. You can think of ComponentFactoryResolver as storing factory components

app.component.ts

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

3) It's time to get the AppInfoWindowComponent factory:

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

4) Having a factory, we are free to use it as we want. Here are some examples:

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

  • ComponentFactory.create(injector, projectableNodes?, rootSelectorOrNode?) Just creates a component, and this component can be inserted into an element that corresponds to rootSelectorOrNode

Note that we can provide a node or selector in the third parameter of the ComponentFactory.create function. This can be useful in many cases. In this example, I just create a component and then paste it into some element.

onMarkerClick might look like this:

 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 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) Undoubtedly, a dynamically created component is not part of the change detection tree, so we also need to take care of detecting changes. This can be done using ApplicationRef.attachView(compRef.hostView) , as described in the example above, or we can do it using the ngDoCheck ( example ) component in which we create a dynamic component ( AppComponent in my case)

app.component.ts

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

This approach is better because it only updates the dynamic component when updating the current component. On the other hand, ApplicationRef.attachView(compRef.hostView) adds a change detector to the root of the change detector tree, and therefore it will be called with every change detection tick.

Plunger example


Tips:

Since addListener works outside the angular2 zone, we need to explicate our code inside the angular2 zone:

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

Source: https://habr.com/ru/post/1012822/


All Articles