Angular2 – Component into dynamically created element

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

Leave a Comment