DEV Community

Simon
Simon

Posted on

Angular Reactive Forms Conditional Validation

So I ran into an issue with conditional form validation and I wanted to make sure that it was clear how to validate the form. Also that the form group didn't need a certain structure to accommodate.

Started a google search, not a lot was there to be found; well I found this example - but it's not as nice as I wanted It to be.

I wrote out on Twitter to get some angular community advice. Based on all the advice I got I took something from the article above, but also Chau Tran shared an article that I got some inspiration from this one - but it used a subscriber that wasn't unsubscribed, wanna note it's an old article and Chau is a solid dev.

So i ended up with something like this - stackblitz:

conditional.component.ts

@Component({ ... })
export class ConditionalComponent {
  form = fb.group({
    type: fb.control<Vals>('a', [watchField()]),
    helloA: [
      null,
      [
        validateIf<Vals>('type', 'a', [
          Validators.required,
          Validators.min(100),
        ]),
      ],
    ],
    helloB: [null, [validateIf<Vals>('type', 'b', Validators.required)]],
    helloAnotherB: [
      null,
      [
        validateIf<Vals>('type', 'b', [
          Validators.required,
          Validators.minLength(4),
        ]),
      ],
    ],
  });
}
Enter fullscreen mode Exit fullscreen mode

conditional.component.html

<form class="conditional-form" [formGroup]="form">
  <h1 class="mat-headline-4">Conditional Form</h1>

  <mat-radio-group formControlName="type">
    <mat-radio-button color="primary" value="a">A</mat-radio-button>
    <mat-radio-button color="primary" value="b">B</mat-radio-button>
  </mat-radio-group>

  <ng-container *ngIf="form.controls.type.value === 'a'">
    <mat-form-field>
      <mat-label>Required and min 100 when A</mat-label>
      <input matInput formControlName="helloA" type="number" />
    </mat-form-field>
  </ng-container>

  <ng-container *ngIf="form.controls.type.value === 'b'">
    <mat-form-field>
      <mat-label>Required when B</mat-label>
      <input matInput formControlName="helloB" type="number" />
    </mat-form-field>

    <mat-form-field>
      <mat-label>Required and minLength 4 when B</mat-label>
      <input matInput formControlName="helloAnotherB" type="text" />
    </mat-form-field>
  </ng-container>

  <button mat-flat-button color="primary" [disabled]="form.invalid">
    Submit
  </button>
</form>
Enter fullscreen mode Exit fullscreen mode

So lets talk about what happens in the custom validators (Example below).

First the validateIf it takes the name of the control conditionalFieldName and a value of that field conditionalValue so if it matches run validations otherwise don't. Lastly i takes a validator or an array of validators that needs to be run if the conditional field value is met.

Secondly of we have the watchField its essentially telling fields that are not the watched field to updateValueAndValidity this is because we in our validateIf check if the field has the same validity so if the value change they need to be re-evaluated

custom-validators.ts

export function watchField() {
  return (formControl: FormControl) => {
    if (!formControl.parent) {
      return null;
    }

    Object.values(formControl.parent.controls).forEach((value) => {
      if (value !== formControl) {
        value.updateValueAndValidity();
      }
    });

    return null;
  };
}

export function validateIf<T>(
  conditionalFieldName: string,
  conditionalValue: T,
  validators: ValidatorFn | ValidatorFn[],
  errorNamespace = 'conditional'
): ValidatorFn {
  return (formControl) => {
    if (!formControl.parent) {
      return null;
    }

    let error = null;
    const conditionalField = formControl.parent.get(conditionalFieldName);

    if (!conditionalField) {
      console.warn('Conditional field not found');

      return null;
    }

    if (conditionalField.value === conditionalValue) {
      const validatorArr = Array.isArray(validators) ? validators : [validators];

      error = validatorArr.some((validator) => validator(formControl) !== null);
    }

    if (errorNamespace && error) {
      const customError = {};
      customError[errorNamespace] = error;
      error = customError;
    }

    return error;
  };
}
Enter fullscreen mode Exit fullscreen mode

Last but not least i took the liberty of typing it out so if the conditional field can be one of three values then you make sure that the validateIf only can validate on that.

If you have suggestions for improvements dont hesitate to share it in the comment section below.

If you liked what you read like and follow!

/Cheers

Top comments (0)