父表单从嵌套表单返回不正确的验证
Parent Form returning incorrect validation from nested form
我正在根据之前在 Whosebug 上提出的问题进行总结,并试图找出一个非常奇怪的问题。
问题是父表单没有更新其有效状态,似乎验证没有一直向上传播,这违反了整个委托原则。
包含所有数据的子表单的屏幕截图
这符合预期,returns 一切都是真的,理应如此。
带有嵌套表单的子表单的屏幕截图无效:
这应该显示两者都是假的,但是正如您所看到的,父级显示为真,就好像委派停止在人的形式上。
最简单的复制方法是:
- ng新测试
- 由你选择路由,非css,真的无所谓
- ng 添加@angular/material
- ng g c 人形
- ng g c 地址形式
更新应用模块的导入
@NgModule({
declarations: [
AppComponent,
PersonFormComponent,
AddressFormComponent
],
imports: [
BrowserModule,
FormsModule, // ADD
ReactiveFormsModule, // ADD
AppRoutingModule,
BrowserAnimationsModule,
MatFormFieldModule, //ADD
MatInputModule, //ADD
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
对 Person 组件进行以下更新
HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input matInput placeholder="First name" formControlName="firstName" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Last name" formControlName="lastName" (blur)="onTouched()" />
</mat-form-field>
<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)="removeAddressAtIndex(i)">Remove Address</button>
</ng-container>
</ng-container>
</fieldset>
<button (click)="addAddress()">Add Address</button>
<h3>The person form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-person-form',
templateUrl: './person-form.component.html',
styleUrls: ['./person-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
],
})
export class PersonFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
form: FormGroup = this.fb.group({
firstName: [null, Validators.required],
lastName: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
removeAddressAtIndex(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
onTouched: Function = () => { };
onChangeSubs: Subscription[] = [];
onValidationChange: any = () => { };
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
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 = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
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>
TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
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[] = [];
onValidationChange: any = () => { };
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
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 = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
需要对 App 组件进行画龙点睛
HTML
<form [formGroup]="form">
<app-person-form formControlName="person"></app-person-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>
<h3>The parent form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
form: FormGroup = this.fb.group({
person: [null, Validators.required],
totalQuantity: [
0,
[Validators.required, Validators.min(0), Validators.max(100)],
],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
}
这让我很头疼,因为我无法弄清楚传播有效停止的原因。
补充我的评论。您需要在 person.component:
中替换您的验证函数
validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
Object.keys(this.form.controls).forEach((e: any) => {
//see that if e=='adresses' this.form.controls['adresses'].errors is null
if (e== 'addresses')
{
this.addresses.controls.forEach((x,i) => {
errors = this.addControlErrors(errors, e+'.'+i);
});
}
else
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
//see that you use this.form.get(controlName)
//NOT this.form.controls[controlName]
//this allow you pass as name some like 'addresses.0'
const controlErrors = this.form.get(controlName).errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
见forked stackblitz(在.html的leyend中我写了控件的“错误”)
我正在根据之前在 Whosebug 上提出的问题进行总结,并试图找出一个非常奇怪的问题。
问题是父表单没有更新其有效状态,似乎验证没有一直向上传播,这违反了整个委托原则。
包含所有数据的子表单的屏幕截图
这符合预期,returns 一切都是真的,理应如此。
带有嵌套表单的子表单的屏幕截图无效:
这应该显示两者都是假的,但是正如您所看到的,父级显示为真,就好像委派停止在人的形式上。
最简单的复制方法是:
- ng新测试
- 由你选择路由,非css,真的无所谓
- ng 添加@angular/material
- ng g c 人形
- ng g c 地址形式
更新应用模块的导入
@NgModule({
declarations: [
AppComponent,
PersonFormComponent,
AddressFormComponent
],
imports: [
BrowserModule,
FormsModule, // ADD
ReactiveFormsModule, // ADD
AppRoutingModule,
BrowserAnimationsModule,
MatFormFieldModule, //ADD
MatInputModule, //ADD
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
对 Person 组件进行以下更新
HTML
<fieldset [formGroup]="form">
<mat-form-field>
<input matInput placeholder="First name" formControlName="firstName" (blur)="onTouched()" />
</mat-form-field>
<mat-form-field>
<input matInput placeholder="Last name" formControlName="lastName" (blur)="onTouched()" />
</mat-form-field>
<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)="removeAddressAtIndex(i)">Remove Address</button>
</ng-container>
</ng-container>
</fieldset>
<button (click)="addAddress()">Add Address</button>
<h3>The person form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-person-form',
templateUrl: './person-form.component.html',
styleUrls: ['./person-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => PersonFormComponent),
},
],
})
export class PersonFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
form: FormGroup = this.fb.group({
firstName: [null, Validators.required],
lastName: [null, Validators.required],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
removeAddressAtIndex(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
onTouched: Function = () => { };
onChangeSubs: Subscription[] = [];
onValidationChange: any = () => { };
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
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 = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
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>
TS
@Component({
selector: 'app-address-form',
templateUrl: './address-form.component.html',
styleUrls: ['./address-form.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: forwardRef(() => AddressFormComponent),
},
],
})
export class AddressFormComponent
implements ControlValueAccessor, OnDestroy, Validator {
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[] = [];
onValidationChange: any = () => { };
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) { }
registerOnValidatorChange?(fn: () => void): void {
this.onValidationChange = fn;
}
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 = {};
Object.keys(this.form.controls).forEach((e: any) => {
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
const controlErrors = this.form.controls[controlName].errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
}
需要对 App 组件进行画龙点睛
HTML
<form [formGroup]="form">
<app-person-form formControlName="person"></app-person-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>
<h3>The parent form is valid: </h3><h2>{{form.valid}}</h2>
TS
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
form: FormGroup = this.fb.group({
person: [null, Validators.required],
totalQuantity: [
0,
[Validators.required, Validators.min(0), Validators.max(100)],
],
addresses: this.fb.array([]),
});
constructor(private fb: FormBuilder, private cdr: ChangeDetectorRef) {}
get addresses() {
return this.form.controls['addresses'] as FormArray;
}
removeAddress(i: number) {
this.addresses.removeAt(i);
this.cdr.detectChanges();
}
addAddress() {
this.addresses.push(this.fb.control(null));
this.cdr.detectChanges();
}
}
这让我很头疼,因为我无法弄清楚传播有效停止的原因。
补充我的评论。您需要在 person.component:
中替换您的验证函数validate(control: AbstractControl) {
if (this.form.valid) {
return null;
}
let errors: any = {};
Object.keys(this.form.controls).forEach((e: any) => {
//see that if e=='adresses' this.form.controls['adresses'].errors is null
if (e== 'addresses')
{
this.addresses.controls.forEach((x,i) => {
errors = this.addControlErrors(errors, e+'.'+i);
});
}
else
errors = this.addControlErrors(errors, e);
});
return errors;
}
addControlErrors(allErrors: any, controlName: string) {
const errors = { ...allErrors };
//see that you use this.form.get(controlName)
//NOT this.form.controls[controlName]
//this allow you pass as name some like 'addresses.0'
const controlErrors = this.form.get(controlName).errors;
if (controlErrors) {
errors[controlName] = controlErrors;
}
return errors;
}
见forked stackblitz(在.html的leyend中我写了控件的“错误”)