In creating dynamic components in Angular 2, I found out that this process requires ViewContainerRef
in order to add newly created component to DOM.
And in passing @Input
and @Output
to those dynamically created components, I found the answer in the second link above and here.
However, if I were to create a service named shape.service
that contains functions returning different shape components with some @Input
like bgColor
, I don't know how this service will create a component without specifying DOM location, and how the container-component receives this returned component (probably its type will be ComponentRef
) from the service and injects it to the DOM container-component specifies.
For example, a service contains a method:
getCircle(bgColor:string): ComponentRef<Circle> {
let circleFactory = componentFactoryResolver.resolveComponentFactory(CircleComponent);
let circleCompRef = this.viewContainerRef.createComponent(circleFactory);
circleCompRef.instance.bgColor = bgColor;
return circleCompRef;
}
First question rises here, how do I make this.viewContainerRef
point to no where for the meantime? The reason why I'm importing ViewContainerRef
is to create component dynamically.
Second question is after container-component receives input-specificcomponentRef
from the service, how will it inject to its DOM?
UPDATE: I think my question above wasn't specific enough. I'm in a situation where:
@Input
,That means the service calling component has no idea where those componentRef will get injected. In short, I need independent component objects that can be injected to anywhere, anytime I want.
I'm reading the rumTimeCompiler solution several times already but I don't really get how that really works. It seems like there's too much work compared to component creation using viewContainerRef. I'll dig into that more if I find no other solution...
In case someone like me still looking for a simple and clear solution nowdays - here it is. I got it from @angular/cdk https://github.com/angular/components/tree/master/src/cdk and made a simple service.
import {
Injectable,
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
Injector,
EmbeddedViewRef
} from '@angular/core';
export type ComponentType<T> = new (...args: any[]) => T;
@Injectable({
providedIn: 'root'
})
export class MyService {
constructor(
private _appRef: ApplicationRef,
private _resolver: ComponentFactoryResolver,
private _injector: Injector
) { }
private _components: ComponentRef<any>[] = [];
add<T>(
component: ComponentType<T> | ComponentRef<T>,
element?: Element | string
): ComponentRef<T> {
const componentRef = component instanceof ComponentRef
? component
: this._resolver.resolveComponentFactory(component).create(this._injector);
this._appRef.attachView(componentRef.hostView);
if (typeof element === 'string') {
element = document.querySelector(element);
}
if (!element) {
element = document.body;
}
element.appendChild(
(componentRef.hostView as EmbeddedViewRef<any>).rootNodes[0] as HTMLElement
);
this._components.push(componentRef);
return componentRef;
}
remove(dialog: number | ComponentRef<any>): boolean {
let componentRef;
if (typeof dialog === 'number' && this._components.length > dialog) {
componentRef = this._components.splice(dialog, 1)[0];
}
else {
for (const cr of this._components) {
if (cr === dialog) {
componentRef = cr;
}
}
}
if (componentRef) {
this._remove(componentRef);
return true;
}
return false;
}
private _remove(componentRef: ComponentRef<any>) {
this._appRef.detachView(componentRef.hostView);
componentRef.destroy();
}
clear() {
while (this._components.length > 0) {
this._remove(this._components.pop());
}
}
getIndex(componentRef: ComponentRef<any>): number {
return this._components.indexOf(componentRef);
}
}
You can pass ComponentClass or ComponentRef to add
and Element or any querySelector string pointing to any DOM element where you want to attach your component as second argument (or nothing, then it assumes you want to attach to body).
const cr = this._service.add(MyComponent); // will create MyComponent and attach it to document.body or
const cr = this._service.add(MyComponent, this.someElement); // to attach to Element stored in this.someElement or
const cr = this._service.add(MyComponent, 'body div.my-class > div.my-other-div'); // to search for that element and attach to it
const crIndex = this._service.getIndex(cr);
cr.instance.myInputProperty = 42;
cr.instance.myOutputEmitter.subscribe(
() => {
// do something then for example remove this component
this._service.remove(cr);
}
);
this._service.remove(crIndex); // remove by index or
this._service.remove(cr); // remove by reference or
this._service.clear(); // remove all dynamically created components
P.S. Don't forget to add your dynamic components to entryComponents: []
of @NgModule
Maybe this plunker will help you: https://plnkr.co/edit/iTG7Ysjuv7oiDozuXwj6?p=preview
As far as i know, you will need the ViewContainerRef
inside of your service.
But the component calling your service can add it as an parameter, like this:
(just a the service.. see plunker for full working example)
import { Injectable, ViewContainerRef, ReflectiveInjector, ComponentFactoryResolver, ComponentRef } from '@angular/core';
import { HelloComponent, HelloModel } from './hello.component';
@Injectable()
export class DynamicCompService {
constructor (private componentFactoryResolver: ComponentFactoryResolver) { }
public createHelloComp (vCref: ViewContainerRef, modelInput: HelloModel): ComponentRef {
let factory = this.componentFactoryResolver.resolveComponentFactory(HelloComponent);
// vCref is needed cause of that injector..
let injector = ReflectiveInjector.fromResolvedProviders([], vCref.parentInjector);
// create component without adding it directly to the DOM
let comp = factory.create(injector);
// add inputs first !! otherwise component/template crashes ..
comp.instance.model = modelInput;
// all inputs set? add it to the DOM ..
vCref.insert(comp.hostView);
return comp;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With