Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError

时间:2017-08-13 13:44:30

标签: javascript forms angular

My reactive form is three component levels deep. The parent component creates a new form without any fields and passes it down to child components.

At first the outer form is valid. Later on a child component adds new form elements with validators (that fail) making the outer form invalid.

I am getting an ExpressionChangedAfterItHasBeenCheckedError error in the console. I want to fix that error.

Somehow this only happens when I add the third level of nesting. The same approach seemed to work for two levels of nesting.

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

Parent component

@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({});
  }
}

Sub component

@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);
  }

Child component

@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

3 个答案:

答案 0 :(得分:17)

要了解您需要阅读Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error文章的问题。

对于您的特定情况,问题是您要在AppComponent中创建表单并在DOM中使用{{myForm.valid}}插值。这意味着更新DOM的AppComponent的Angular will run create and run updateRenderer function。然后使用子组件的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;
    })
  }
}

答案 1 :(得分:0)

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();
  }
}

答案 2 :(得分:0)

我在Angular 9和更高版本中具有相同的场景和相同的问题,但效果很好。我做了一些调整:同步添加没有验证器的控件,并异步添加所需的验证器...因为我立即需要控件,否则会出现错误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(); }); }

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

    ELASTICSEARCH_DSL={
    'default': {
        'hosts': 'elasticsearch'
    },
}