Angular2 - Component into dynamically created element
Solution 1:
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 matchesrootSelectorOrNode
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));
});