Angular Reative Forms > 用户自定义表单错误
Angular Reative Forms > User customized forms error
我正在尝试在我的 angular 应用程序上实现一项功能,用户可以在其中创建自己的表单(选择 fields/validation/etc)。一旦他们创建了表单,我将把它的 JSON 结构存储在数据库中供以后使用:
export interface FormModel {
id: string;
name: string;
type: 'group' | 'control';
children: FormModel[];
isArray: boolean;
}
示例结构是这样的:
[{
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
}
];
基于从数据库加载的 JSON 创建响应式表单后,我在创建相应的 html 时遇到困难,因为它具有递归。
经过多次尝试,我设法得到了以下内容,它生成了一个 HTML 类似于我理解应该需要的内容:
<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>
我使用的模板如下:
<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>
在我看来是对的,但我总是报错:
ERROR
Error: Cannot find control with path: 'variables -> 0'
ERROR
Error: Cannot find control with path: 'variables -> 0 -> user'
我用示例创建了一个 stackblitz:
https://stackblitz.com/edit/angular-mtbien
你们能帮我找出问题所在吗?我已经为此工作了 2 天,但没有任何成功:(
谢谢!
您生成的 html 需要与 FormArray 的顺序不同。您需要将 formArrayName="users"
分配给外部 html 元素,并且在 html 元素内部您需要 [formGroupName]="i"
其中 i
是 FormControl 的当前索引或数组中的 FormGroup。
所以你正在寻找这样的结构:
<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 是一篇很好的文章,描述了 FormArray 的正确设置。
话虽如此,我还是分叉了你的 stackblitz 看看。我将 FormArray 和 FormGroups 移动到单独的组件中而不是使用 ng-template,但如果你真的需要,你可以使用 ng-template.
来做同样的事情
所以基本上我更改的是 FormArray 的顺序和绑定,我使用 FormGroup、FormArrays 和 FormControls 对象本身,而不是使用模板内的 FormGroup/FormControl 值(如 isFormArray)来确定哪个模板需要待用。
您的问题的可能解决方案如下所示:
起始组件
<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>
备注
如果将 <form>
中的表单元素拆分为不同的子组件,则需要在任何元素上使用 FormGroup-Binding,例如只是一个 <div>
此实现要求所有 FormArray 项都是 FormGroup。如果情况并非总是如此,您需要添加它。
正如 Ersenkoening 所说,要使用 FormsGroup 和 Form Arrays,您可以直接使用 "controls"。使用FormArrayName、Form Group等,真是头疼。
看到Ersenkoening的form-array.component.html可以像
这样编码更简单
<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>
是的,这是管理 FormArray 的另一种方式,但请记住,formArray 只是一个 "especial" FormGroup。
更新
有了这个想法,我们要更深入,在html中只使用[formControl],所以,我们需要将"control"作为变量传递。参见 stackblitz
form-filed-view就像
<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>
更新2简化了表单的创建,见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();
}
我正在尝试在我的 angular 应用程序上实现一项功能,用户可以在其中创建自己的表单(选择 fields/validation/etc)。一旦他们创建了表单,我将把它的 JSON 结构存储在数据库中供以后使用:
export interface FormModel {
id: string;
name: string;
type: 'group' | 'control';
children: FormModel[];
isArray: boolean;
}
示例结构是这样的:
[{
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
}
];
基于从数据库加载的 JSON 创建响应式表单后,我在创建相应的 html 时遇到困难,因为它具有递归。
经过多次尝试,我设法得到了以下内容,它生成了一个 HTML 类似于我理解应该需要的内容:
<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>
我使用的模板如下:
<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>
在我看来是对的,但我总是报错:
ERROR
Error: Cannot find control with path: 'variables -> 0'
ERROR
Error: Cannot find control with path: 'variables -> 0 -> user'
我用示例创建了一个 stackblitz: https://stackblitz.com/edit/angular-mtbien
你们能帮我找出问题所在吗?我已经为此工作了 2 天,但没有任何成功:(
谢谢!
您生成的 html 需要与 FormArray 的顺序不同。您需要将 formArrayName="users"
分配给外部 html 元素,并且在 html 元素内部您需要 [formGroupName]="i"
其中 i
是 FormControl 的当前索引或数组中的 FormGroup。
所以你正在寻找这样的结构:
<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 是一篇很好的文章,描述了 FormArray 的正确设置。
话虽如此,我还是分叉了你的 stackblitz 看看。我将 FormArray 和 FormGroups 移动到单独的组件中而不是使用 ng-template,但如果你真的需要,你可以使用 ng-template.
来做同样的事情所以基本上我更改的是 FormArray 的顺序和绑定,我使用 FormGroup、FormArrays 和 FormControls 对象本身,而不是使用模板内的 FormGroup/FormControl 值(如 isFormArray)来确定哪个模板需要待用。
您的问题的可能解决方案如下所示:
起始组件
<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>
备注
如果将 <form>
中的表单元素拆分为不同的子组件,则需要在任何元素上使用 FormGroup-Binding,例如只是一个 <div>
此实现要求所有 FormArray 项都是 FormGroup。如果情况并非总是如此,您需要添加它。
正如 Ersenkoening 所说,要使用 FormsGroup 和 Form Arrays,您可以直接使用 "controls"。使用FormArrayName、Form Group等,真是头疼。
看到Ersenkoening的form-array.component.html可以像
这样编码更简单<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>
是的,这是管理 FormArray 的另一种方式,但请记住,formArray 只是一个 "especial" FormGroup。
更新 有了这个想法,我们要更深入,在html中只使用[formControl],所以,我们需要将"control"作为变量传递。参见 stackblitz
form-filed-view就像
<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>
更新2简化了表单的创建,见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();
}