Brian Love
Angular + TypeScript Developer in Denver, CO

Dynamic FormGroup Custom Validation in Angular2

Reading time ~5 minutes

In this post I will show you how to create nested FormGroups to create a custom validator. This works well for related form inputs, such as checkboxes. If you are new to Angular 2 forms, check out Thoughtram’s post on reactive forms - it’s a good read.

Getting Started

To review Angular 2 has both model-driven forms as well as template-driven forms. I am using the model-driven reactive form. As such, I have imported the necessary classes from the @angular/forms module.

import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"

I have imported the FormBuilder, FormControl, FormGroup and Validators classes. A quick overview of these:

  • FormBuilder - this enables us to use some short-hand syntax for creating our FormGroups.
  • FormControl - this represents a single control (or HTMLFormElement).
  • FormGroup - this represents a group of form fields. We will also use the FormGroupDirective in our view.
  • Validators - this class provides some built-in validators that are used by our form controls.

Form Template

Before we get to the form template, I want to create an array of days of the week and store this as a public variable in my component. This is only because I am going to use the name and values when creating the checkboxes in my form. You could simply create the necessary input[type="checkbox"] elements in your template instead.

interface IDay {
  name: string;
  value: string;
}

@Component({})
export class UpdateComponent implements ngOnInit {
  public days: IDay[] = [
    {value: "sunday", name:"Sunday"},
    {value: "monday", name:"Monday"},
    {value: "tuesday", name:"Tuesday"},
    {value: "wednesday", name:"Wednesday"},
    {value: "thursday", name:"Thursday"},
    {value: "friday", name:"Friday"},
    {value: "saturday", name:"Saturday"},
  ];
}

Now, let’s look at the form template:

<form [formGroup]="detailsForm" novalidate>
  <input type="text" [formControl]="detailsForm.get('name')" [class.invalid]="!detailsForm.controls.name.valid">
  <div formGroupName="days">
    <p *ngFor="let day of days">
      <input type="checkbox" [formControlName]="day.value" [class.invalid]="!detailsForm.controls.days.valid">
      <label></label>
    </div>
  </div>
</form>

A couple of things to note:

  • The form uses the FormGroupDirective to specify the FormGroup variable in my controller.
  • Next, I have create a simple text input field. I bind the formControl directive to the “name” FormControl instance in my detailsForm. I am also going to add the “invalid” class when the value is not valid. This field will be required.
  • Next, I have created another FormGroup, this is within the detailsForm and is called days. This is defined using the FormGroupName directive.
  • Within the days FormGroup I am looping over the array of IDay objects using the NgFor directive.
  • Each checkbox input is wrapped in a paragraph.
  • The checkbox input uses the FormControlName directive to bind to the value string value within the IDay object. I have also specified a class of “invalid” that will be applied to each checkbox input when the group is invalid.
  • Last, I have a

Building a FormGroup

Back to our controller, we now need to create our FormGroup variable named detailsForm. The detailsForm will contain all of the details about the FormControls, any nested FormGroups, and their FormControls.

@Component({})
export class UpdateComponent implements ngOnInit {
  public detailsForm: FormGroup;

  constructor(private formBuilder: FormBuilder) { }

  public ngOnInit() {
    //create daysFormGroup using FormGroup long-hand syntax
    //this is so I can create a dynamic form from the array of IDay objects
    let daysFormGroup: FormGroup = new FormGroup({});
    for (let day of this.days) {
      let control: FormControl = new FormControl(day.value, Validators.required);
      daysFormGroup.addControl(day.value, control);
    }

    //create detailsForm FormGroup using FormBuilder's short-hand syntax
    this.detailsForm = this.formBuilder.group({
      name: ["", Validators.required],
      days: daysFormGroup
    });
  }
}

Let’s walk through this:

  • First, I define the detailsForm public variable that is of type FormGroup.
  • In my constructor function I inject the FormBuilder. This FormBuilder will be available as a private variable in my class. I will use this later to use the FormBuilder.group() short-hand syntax for creating a FormGroup.
  • In my ngOnInit method I create a daysFormGroup variable so that I can dynamically create the FormControls based on the days defined. I could have easily hard-coded these controls, as well as the checkbox inputs in my template. In this example I also wanted to show the ability to use dynamic forms in Angular 2.
  • Next, I create the detailsForm FormGroup using the FormBuilder. First, I have the name text input, which uses the required method in the Validators class to require a value. Then, I define the days FormGroup.

Create Custom Validator for FormGroup

Now we can create our custom validator for the daysOfWeek FormGroup. I am going to create a new private method in my UpdateComponent named validateDays(). This method will be passed the FormGroup.

/**
 * Validate the days checkboxes. At least one must be selected.
 *
 * @class UpdateComponent
 * @method validateDays
 * @return {null|Object} Null if valid.
 */
private validateDays(formGroup: FormGroup) {
  for (let key in formGroup.controls) {
    if (formGroup.controls.hasOwnProperty(key)) {
      let control: FormControl = <FormControl>formGroup.controls[key];
      if (control.value) {
        return null;
      }
    }
  }

  return {
    validateDays: {
      valid: false
    }
  };
}

The validateDays method loops over each control in the FormGroup and checks if the control has a value. If any of the control’s have a value, then the method returns null - the FormGroup is valid. Otherwise, we return an object specifying that the validateDays validator is not valid.

The last thing we need to do is tell our daysFormGroup instance that we have a validator. So, let’s modify our ngOnInit() method:

@Component({})
export class UpdateComponent implements ngOnInit {
  public ngOnInit() {
    //create daysFormGroup using FormGroup long-hand syntax
    //this is so I can create a dynamic form from the array of IDay objects
    let daysFormGroup: FormGroup = new FormGroup({}, (formGroup: FormGroup) => {
      return this.validateDays(formGroup);
    });
  }
}

I am now specifying the validator function for the FormGroup. Unfortunately, it doesn’t look like there is a way to set the validator after creating the FormGroup instance.

Using FormBuilder

You can also set the validator when using FormBuilder using the following syntax:

@Component({})
export class UpdateComponent implements ngOnInit {
  public ngOnInit() {
    //create detailsForm FormGroup using FormBuilder's short-hand syntax
    this.detailsForm = this.formBuilder.group({
      name: ["", Validators.required],
      days: this.formBuilder.group({
        sunday: false,
        monday: false,
        tuesday: false,
        wednesday: false,
        thursday: false,
        friday: false,
        saturday: false
      }, {
        validator: (formGroup: FormGroup) => {
          return this.validateDays(formGroup);
        }
      })
    });
  }
}

In this example I am not dynamically creating my FormGroup, rather I have specified each control in my form (each day of the week). I then pass in the extras object as the second argument, which has a property named validator, which is my validator function. I am using TypeScript’s fat arrow function to bind the scope of the anonymous function to the class’s this scope.

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).