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 ourFormGroup
s.FormControl
- this represents a single control (orHTMLFormElement
).FormGroup
- this represents a group of form fields. We will also use theFormGroupDirective
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>{{day.name}}</label>
</div>
</div>
</form>
A couple of things to note:
- The form uses the
FormGroupDirective
to specify theFormGroup
variable in my controller. - Next, I have create a simple text input field. I bind the formControl directive to the “name”
FormControl
instance in mydetailsForm
. 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 thedetailsForm
and is calleddays
. This is defined using the FormGroupName directive. - Within the
days
FormGroup
I am looping over the array ofIDay
objects using theNgFor
directive. - Each checkbox input is wrapped in a paragraph.
- The checkbox input uses the
FormControlName
directive to bind to thevalue
string value within theIDay
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 FormControl
s, any nested FormGroup
s, and their FormControl
s.
@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 typeFormGroup
. - In my constructor function I inject the
FormBuilder
. ThisFormBuilder
will be available as a private variable in my class. I will use this later to use theFormBuilder.group()
short-hand syntax for creating aFormGroup
. - In my
ngOnInit
method I create adaysFormGroup
variable so that I can dynamically create theFormControl
s based on thedays
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 theFormBuilder
. First, I have thename
text input, which uses therequired
method in theValidators
class to require a value. Then, I define the daysFormGroup
.
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.