Search code examples
angularangular-reactive-formscontrolvalueaccessor

How to trigger validation on form using angulars controlvalueaccessor


When working with forms in angular a normal approach is to just have the form elements (like inputs) directly in the form

<form>
 <input>
</form>

When this form is submitted and the input has a validator eg. required. the form inputs are validated and if it's not valid we can show that to the user... great...

To be able to reuse custom inputs we can create a component that contains this input + extras.

<form>
 <custom-component>
</form>

Check this stackblitz: https://stackblitz.com/edit/components-issue-whxtth?file=src%2Fapp%2Fuser%2Fuser.component.html

When hitting the submit button only one input is validated. If you interact with both inputs they will validate

There is nothing strange as far as I can see in the CVA component.

@Component({
  selector: "user",
  templateUrl: "./user.component.html",
  styleUrls: ["./user.component.scss"],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent implements ControlValueAccessor {
  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }

  onTouched = (_value?: any) => {};
  onChanged = (_value?: any) => {};

  writeValue(val: string): void {}

  registerOnChange(fn: any): void {
    this.onChanged = fn;
  }

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

Solution

  • Like I said the main reason why your input doesn't react to the validation is your component configured as changeDetection: ChangeDetectionStrategy.OnPush this will make your component only react to restricted actions :

    1. The Input reference changes
    2. An event originated from the component or one of its children
    3. Use the async pipe in the view
    4. Run change detection explicitly (Will provide a workaround)

    So you have 2 options :

    1. Embrace the default change detection strategy (Like Angular Material did)
    2. Pick an ugly workaround.

    Here how that workaround is :

    constructor(
        @Optional() @Self() public ngControl: NgControl,
        private cdr: ChangeDetectorRef // import the CDR
      ) {
        if (this.ngControl != null) {
          this.ngControl.valueAccessor = this;
          const { _parent } = this.ngControl as any; // this is not a public property
          // and refers to the parent form
          _parent.ngSubmit.subscribe((r: any) => { // this is fired when the form submitted
            this.cdr.detectChanges(); // detect changes manually
          });
        }
    }
    

    Stackblitz

    This will work where you have a form as the parent. With some null checks, it will work consistently. But you may encounter some other scenarios where you may not have the opportunity to trigger manual change detection and it will hurt the reusability of your component badly.