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