Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError

前端 未结 3 445
萌比男神i
萌比男神i 2021-02-04 02:01

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 fo

3条回答
  •  抹茶落季
    2021-02-04 02:33

    To understand the problem you need to read Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error article.

    For your particular case the problem is that you're creating a form in the AppComponent and use a {{myForm.valid}} interpolation in the DOM. It means that Angular will run create and run updateRenderer function for the AppComponent that updates DOM. Then you use the ngOnInit lifecycle hook of subcomponent to add subgroup with control to this form:

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

    The control becomes invalid because you don't supply initial value and you specify a required validator. Hence the entire form becomes invalid and the expression {{myForm.valid}} evaluates to false. But when Angular ran change detection for the AppComponent it evaluated to true. And that's what the error says.

    One possible fix could be to mark the form as invalid in the start since you're planning to add required validator, but it seems Angular doesn't provide such method. Your best choice is probably to add controls asynchronously. In fact, this is what Angular does itself in the sources:

    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 = container.registerControl(dir.name, dir.control);
          setUpControl(dir.control, dir);
          dir.control.updateValueAndValidity({emitEvent: false});
        });
      }
    

    So for you case it will be:

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

    Or use some variable in the AppComponent to hold form state and use it in the template:

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

提交回复
热议问题