父表单从嵌套表单返回不正确的验证

Parent Form returning incorrect validation from nested form

我正在根据之前在 Whosebug 上提出的问题进行总结,并试图找出一个非常奇怪的问题。

问题是父表单没有更新其有效状态,似乎验证没有一直向上传播,这违反了整个委托原则。

包含所有数据的子表单的屏幕截图

这符合预期,returns 一切都是真的,理应如此。

带有嵌套表单的子表单的屏幕截图无效:

这应该显示两者都是假的,但是正如您所看到的,父级显示为真,就好像委派停止在人的形式上。

最简单的复制方法是:

更新应用模块的导入

@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中我写了控件的“错误”)