I have a similar requirement and used the ComponentFactoryResolver
. However, I have put a wrapper around the ng-template
like this:
@Component({
selector: 'tn-dynamic',
template: `<ng-template #container></ng-template>`,
providers: [SubscriptionManagerService],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DynamicComponent<T>
extends DynamicComponentBase<T, T, ComponentData>
implements OnChanges, OnDestroy {
@Input()
set componentData(data: ComponentData) {
if (!data) {
return;
}
try {
let type: Type<T> = getComponentType(data.type);
this._factory = this.resolver.resolveComponentFactory(type);
let injector = Injector.create([], this.vcRef.parentInjector);
this._factoryComponent = this.container.createComponent(this._factory, 0, injector);
this._currentComponent = this._factoryComponent.instance;
this._factoryComponent.location.nativeElement.classList.add('tn-dynamic-child');
} catch (er) {
console.error(`The type ${data.type.library}
.${data.type.key} has not been registered. Check entryComponents?`);
throw er;
}
}
// Handle ngOnChanges, ngOnDestroy
}
Then I would put my loop around my component <tn-dynamic [componentData]="myComponentData">
Your componentData contains the type of control, so you would have another service that would return the right type based on the requested type.
When you start using resolver as such, your inputs/outputs are not assigned. So you have to handle that yourself.
ngOnChanges(changes: SimpleChanges) {
let propertyWatch = this.getPropertyWatch();
for (let change in changes) {
if (change === propertyWatch) {
let data = this.getComponentData(changes[change].currentValue);
if (data) {
if (data.inputs) {
this.assignInputs(data.inputs);
}
if (data.outputs) {
this.assignOutputs(data.outputs);
}
if (this.implementsOnChanges()) {
let dynamiChanges = DynamicChanges.create(data);
if (dynamiChanges) {
(<OnChanges><any>this._currentComponent).ngOnChanges(dynamiChanges);
}
}
}
}
}
}
private unassignVariables() {
if (this.factory && this.factory.inputs) {
for (let d of this.factory.inputs) {
this._currentComponent[d.propName] = null;
}
}
}
protected assignInputs(inputs: ComponentInput) {
for (let key in inputs) {
if (inputs[key] !== undefined) {
this._currentComponent[key] = inputs[key];
}
}
}
private assignOutputs(outputs: ComponentOutput) {
for (let key in outputs) {
if (outputs[key] !== undefined) {
let eventEmitter: EventEmitter<any> = this._currentComponent[key];
let subscription = eventEmitter.subscribe(m => outputs[key](m));
this.sm.add(subscription);
}
}
}
Then, I found that it was better to handle my form inputs with formControl
rather than ngModel
. Especially when it comes to handle the validators. If you keep with ngModel
, you will not be able to add/remove validators easily. However, creating a dynamic component WITH a [formControl]
attached to the control generated with the ComponentFactoryResolver
seems to be impossible. So I had to compile template on the fly. So I create a control with another service that looks like this:
const COMPONENT_NAME = 'component';
@Injectable()
export class RuntimeComponent {
constructor(
private compiler: Compiler,
@Optional() @Inject(DEFAULT_IMPORTS_TOKEN)
protected defaultImports: DefaultImports
) {
}
protected createNewComponent(tmpl: string, args: any[]): Type<any> {
@Component({
selector: 'tn-runtime-component',
template: tmpl,
})
class CustomDynamicComponent<T> implements AfterViewInit, DynamicComponentData<T> {
@ViewChild(COMPONENT_NAME)
component: T;
constructor(
private cd: ChangeDetectorRef
) { }
ngAfterViewInit() {
this.cd.detectChanges();
}
}
Object.defineProperty(CustomDynamicComponent.prototype, 'args', {
get: function () {
return args;
}
});
// a component for this particular template
return CustomDynamicComponent;
}
protected createComponentModule(componentType: any) {
let imports = [
CommonModule,
FormsModule,
ReactiveFormsModule
];
if (this.defaultImports && this.defaultImports.imports) {
imports.push(...this.defaultImports.imports);
}
@NgModule({
imports: imports,
declarations: [
componentType
],
})
class RuntimeComponentModule {
}
// a module for just this Type
return RuntimeComponentModule;
}
public createComponentFactoryFromStringSync(template: string, attributeValues?: any[]) {
let type = this.createNewComponent(template, attributeValues);
let module = this.createComponentModule(type);
let mwcf = this.compiler.compileModuleAndAllComponentsSync(module);
return mwcf.componentFactories.find(m => m.componentType === type);
}
public createComponentFactoryFromMetadataSync(selector: string, attributes: { [attribute: string]: any }) {
let keys = Object.keys(attributes);
let attributeValues = Object.values(attributes);
let attributeString = keys.map((attribute, index) => {
let isValueAFunctionAsString = typeof attributeValues[index] === 'function' ? '($event)' : '';
return `${attribute}="args[${index}]${isValueAFunctionAsString}"`;
}).join(' ');
let template = `<${selector} #${COMPONENT_NAME} ${attributeString}></${selector}>`;
return this.createComponentFactoryFromStringSync(template, attributeValues);
}
}
My code is not perfect, and this is why I give you only the important parts. You have to play with the idea and make it work your way. I should write a blog post about this one day :)
I looked at your plunker and you are NOT using the #name properly when using a ngFor. You won't get it properly in TypeScript if your loop is working.
Also, you can't do *ngFor=""
on an ng-template. So your loop is just not working. See https://toddmotto.com/angular-ngfor-template-element
Good luck!