动态嵌套反应形式:ExpressionChangedAfterItHasBeenCheckedError

问题描述 投票:0回答:5

我的反应形式有三个组件级别深。父组件创建一个没有任何字段的新表单并将其传递给子组件。

首先外部形式是有效的。随后,子组件添加带有验证器(失败)的新表单元素,从而使外部表单无效。

我在控制台中收到 ExpressionChangedAfterItHasBeenCheckedError 错误。我想修复这个错误。

不知何故,只有当我添加第三层嵌套时才会发生这种情况。同样的方法似乎适用于两层嵌套。

笨蛋:https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview

父组件

@Component({
  selector: 'app-root',
  template: `
    myForm.valid: <b>{{myForm.valid}}</b>
    <form>
      <app-subform [myForm]="myForm"></app-subform>
    </form>
  `
})
export class AppComponent implements OnInit {
  ...

  ngOnInit() {
    this.myForm = this.formBuilder.group({});
  }
}

子组件

@Component({
  selector: 'app-subform',
  template: `
    <app-address-form *ngFor="let addressData of addressesData;"
      [addressesForm]="addressesForm">
    </app-address-form>
  `
})
export class SubformComponent implements OnInit {
  ...

  addressesData = [...];

  constructor() { }

  ngOnInit() {
    this.addressesForm = new FormArray([]);
    this.myForm.addControl('addresses', this.addressesForm);
  }

子组件

@Component({
  selector: 'app-address-form',
  template: `
    <input [formControl]="addressForm.controls.addressLine1">
    <input [formControl]="addressForm.controls.city">
  `
})
export class AddressFormComponent implements OnInit {
  ...

  ngOnInit() {
    this.addressForm = this.formBuilder.group({
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]
      ],
      city: [
        this.addressData.city
      ]
    });

    this.addressesForm.push(this.addressForm);
  }
}

enter image description here

javascript forms angular
5个回答
55
投票

要了解问题,您需要阅读 您需要了解的有关

ExpressionChangedAfterItHasBeenCheckedError
错误 的所有内容。

对于您的特定情况,问题是您在

AppComponent
中创建表单并在 DOM 中使用
{{myForm.valid}}
插值。这意味着 Angular 将为更新 DOM 的 AppComponent
 运行 create 并运行 updateRenderer 函数 
。然后使用子组件的
ngOnInit
生命周期钩子将带有控件的子组添加到此表单中:

export class AddressFormComponent implements OnInit {
  @Input() addressesForm;
  @Input() addressData;

  ngOnInit() {
    this.addressForm = this.formBuilder.group({
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]   <-----------
      ]

    this.addressesForm.push(this.addressForm); <--------

控件变得无效,因为您不提供初始值并且指定了所需的验证器。因此,整个形式变得无效,并且表达式

{{myForm.valid}}
的计算结果为
false
。但是当 Angular 对
AppComponent
运行变化检测时,它的评估结果为
true
。这就是错误所说的。

一个可能的修复方法是从一开始就将表单标记为无效,因为您计划添加所需的验证器,但 Angular 似乎没有提供这样的方法。您最好的选择可能是异步添加控件。事实上,这就是 Angular 在源代码中所做的事情:

const resolvedPromise = Promise.resolve(null);

export class NgForm extends ControlContainer implements Form {
  ...

  addControl(dir: NgModel): void {
    // adds controls asynchronously using Promise
    resolvedPromise.then(() => {
      const container = this._findContainer(dir.path);
      dir._control = <FormControl>container.registerControl(dir.name, dir.control);
      setUpControl(dir.control, dir);
      dir.control.updateValueAndValidity({emitEvent: false});
    });
  }

所以对于你来说它将是:

const resolvedPromise = Promise.resolve(null);

@Component({
   ...
export class AddressFormComponent implements OnInit {
  @Input() addressesForm;
  @Input() addressData;

  addressForm;

  ngOnInit() {
    this.addressForm = this.formBuilder.group({
      addressLine1: [
        this.addressData.addressLine1,
        [ Validators.required ]
      ],
      city: [
        this.addressData.city
      ]
    });

    resolvedPromise.then(() => {
       this.addressesForm.push(this.addressForm); <-------
    })
  }
}

或者在

AppComponent
中使用一些变量来保存表单状态并在模板中使用它:

{{formIsValid}}

export class AppComponent implements OnInit {
  myForm: FormGroup;
  formIsValid = false;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({});
    this.myForm.statusChanges((status)=>{
       formIsValid = status;
    })
  }
}

5
投票
import {ChangeDetectorRef} from '@angular/core';
....

export class SomeComponent {

  form: FormGroup;

  constructor(private fb: FormBuilder,
              private ref: ChangeDetectorRef) {
    this.form = this.fb.group({
      myArray: this.fb.array([])
    });
  }

  get myArray(): FormArray {
    return this.form.controls.myArray as FormArray;
  }

  addGroup(): void {
    const newGroup = this.fb.group({
      prop1: [''],
      prop2: ['']
    });

    this.myArray.push(newGroup);
    this.ref.detectChanges();
  }
}

4
投票

我在 Angular 9 和以上解决方案中遇到了相同的场景和相同的问题,效果很好。我稍微调整了一下:同步添加不带验证器的控件并异步添加所需的验证器...因为我立即需要控件,否则我会收到错误

cannot find formControl ...

这是我基于上面接受的答案的解决方案:

const resolvedPromise = Promise.resolve(null);
@Component({
  selector: 'password-input',
  templateUrl: './password-input.component.html',
  styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
  @Input() parentFormGroup : FormGroup;
  @Input() controlName : string = 'password';
  @Input() placeholder : string = 'Password';
  @Input() label : string = null;

  ngOnInit(): void {
    this.parentFormGroup.addControl(this.controlName, new FormControl(null));
   
    resolvedPromise.then( () => {
      this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
      this.parentFormGroup.get(this.controlName).updateValueAndValidity();
    });
  }

1
投票

按照Max Koretskyi的解决方案,我们可以在

ngOnInit
上使用异步/等待模式。

这是一个例子:

@Component({
  selector: 'my-sub-component',
  templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
  @Input()
  form: FormGroup;

  async ngOnInit() {
    // Avoid ExpressionChangedAfterItHasBeenCheckedError
    await Promise.resolve(); 

    this.form.removeControl('config');
    
    this.form.addControl('config', new FormControl("", [Validator.required]));       
  }
}

0
投票

Matdialog 组件内具有 min 属性的 Angular Material Datepicker 可能会发生这种情况。

https://stackblitz.com/edit/angular-jgnjsl?devToolsHeight=33&file=src%2Fapp%2Fcustom-datepicker.component.ts

我的解决方案是实现自定义 minDateValidator:

  public minDateValidator(minDate: Date): ValidatorFn {
    return (formGroup: FormGroup) => {
      const startDateControl = formGroup.get('start');
      const startMoment = this.toMoment(startDateControl.value);
      const minMoment = this.toMoment(minDate);

      if (minMoment.isAfter(startMoment)) {
        return { minDate: true };
      }

      return null;
    };
  }

用途:

return this.formBuilder.group(
      {
        start: [''],
        b: ['', [Validators.required]],
        c: ['', [Validators.required]],
        d: ['', [Validators.required]],
        e: [''],
      },
      {
        validators: [
          minDateValidator(this.data.earliestSelectableDate),
        ],
        updateOn: 'blur',
      },
    );

模板:

<input
   class="calendar-input"
   formControlName="start"
   matInput
   [matDatepicker]="startPicker"
   (click)="startPicker.open()" />

   <mat-error>
      <span *ngIf="row.errors?.minDate">
        {{ 'common.minDate' | translate }}
      </span>
   </mat-error>
© www.soinside.com 2019 - 2024. All rights reserved.