Angular Reactive Forms Conditional Validation
Tags: angular, validation, form, typescript
Published: 2023-03-03 10:29:43

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](https://medium.com/ngx/3-ways-to-implement-conditional-validation-of-reactive-forms-c59ed6fc3325) - 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](https://twitter.com/Nartc1410) shared an article that I got some inspiration from this [one](https://medium.com/@ctran2428/angular-reactive-forms-custom-required-when-validator-935eb367cca3) - 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](https://angular-pxpqcx.stackblitz.io):

**conditional.component.ts**

```typescript
@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),
        ]),
      ],
    ],
  });
}
```

**conditional.component.html**

```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>
```

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**

```typescript
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;
  };
}
```

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