Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 - Component into dynamically created element

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:

  • How to place a dynamic component in a container

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.

like image 586
Tiberiu C. Avatar asked Dec 01 '16 23:12

Tiberiu C.


Video Answer


1 Answers

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));
});
like image 168
yurzui Avatar answered Oct 06 '22 01:10

yurzui