9

In my application, I have a need for a reusable nested form component, such as Address. I want my AddressComponent to deal with its own FormGroup, so that I don't need to pass it from the outside. At Angular conference (video, presentation) Kara Erikson, a member of Angular Core team recommended to implement ControlValueAccessor for the nested forms, making the nested form effectively just a FormControl.

I also figured out that I need to implement Validator, so that the validity of my nested form can be seen by the main form.

In the end, I created the SubForm class that the nested form needs to extend:

export abstract class SubForm implements ControlValueAccessor, Validator {

  form: FormGroup;

  public onTouched(): void {
  }

  public writeValue(value: any): void {
    if (value) {
      this.form.patchValue(value, {emitEvent: false});
      this.onTouched();
    }
  }

  public registerOnChange(fn: (x: any) => void): void {
    this.form.valueChanges.subscribe(fn);
  }

  public registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.form.disable()
      : this.form.enable();
  }

  validate(c: AbstractControl): ValidationErrors | null {
    return this.form.valid ? null : {subformerror: 'Problems in subform!'};
  }

  registerOnValidatorChange(fn: () => void): void {
    this.form.statusChanges.subscribe(fn);
  }
}

If you want your component to be used as a nested form, you need to do the following:

@Component({
  selector: 'app-address',
  templateUrl: './address.component.html',
  styleUrls: ['./address.component.css'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => AddressComponent),
      multi: true
    },
    {
      provide: NG_VALIDATORS,
      useExisting: forwardRef(() => AddressComponent),
      multi: true
    }
  ],
})

export class AddressComponent extends SubForm {

  constructor(private fb: FormBuilder) {
    super();
    this.form = this.fb.group({
      street: this.fb.control('', Validators.required),
      city: this.fb.control('', Validators.required)
    });
  }

}

Everything works well unless I check the validity status of my subform from the template of my main form. In this case ExpressionChangedAfterItHasBeenCheckedError is produced, see ngIf statement (stackblitz code) :

<form action=""
      [formGroup]="form"
      class="main-form">
  <h4>Upper form</h4>
  <label>First name</label>
  <input type="text"
         formControlName="firstName">
         <div *ngIf="form.controls['address'].valid">Hi</div> 
  <app-address formControlName="address"></app-address>
  <p>Form:</p>
  <pre>{{form.value|json}}</pre>
  <p>Validity</p>
  <pre>{{form.valid|json}}</pre>


</form>

3 Answers 3

5
+50

Use ChangeDetectorRef

Checks this view and its children. Use in combination with detach to implement local change detection checks.

This is a cautionary mechanism put in place to prevent inconsistencies between model data and UI so that erroneous or old data are not shown to a user on the page

Ref:https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4

Ref:https://angular.io/api/core/ChangeDetectorRef

import { Component, OnInit,ChangeDetectorRef } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-upper',
  templateUrl: './upper.component.html',
  styleUrls: ['./upper.component.css']
})
export class UpperComponent implements OnInit {

  form: FormGroup;

  constructor(private fb: FormBuilder,private cdr:ChangeDetectorRef) {
    this.form = this.fb.group({
      firstName: this.fb.control('', Validators.required),
      address: this.fb.control('')
    });
  }

  ngOnInit() {
    this.cdr.detectChanges();
  }


}

Your Forked Example:https://stackblitz.com/edit/github-3q4znr

Sign up to request clarification or add additional context in comments.

11 Comments

thanks! Does it mean that officially recommended way of composing the forms doesn't work and since it relies on a manual change detection call?
it will work.In your example some how change detection is being triggered after the change detection cycle that is why angular throwing error
I see that it works, my question is whether or not it's a bug in angular or not?
Yes, I know this kind of error and how to deal with it. It just surprises me that we need to manually trigger change detection or artificially make the validator asynchronous for such a fundamental operation as decomposition of forms.
Many developers even view it as a bug. But it’s certainly not. This is a cautionary mechanism put in place to prevent inconsistencies between model data and UI so that erroneous or old data are not shown to a user on the page. Check this also blog.angularindepth.com/…
|
2

WriteValue will be triggered in the same digest cycle with the normal change detection lyfe cycle hook.

To fix that without using changeDetectionRef you can define your validity status field and change it reactively.

public firstNameValid = false;

   this.form.controls.firstName.statusChanges.subscribe(
      status => this.firstNameValid = status === 'VALID'
    );

<div *ngIf="firstNameValid">Hi</div>

Comments

-2

Try to use [hidden] in stand of *ngIf, it will work without ChangeDetectorRef.

Update URL : https://stackblitz.com/edit/github-3q4znr-ivtrmz?file=src/app/upper/upper.component.html

<div [hidden]="!form.controls['address'].valid">Hi</div>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.