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 ourFormGroups.FormControl- this represents a single control (orHTMLFormElement).FormGroup- this represents a group of form fields. We will also use theFormGroupDirectivein 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
FormGroupDirectiveto specify theFormGroupvariable in my controller. - Next, I have create a simple text input field. I bind the formControl directive to the “name”
FormControlinstance 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 thedetailsFormand is calleddays. This is defined using the FormGroupName directive. - Within the
daysFormGroupI am looping over the array ofIDayobjects using theNgFordirective. - Each checkbox input is wrapped in a paragraph.
- The checkbox input uses the
FormControlNamedirective to bind to thevaluestring value within theIDayobject. 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
detailsFormpublic variable that is of typeFormGroup. - In my constructor function I inject the
FormBuilder. ThisFormBuilderwill 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
ngOnInitmethod I create adaysFormGroupvariable so that I can dynamically create theFormControls based on thedaysdefined. 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
detailsFormFormGroupusing theFormBuilder. First, I have thenametext input, which uses therequiredmethod in theValidatorsclass 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.