Search code examples
angularvalidationangular-reactive-formscontrolvalueaccessor

Angular form control validation when using ControlValueAccessor


In the parent component I define the form.

  cartForm = new FormGroup({
    company: new FormControl('', [Validators.required]),
    costCenter: new FormGroup({
      account: new FormControl('', [Validators.required]),
      activity: new FormControl('', [Validators.required, Validators.maxLength(5), Validators.minLength(5)]),
      project: new FormControl('', [Validators.required, Validators.minLength(3), Validators.maxLength(10)]),
    })
  });

then the formControl project is used in a child component. In this component I need a custom validation and I can achive that by just adding

  validate(control: AbstractControl): ValidationErrors | null {
    // To my validation here
  }

And that works except that the Validators specified in the parent componet are overwritten.

Second approach would be to create a custom validator class.. but then I can't get the @Input from the component..?

Update: This is the component

@Component({
  selector: 'ssp-project',
  templateUrl: './project.component.html',
  styleUrls: ['./project.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectComponent implements ControlValueAccessor, OnInit {

  @Input('companyNumber') set companyNumber(companyNumber: string) {
    this._companyNumber = companyNumber;
  }
  private _companyNumber: string;

  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 {
    if (val) {
      this.ngControl.control?.setValue(val);
    }

  }

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

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

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

  ngOnInit() {
    // this.ngControl.control.setValidators([this.validate.bind(this)]);
    this.ngControl.control.setValidators(this.ngControl.control.validator ? [this.ngControl.control.validator, this.validate] : this.validate);
    this.ngControl.control.updateValueAndValidity();
  }

  validate(control: AbstractControl): ValidationErrors | null {

    if (this._companyNumber && control.value) {
      return this._costCenterService
        .validateProject(control.value, this._companyNumber)
        .pipe(
          debounceTime(3000),
          map(projectIsValid => {
            return { invalid: !projectIsValid };
          })
        ).subscribe();

    }
  }
}


Solution

  • You can add a validator to the list of existing validators using the validator property of the FormControl

    control.setValidators(control.validator ? [ control.validator, this.validate ] : this.validate);
    

    Edit: since the code was added in the question, the focus has shifted to async validators. E.g.:

    control.setAsyncValidators(
      control.asyncValidator
        ? [control.asyncValidator, this.asyncValidator]
        : this.asyncValidator
    );
    
    // definition of asyncValidator
    asyncValidator(ctrl: AbstractControl): Observable<ValidationErrors | null> {
      // Whatever your validation does asynchronously
      return of(null);
    }