问题
I am trying to implement a feature on my angular app where the users will be able to create their own forms (selecting fields/validation/etc). Once they create the form, I'll store it's JSON structure on the DB for use later:
export interface FormModel {
id: string;
name: string;
type: 'group' | 'control';
children: FormModel[];
isArray: boolean;
}
A sample structure is this:
[{
id: 'users',
name: 'Users',
type: 'group',
isArray: true,
children: [
{
id: "user",
name: "User",
type: 'group',
isArray: false,
children: [
{
id: "name",
name: "Name",
type: 'control',
children: null,
isArray: false
},
{
id: "age",
name: "Age",
type: 'control',
children: null,
isArray: false
}
]
},
{
id: "user",
name: "User",
type: 'group',
isArray: false,
children: [
{
id: "name",
name: "Name",
type: 'control',
children: null,
isArray: false,
},
{
id: "age",
name: "Age",
type: 'control',
children: null,
isArray: false
}
]
}
]
}, {
id: 'comments',
name: 'Comments',
type: 'control',
children: null,
isArray: false
}
];
After creating the reactive form based on the JSON loaded from the DB, I'm having difficulties creating the respective html since it has recursion.
After many attempts, I managed to get to the following, where it generates a HTML similar to what I understand should be needed:
<div formGroupName="users">
<div formArrayName="0">
<div formGroupName="user">
<input type="text" formControlName="name">
<input type="text" formControlName="age">
</div>
</div>
<div formArrayName="0">
<div formGroupName="user">
<input type="text" formControlName="name">
<input type="text" formControlName="age">
</div>
</div>
</div>
The template I've used is the following:
<form name="myForm" [formGroup]="myForm" fxLayout="column" fxFlex>
<div formGroupName="variables">
<ng-template #recursiveList let-controls let-prefix="prefix">
<ng-container *ngFor="let item of controls; let i = index;">
<input type="text" [formControlName]="item.id" *ngIf="(item?.children?.length > 0) == false">
<div *ngIf="item?.children?.length > 0 && item.isArray" [formArrayName]="item.id">
<ng-container
*ngTemplateOutlet="recursiveArray; context:{ $implicit: item.children, prefix: item.isArray }">
</ng-container>
</div>
<div *ngIf="item?.children?.length > 0 && !item.isArray" [formGroupName]="item.id">
<ng-container
*ngTemplateOutlet="recursiveList; context:{ $implicit: item.children, prefix: item.isArray }">
</ng-container>
</div>
</ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="recursiveList; context:{ $implicit: formFields, prefix: '' }">
</ng-container>
<ng-template #recursiveArray let-controls let-prefix="prefix">
<ng-container *ngFor="let item of controls; let i = index;">
<div [formGroupName]="i">
<input type="text" [formControlName]="item.id" *ngIf="(item?.children?.length > 0) == false">
<div *ngIf="item?.children?.length > 0 && item.isArray" [formArrayName]="item.id">
<ng-container
*ngTemplateOutlet="recursiveArray; context:{ $implicit: item.children, prefix: item.isArray }">
</ng-container>
</div>
<div *ngIf="item?.children?.length > 0 && !item.isArray" [formGroupName]="item.id">
<ng-container
*ngTemplateOutlet="recursiveList; context:{ $implicit: item.children, prefix: item.isArray }">
</ng-container>
</div>
</div>
</ng-container>
</ng-template>
</div>
</form>
It seems to me that it is right, but I keep getting errors:
ERROR
Error: Cannot find control with path: 'variables -> 0'
ERROR
Error: Cannot find control with path: 'variables -> 0 -> user'
I have created a stackblitz with the sample: https://stackblitz.com/edit/angular-mtbien
Could you guys help me identify the problem? I've been working on this for 2 days without any success :(
Thanks!
回答1:
As Ersenkoening say, for work with FormsGroup and Form Arrays you can use the "controls", directly. Use FormArrayName, Form Group, etc, can be a real headache.
See that the form-array.component.html of Ersenkoening can be coded more simple like
<div [formGroup]="formArray">
<ng-container *ngFor="let child of formArray.controls; let i = index">
<app-form-group [formGroup]="child"></app-form-group>
</ng-container>
</div>
Yes is a different way to mannage a FormArray but remember that a formArray it's only a "especial" FormGroup.
Update With this idea, we are going to go more deeper, in the html only use [formControl], so, we need pass the "control" as a variable. See stackblitz
The form-filed-view is like
<form name="myForm" [formGroup]="myForm" fxLayout="column" fxFlex>
<div *ngFor="let item of formFields;let i=index">
{{item.id}}
<ng-container *ngTemplateOutlet="recursiveList; context:{
$implicit: formFields[i],
<!--see how pass the control of myForm--->
control:myForm.get(item.id) }">
</ng-container>
</div>
<ng-template #recursiveList let-item let-control="control">
<div *ngIf="!item.children">
{{item.id}}<input [formControl]="control">
</div>
<div *ngIf="item.children">
<div *ngIf="!item.isArray">
<div *ngFor="let children of item.children">
<ng-container *ngTemplateOutlet="recursiveList; context:{
$implicit:children,
<!--see how pass the control of a formGroup--->
control:control.get(children.id)}">
</ng-container>
</div>
</div>
<div *ngIf="item.isArray">
<div *ngFor="let children of item.children;let i=index">
<ng-container *ngTemplateOutlet="recursiveList; context:{
$implicit:children,
<!--see how pass the control of a formArray--->
control:control.at(i)}">
</ng-container>
</div>
</div>
</div>
</ng-template>
</form>
<pre>
{{myForm?.value|json}}
</pre>
Update 2 simplify the creation of the form, see stackblitz
ngOnInit() {
let group = new FormGroup({});
this.formFields.forEach(element => {
let formItem = this.createFormGroup(element);
group.addControl(element.id, formItem);
});
this.myForm = group;
}
private createFormGroup(formItem: FormModel) {
if (formItem.type=="group")
{
if (formItem.isArray && formItem.children.length<formItem.minQtd)
{
const add={...formItem.children[0]}
//here we can "clean" the value
while (formItem.children.length<formItem.minQtd)
formItem.children.push(add)
}
let group:FormGroup=new FormGroup({});
let controls:any[]=[]
formItem.children.forEach(element=>{
let item=this.createFormGroup(element);
if (formItem.isArray)
controls.push(item);
else
group.addControl(element.id, item);
})
if (!formItem.isArray)
return group;
return new FormArray(controls)
}
if (formItem.type=="control")
return new FormControl();
}
回答2:
Your generated html needs to be in a different order for a FormArray. You need to assign the formArrayName="users" to the outer html element and inside that html element you need the [formGroupName]="i" where i is the current index of the FormControl or FormGroup inside your array.
So you are looking for a structure like this:
<div formArrayName="FORM_ARRAY_NAME"
*ngFor="let item of orderForm.get('items').controls; let i = index;">
<div [formGroupName]="i">
<input formControlName="FORM_CONTROL_NAME">
</div>
Chosen name: {{ orderForm.controls.items.controls[i].controls.name.value }}
</div>
Here is a nice article describing the right setup for a FormArray.
Having said that, I forked your stackblitz and had a look. I moved the FormArray and FormGroups into separate components instead of using ng-template, but if you really need to, you could do the same using ng-template.
So basically what I changed was the order and bindings for a FormArray and I worked with the FormGroup, FormArrays and FormControls objects itself rather than using the FormGroup/FormControl values like isFormArray inside the template to determine which template needs to be used.
A possible solution for your problem could look like this:
Starting component
<form name="myForm" [formGroup]="myForm" fxLayout="column" fxFlex>
<app-form-group [formGroup]="myForm.get('variables')"></app-form-group>
</form>
form-group.component.ts
<div [formGroup]="formGroup"> // necessary because the <form> tag is outside this component
<ng-container *ngFor="let key of controlKeys">
<ng-container *ngIf="!isFormArray(key) && !isFormGroup(key)">
<p>
<label>{{key}}</label>
<input type="text" [formControlName]="key">
</p>
</ng-container>
<ng-container *ngIf="isFormArray(key)">
<app-form-array [formArray]="formGroup.get(key)" [formArrayName]="key" [parentFormGroup]="formGroup" ></app-form-array>
</ng-container>
<ng-container *ngIf="isFormGroup(key)">
<app-form-group [formGroup]="formGroup.get(key)"></app-form-group>
</ng-container>
</ng-container>
</div>
form-froup.component.ts
public isFormArray(key: string): boolean {
return this.formGroup.get(key) instanceof FormArray
}
public isFormGroup(key: string): boolean {
return this.formGroup.get(key) instanceof FormGroup
}
get controlKeys() {
return Object.keys(this.formGroup.controls);
}
form-array.component.html
<div [formGroup]="parentFormGroup">
<div [formArrayName]="formArrayName">
<ng-container *ngFor="let child of formArray.controls; let i = index">
<div [formGroupName]="i">
<app-form-group [formGroup]="child"></app-form-group>
</div>
</ng-container>
</div>
</div>
Here is the forked stackblitz
Note
If you split up the the form elements inside a <form> into different subcomponents, you need a FormGroup-Binding on any element e.g. simply a <div>
This implementation expects that all FormArray items are FormGroups. If that's not always the case you would need to add that.
来源:https://stackoverflow.com/questions/56419065/angular-reative-forms-user-customized-forms-error