响应式表单,子组件的 FormArray 返回错误
Reactive Form, with FormArray of child components returning error
我正在研究如何将复杂的表单重构为更小的表单。
本质上有一个父表单,它有一个子组件数组,其中包含它们的所有者表单。
父 TS
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
form: FormGroup = this.fb.group({
mainAddress: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
}
addAddress() {
this.addresses.push(
this.fb.control({
address: [null, Validators.required],
})
);
}
}
<form [formGroup]="form">
<app-address-form formControlName="mainAddress"></app-address-form>
<hr />
<ng-container formArrayName="addresses">
<ng-container *ngFor="let addressForm of addresses.controls; index as i">
<app-address-form [formControlName]="i"></app-address-form>
<button (click)="removeAddress(i)">Remove Address</button>
</ng-container>
</ng-container>
</form>
<button (click)="addAddress()">Add Address</button>
子地址 TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: AddressFormComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: AddressFormComponent,
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator
{
@Input()
legend: string;
form: FormGroup = this.fb.group({
addressLine1: [null, [Validators.required]],
addressLine2: [null, [Validators.required]],
zipCode: [null, [Validators.required]],
city: [null, [Validators.required]],
});
onTouched: Function = () => {};
onChangeSubs: Subscription[] = [];
constructor(private fb: FormBuilder) {}
ngOnDestroy() {
for (let sub of this.onChangeSubs) {
sub.unsubscribe();
}
}
registerOnChange(onChange: any) {
const sub = this.form.valueChanges.subscribe(onChange);
this.onChangeSubs.push(sub);
}
registerOnTouched(onTouched: Function) {
this.onTouched = onTouched;
}
setDisabledState(disabled: boolean) {
if (disabled) {
this.form.disable();
} else {
this.form.enable();
}
}
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
errors = this.addControlErrors(errors, 'addressLine1');
errors = this.addControlErrors(errors, 'addressLine2');
errors = this.addControlErrors(errors, 'zipCode');
errors = this.addControlErrors(errors, 'city');
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
子地址HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input
matInput
placeholder="Address Line 1"
formControlName="addressLine1"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Address Line 2"
formControlName="addressLine2"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Zip Code"
formControlName="zipCode"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="City"
formControlName="city"
(blur)="onTouched()"
/>
</mat-form-field>
</fieldset>
Error i get is :
Error: NG01002: Must supply a value for form control with name: 'addressLine1'
请记住,我正在尝试通过尽可能多地分离(DRY 原则)来避免代码重复。
感谢任何反馈
这有一个类似的解决方案 here。
如果您在 FormGroup
上使用 setValue()
方法,您将需要更新该组中的所有控件。如果您只更新单个控件,则需要改用 patchValue()
。否则你会得到 NG01002 错误。
改变这个:
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
为此,将要更新的控件添加为参数:
writeValue(controlToUpdate, value: any) {
if (value) {
console.log(value);
this.form.patchValue({controlToUpdate: value},{emitEvent: false});
}
}
希望这对您有所帮助。
您有一个 FormControls (*) 数组(每个 FormControl“存储”一个具有以下属性的复杂对象:addressLine1、addressLine2、zipCode 和 city,但 FormArray 仍然是一个FormControls 的 FormArray)
所以,你的函数 addAddress 应该是这样的
addAddress() {
this.addresses.push(this.fb.control(null))
}
(*)当我们使用嵌套表单时存在两种方法。一种是您的方法:创建一个存储复杂对象的自定义 formControl,并在组件内部创建“sub-formGroup”。另一个是在主组件中创建“sub-formGroup”并将 formGroup 传递给一个组件(在最后一种情况下,您不创建实现 ControlValueAccess 的自定义表单控件,否则是一个带有 @Input
[= 的简单组件16=]
更新 关于 person.component 和 adress.component
中的函数验证
在person.component中你可以创建一个函数验证像
control:AbstractControl //<--if you want to see the error
//in .html you can use
//{{control.errors|json}}
validate(control: AbstractControl) {
if (!this.control) this.control = control; //<--this line is only if
//we can "get" the control
if (this.form.valid)
return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
//see that this.adresses is a FormArray, the formArray has no errors
//else his elements
if (e == 'addresses') {
this.addresses.controls.forEach((x,i) => {
const err = x.errors;
if (err) {
errors = errors || {};
errors[e+'.'+i] = err;
}
});
} else {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
}
});
return errors;
}
}
当您添加地址时使用 updateValueAndValidity
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
this.form.updateValueAndValidity();
}
函数验证 adress.form 喜欢
validate(control: AbstractControl) {
if (!this.control) this.control = control;
if (this.form.valid) return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
});
return errors;
}
我正在研究如何将复杂的表单重构为更小的表单。
本质上有一个父表单,它有一个子组件数组,其中包含它们的所有者表单。
父 TS
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
form: FormGroup = this.fb.group({
mainAddress: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
}
addAddress() {
this.addresses.push(
this.fb.control({
address: [null, Validators.required],
})
);
}
}
<form [formGroup]="form">
<app-address-form formControlName="mainAddress"></app-address-form>
<hr />
<ng-container formArrayName="addresses">
<ng-container *ngFor="let addressForm of addresses.controls; index as i">
<app-address-form [formControlName]="i"></app-address-form>
<button (click)="removeAddress(i)">Remove Address</button>
</ng-container>
</ng-container>
</form>
<button (click)="addAddress()">Add Address</button>
子地址 TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: AddressFormComponent,
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: AddressFormComponent,
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator
{
@Input()
legend: string;
form: FormGroup = this.fb.group({
addressLine1: [null, [Validators.required]],
addressLine2: [null, [Validators.required]],
zipCode: [null, [Validators.required]],
city: [null, [Validators.required]],
});
onTouched: Function = () => {};
onChangeSubs: Subscription[] = [];
constructor(private fb: FormBuilder) {}
ngOnDestroy() {
for (let sub of this.onChangeSubs) {
sub.unsubscribe();
}
}
registerOnChange(onChange: any) {
const sub = this.form.valueChanges.subscribe(onChange);
this.onChangeSubs.push(sub);
}
registerOnTouched(onTouched: Function) {
this.onTouched = onTouched;
}
setDisabledState(disabled: boolean) {
if (disabled) {
this.form.disable();
} else {
this.form.enable();
}
}
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
errors = this.addControlErrors(errors, 'addressLine1');
errors = this.addControlErrors(errors, 'addressLine2');
errors = this.addControlErrors(errors, 'zipCode');
errors = this.addControlErrors(errors, 'city');
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
子地址HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input
matInput
placeholder="Address Line 1"
formControlName="addressLine1"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Address Line 2"
formControlName="addressLine2"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="Zip Code"
formControlName="zipCode"
(blur)="onTouched()"
/>
</mat-form-field>
<mat-form-field>
<input
matInput
placeholder="City"
formControlName="city"
(blur)="onTouched()"
/>
</mat-form-field>
</fieldset>
Error i get is :
Error: NG01002: Must supply a value for form control with name: 'addressLine1'
请记住,我正在尝试通过尽可能多地分离(DRY 原则)来避免代码重复。
感谢任何反馈
这有一个类似的解决方案 here。
如果您在 FormGroup
上使用 setValue()
方法,您将需要更新该组中的所有控件。如果您只更新单个控件,则需要改用 patchValue()
。否则你会得到 NG01002 错误。
改变这个:
writeValue(value: any) {
if (value) {
console.log(value);
this.form.setValue(value, { emitEvent: false });
}
}
为此,将要更新的控件添加为参数:
writeValue(controlToUpdate, value: any) {
if (value) {
console.log(value);
this.form.patchValue({controlToUpdate: value},{emitEvent: false});
}
}
希望这对您有所帮助。
您有一个 FormControls (*) 数组(每个 FormControl“存储”一个具有以下属性的复杂对象:addressLine1、addressLine2、zipCode 和 city,但 FormArray 仍然是一个FormControls 的 FormArray)
所以,你的函数 addAddress 应该是这样的
addAddress() {
this.addresses.push(this.fb.control(null))
}
(*)当我们使用嵌套表单时存在两种方法。一种是您的方法:创建一个存储复杂对象的自定义 formControl,并在组件内部创建“sub-formGroup”。另一个是在主组件中创建“sub-formGroup”并将 formGroup 传递给一个组件(在最后一种情况下,您不创建实现 ControlValueAccess 的自定义表单控件,否则是一个带有 @Input
[= 的简单组件16=]
更新 关于 person.component 和 adress.component
中的函数验证在person.component中你可以创建一个函数验证像
control:AbstractControl //<--if you want to see the error
//in .html you can use
//{{control.errors|json}}
validate(control: AbstractControl) {
if (!this.control) this.control = control; //<--this line is only if
//we can "get" the control
if (this.form.valid)
return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
//see that this.adresses is a FormArray, the formArray has no errors
//else his elements
if (e == 'addresses') {
this.addresses.controls.forEach((x,i) => {
const err = x.errors;
if (err) {
errors = errors || {};
errors[e+'.'+i] = err;
}
});
} else {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
}
});
return errors;
}
}
当您添加地址时使用 updateValueAndValidity
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
this.form.updateValueAndValidity();
}
函数验证 adress.form 喜欢
validate(control: AbstractControl) {
if (!this.control) this.control = control;
if (this.form.valid) return null;
//if only give a generic error
//return {error:'form not fullfilled'}
let errors: any = null;
Object.keys(this.form.controls).forEach((e: any) => {
const err = this.form.get(e).errors;
if (err) {
errors = errors || {};
errors[e] = err;
}
});
return errors;
}