I am using angular 7 appplication and i am writing a generic dynamic form creator. I am using reactive forms and ng-template and <ng-container *ngTemplateOutlet>
for recursively displaying the form element.The code of the project could be seen Here.
But i am facing the following error
ERROR Error: Cannot find control with name: 'data'
at _throwError (forms.js:1775)
at setUpControl (forms.js:1683)
at FormGroupDirective.push../node_modules/@angular/forms/fesm5/forms.js.FormGroupDirective.addControl (forms.js:4532)
at FormControlName.push../node_modules/@angular/forms/fesm5/forms.js.FormControlName._setUpControl (forms.js:5030)
at FormControlName.push../node_modules/@angular/forms/fesm5/forms.js.FormControlName.ngOnChanges (forms.js:4980)
at checkAndUpdateDirectiveInline (core.js:9239)
at checkAndUpdateNodeInline (core.js:10507)
at checkAndUpdateNode (core.js:10469)
at debugCheckAndUpdateNode (core.js:11102)
at debugCheckDirectivesFn (core.js:11062)
But if i see my formGroup object i can see a control with data as the name of the control as shown below.
What is the mistake i am doing ? Please help.
Reproduction of the problem is Here stackblitz
You're right there is a problem with ng-template here.
FormControlName
directive heavily relies on hierarhy of elements above current element to determine FormControl
.
Since you're using ngTemplateOutlet
this hierarhy mixed up and Angular can't find parent FormGroup
or FormArray
controls.
You can refactor your example to use nested components and it should work if you will keep hierarhy of child AbstractControls
.
On the other hand, you can make it work with ngTemplateOutlet
if you will use FormControl directive.
You need to only make sure you provided correct control to your input
element.
Here's how it could be done:
<form [formGroup]="form">
<ng-container *ngTemplateOutlet="controlList; context: {controls: schema, path: []}"></ng-container>
<ng-template #controlList let-controls="controls" let-path="path" let-isArray="isArray">
<ng-container *ngFor="let control of controls; let i = index;">
<ng-container *ngIf="control?.type === 'simple'">
<div class="control">
<label>
{{ control.label }}
<input type="text" [formControl]="form.get(isArray ? path.concat(i, control.name) : path.concat(control.name))">
</label>
</div>
</ng-container>
<ng-container *ngIf="['object', 'array'].indexOf(control.type) > -1">
<fieldset>
<legend>{{control.name}}</legend>
<ng-container
*ngTemplateOutlet="controlList; context: {controls: control.items, isArray: control.type === 'array', path: isArray ? path.concat(i, control.name) : path.concat(control.name)}">
</ng-container>
</fieldset>
</ng-container>
</ng-container>
</ng-template>
</form>
The key point here is to get correct path
to destination control which I'm passing as part of context to next child level.
Also if you want to generate infinity structure nested elements you should refactor your code for building FormGroup:
ngOnInit() {
this.schema = data.start.fields.schema;
this.form = this.createFormGroup(this.schema);
}
createFormGroup(items) {
let group: { [controlId: string]: AbstractControl; } = {};
items.forEach((item) => group[item.name] = this.createFormControl(item));
return this.fb.group(group);
}
createFormControl(item) {
if (item.type === 'array') {
return this.fb.array(item.items.map((subItem) => {
return this.fb.group({
[subItem.name]: this.createFormControl(subItem)
})
}));
} else if (item.type === 'object') {
return this.createFormGroup(item.items);
} else {
return this.fb.control(item.value, []);
}
}
Ng-run Example