Angular - Async validator with valueChanges from another control

Viewed 1957

I am trying to access the valueChanges of another field (B) in an asynchronous validator (of field A). But the validation is only triggered when the value of field A changes. If only the value of B changes, the status remains "PENDING" until the value of A is changed.

What I want to do is create a validator that triggers when the given observables emits a value AND when the value of field B changes. I can imagine that the problem has something to do with the valueChanges observable not emitting any value, but how do I fix that?

StackBlitz which recreates the problem: https://stackblitz.com/edit/angular-ivy-epoeen?devtoolsheight=33&file=src/app/app.component.ts

Example component:

export class AppComponent {
  public form = new FormGroup({
    fieldA: new FormControl(null, { updateOn: 'blur' }),
    fieldB: new FormControl(null, { updateOn: 'blur' }),
  });

  constructor() {
    this.fieldBControl.setAsyncValidators(
      FieldBValidators.asyncValidateFieldBShouldNotBe(this.fieldA$)
    );

    this.fieldA$.subscribe((value) => console.log('fieldA changed to', value));
    this.fieldB$.subscribe((value) => console.log('fieldB changed to', value));
    this.fieldBStatus$.subscribe((status) => console.log('status changed to', status));
  }

  public get fieldAControl(): FormControl {
    return this.form.get('fieldA') as FormControl;
  }

  public get fieldA$(): Observable<string> {
    return this.fieldAControl.valueChanges;
  }

  public get fieldBControl(): FormControl {
    return this.form.get('fieldB') as FormControl;
  }

  public get fieldB$(): Observable<string> {
    return this.fieldBControl.valueChanges;
  }

  public get fieldBStatus$(): Observable<string> {
    return this.fieldBControl.statusChanges;
  }
}

Validator:

export class FieldBValidators {
  public static asyncValidateFieldBShouldNotBe(
    value$: Observable<string>
  ): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors> =>
      value$.pipe(
        tap((value) => {
          console.log('fieldA', value);
          console.log('fieldB', control.value);
        }),
        take(1),
        map(
          (value): ValidationErrors => {
            if (value === control.value) {
              return { error: true };
            }
            return null;
          }
        )
      );
  }
}

Console output is like this:

app.component.ts:24      fieldA changed to a
fieldB.validators.ts:23  fieldA a
fieldB.validators.ts:24  fieldB null
app.component.ts:25      fieldB changed to a
app.component.ts:27      status changed to PENDING
app.component.ts:25      fieldB changed to b
app.component.ts:27      status changed to PENDING
app.component.ts:24      fieldA changed to b
fieldB.validators.ts:23  fieldA b
fieldB.validators.ts:24  fieldB b
app.component.ts:27      status changed to INVALID

My attempt was to add a startWith pipe to the value, but then the Validator function always gets the initial value.

Thank you very much for any input. Have a nice day!

1 Answers

I don't get why you try to compare 2 form values in an asynchronous way. There is a concept called Cross form validation. That is a validator function which is installed on the form group instead of the form control. That concept would look like this for you:

(Stackblitz: https://stackblitz.com/edit/angular-ivy-epz6bv?file=src%2Fapp%2Fapp.component.ts)

export class FieldBValidators {
  public static fieldsShouldNotEqual(left: string, right: string): ValidatorFn {
    return (group: FormGroup): ValidationErrors | null =>
      group.get(left).value == group.get(right).value ? { error: true } : null;
  }
}
this.form.setValidators(
      FieldBValidators.fieldsShouldNotEqual("fieldA", "fieldB")
    ); // Field A should not equal Field B
Related