Angular 2+ Reactive Form - Dynamic Required Fields

May 13, 2018

Complicated Problem 1: I need dynamic required fields

Let’s say we only want the subcategories field to be required if categories has a value.

<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="category">Category</label>
    <div>
      <ng-container *ngFor="let category of categories">
        <div class="form-check form-check-inline">
          <input class="form-check-input" type="radio" [value]="category.id" formControlName="category">
          <label class="form-check-label">{{category.name}}</label>
        </div>
      </ng-container>
    </div>
  </div>
  <div class="form-group" *ngIf="recipeForm.controls.category.value">
    <label for="category">Subcategory</label>
    <my-checkboxes formControlName="subcategory" [data]="subcategories"></my-checkboxes>
  </div>
</form>

Initially we want our subcategory control to be un-required.

this.recipeForm = this.fb.group({
  name: [null, Validators.required],
  description: [null],
  source: [null],
  url: [null],
  category: [null],
  subcategory: [[]],
  ingredients: this.fb.array([])
});

First, we’ll subscribe to changes on the field that determines other fields’ requiredness

onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
  });
}

Next, we’ll get the subcategory fields control and use setValidators method.

onChanges() {
  this.recipeForm.get('category').valueChanges.subscribe(val => {
    const subCategoryControl = this.recipeForm.get('subcategory');
    if (val) {
      subCategoryControl.setValidators(Validators.required);
    }
    else {
      subCategoryControl.setValidators(null);
    }
  });
}

Finally, we’ll update the changed fields validity so the user is aware of the updated changes.

  onChanges() {
    this.recipeForm.get('category').valueChanges.subscribe(val => {
      const subCategoryControl = this.recipeForm.get('subcategory');
      if (val) {
        subCategoryControl.setValidators(Validators.required);
        subCategoryControl.updateValueAndValidity();
      }
      else {
        subCategoryControl.setValidators(null);
        subCategoryControl.updateValueAndValidity();
      }
    });
  }

But what if we wanted to preselect an option in our dependent field based on data from another field? This is a common requirement pattern we see in more complicated forms, setting options based on previous input selections. Here it’s NBD, we can just use patchValue.

  onChanges() {
    this.recipeForm.get('category').valueChanges.subscribe(val => {
      const subCategoryControl = this.recipeForm.get('subcategory');
      subCategoryControl.patchValue('my prepopulated data here');
    });
  }

How about some validation then?

Avril Lavigne

Angular validation classes:

  .ng-valid
  .ng-invalid
  .ng-pending
  .ng-pristine
  .ng-dirty
  .ng-untouched
  .ng-touched

We’ll let Angular do the heavy lifting here.

Avril Lavigne

I like to highlight as the user touches fields, but then highlight required untouched fields on save.

  .ng-valid[required], .ng-valid.required  {
  	border-left: 5px solid #42A948; /* green */
  }
  .ng-touched.ng-invalid:not(form)  {
  	border-left: 5px solid #a94442; /* red */
  }

This means we need to highlight invalid untouched fields on save - we can do this easily by marking them as touched. Nothing too crazy here, we’re just mapping over the keys of our FormControl object to get each control and mark it as touched.

  Object.keys(this.recipeForm.controls).forEach(field => {
    const control = this.recipeForm.get(field);
    control.markAsTouched({ onlySelf: true });
  })

Demo code available at https://github.com/tehfedaykin/complicated-forms-app