Web Front-end25+ minute read

Angular 4 Forms: Validation and Nesting

Validating user inputs is an essential part of any robust web application. Angular 4 makes it especially easy for both template-driven and reactive forms.

In this article, Toptal Freelance Angular Developer Igor Geshoski walks us through the different approaches in Angular 4 form validation and shows how even complex form validation can be done easily.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Validating user inputs is an essential part of any robust web application. Angular 4 makes it especially easy for both template-driven and reactive forms.

In this article, Toptal Freelance Angular Developer Igor Geshoski walks us through the different approaches in Angular 4 form validation and shows how even complex form validation can be done easily.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Igor Geshoski
Verified Expert in Engineering
10 Years of Experience

Igor has been honing his algorithms and problem solving skills on large-scale Java based enterprise applications for about five years.

Read More
Share

On the web, some of the earliest user input elements were a button, checkbox, text input, and radio buttons. To this very day, these elements are still used in modern web applications even though the HTML standard has come a long way from its early definition and now allows all sorts of fancy interactions.

Validating user inputs is an essential part of any robust web application.

Forms in Angular applications can aggregate the state of all inputs that are under that form and provide an overall state like the validation status of the full form. This can come really handy to decide if the user input will be accepted or rejected without checking each input separately. Note that Angular input validation without <form> falls outside the scope of this article.

In this article, you will learn how you can work with forms and perform form validation with ease in your Angular application.

There are two types of forms in Angular: template-driven and reactive forms. We will go through each form type by using the same example to see how the same things can be implemented in a different ways. Later, in the article, we will look at a novel approach on how to set up and work with Angular nested form validation.

Angular 4 Forms

Angular 4 supports these commonly-used Angular form statuses:

  • valid – state of the validity of all form controls, true if all controls are valid

  • invalid – inverse of valid; true if some control is invalid

  • pristine – gives a status about the “cleanness” of the form; true if no control was modified

  • dirty – inverse of pristine; true if some control was modified

Let’s take a look at a basic example of a form:

<form>
  <div>
    <label>Name</label>
    <input type="text" name="name"/>
  </div>

  <div>
    <label>Birth Year</label>
    <input type="text" name="birthYear"/>
  </div>

  <div>
    <h3>Location</h3>
    <div>
      <label>Country</label>
      <input type="text" name="country"/>
    </div>
    <div>
      <label>City</label>
      <input type="text" name="city"/>
    </div>
  </div>

  <div>
    <h3>Phone numbers</h3>
    <div>
      <label>Phone number 1</label>
      <input type="text" name="phoneNumber[1]"/>
      <button type="button">remove</button>
    </div>
    <button type="button">Add phone number</button>
  </div>

  <button type="submit">Register</button>
  <button type="button">Print to console</button>
</form>

The specification for this example is the following:

  • name - is required and unique among all registered users

  • birthYear - should be a valid number and the user must have at least 18 and less than 85 years

  • country - is mandatory, and just to make things a bit complicated, we need a validation that if the country is France, then the city must be Paris (let’s say that our service is offered only in Paris)

  • phoneNumber – each phone number must follow a specified pattern, there must be at least one phone number, and the user is allowed to add a new or remove an existing telephone number.

  • The “Register” button is enabled only if all inputs are valid and, once clicked, it submits the form.

  • The “Print to Console” just prints the value of all inputs to console when clicked.

The ultimate goal is to fully implement the specification defined.

Template-driven Forms

Template-driven forms are very similar to the forms in AngularJS (or Angular 1, as some refer to it). So, someone who has worked with forms in AngularJS will be very familiar with this approach to working with forms.

With the introduction of modules in Angular 4, it is enforced that each specific type of form is in a separate module and we must explicitly define which type are we going to use by importing the proper module. That module for the template-driven forms is FormsModule. That being said, you can activate the template-driven forms as following:

import {FormsModule} from '@angular/forms'
import {NgModule} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {AppComponent} from 'src/app.component';

@NgModule({
  imports: [ BrowserModule, FormsModule ],
  declarations: [ AppComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

As presented in this code-snippet, we first must import the browser module as it “provides services that are essential to launch and run a browser app.” (from the Angular 4 docs). Then we import the required FormsModule to activate the template-driven forms. And last is the declaration of the root component, AppComponent, where in the next steps we will implement the form.

Bear in mind that in this example and the following examples, you must make sure that the app is properly bootstrapped using the platformBrowserDynamic method.

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app.module';

platformBrowserDynamic().bootstrapModule(AppModule);

We can assume that our AppComponent (app.component.ts) looks something like this:

import {Component} from '@angular/core'

@Component({
  selector: 'my-app',
  templateUrl: 'src/app.component.tpl.html'
})
export class AppComponent {

}

Where the template of this component is in the app.component.tpl.html and we can copy the initial template to this file.

Notice that each input element must have the name attribute to be properly identified within the form. Although this seems like a simple HTML form, we have already defined an Angular 4 supported form (maybe you don’t see it yet). When the FormsModule is imported, Angular 4 automatically detects a form HTML element and attaches the NgForm component to that element (by the selector of the NgForm component). That is the case in our example. Although this Angular 4 form is declared, at this point it doesn’t know of any Angular 4 supported inputs. Angular 4 is not that invasive to register each input HTML element to the nearest form ancestor.

The key that allows an input element to be noticed as an Angular 4 element and registered to the NgForm component is the NgModel directive. So, we can extend the app.component.tpl.html template as following:

<form>
  ..
  <input type="text" name="name" ngModel>
  ..
  <input type="text" name="birthYear" ngModel >
  ..
  <input type="text" name="country" ngModel/>
  ..
  <input type="text" name="city" ngModel/>
  ..
  <input type="text" name="phoneNumber[1]" ngModel/>
</form>

By adding the NgModel directive, all inputs are registered to the NgForm component. With this, we have defined a fully working Angular 4 form and so far, so good, but we still don’t have a way to access the NgForm component and the functionalities that it offers. The two main functionalities offered by NgForm are:

  • Retrieving the values of all registered input controls

  • Retrieving the overall state of all controls

To expose the NgForm, we can add the following to the <form> element:

<form #myForm="ngForm">
  ..
</form>

This is possible thanks to the exportAs property of the Component decorator.

Once this is done, we can access the values of all input controls and extend the template to:

<form #myForm="ngForm">
  ..
  <pre>{{myForm.value | json}}</pre>
</form>

With myForm.value we are accessing JSON data containing the values of all registered inputs, and with {{myForm.value | json}}, we are pretty-printing the JSON with the values.

What if we want to have a sub-group of inputs from a specific context wrapped in a container and separate object in the values JSON e.g., location containing country and city or the phone numbers? Don’t stress out—template-driven forms in Angular 4 have that covered as well. The way to achieve this is by using the ngModelGroup directive.

<form #myForm="ngForm">
  ..
  <div ngModelGroup="location">
    ..
  </div>
  </div ngModelGroup="phoneNumbers">
  ..
  <div>
    ..
</form>

What we lack now is a way to add multiple phone numbers. The best way to do this would have been to use an array, as the best representation of an iterable container of multiple objects, but at the moment of writing this article, that feature is not implemented for the template-driven forms. So, we have to apply a workaround to make this work. The phone numbers section needs to be updated as following:

<div ngModelGroup="phoneNumbers">
  <h3>Phone numbers</h3>
  <div *ngFor="let phoneId of phoneNumberIds; let i=index;">
    <label>Phone number {{i + 1}}</label>
    <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel/>
    <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button>
  </div>
  <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button>
</div>

The myForm.control.markAsTouched() is used to make the form touched so we can display the errors at that moment. The buttons don’t activate this property when clicked, only the inputs. To make the next examples more clear, I will not add this line on the click handler for add() and remove(). Just imagine it is there. (It is present in the Plunkers.)

We also need to update AppComponent to contain the following code:

private count:number = 1;

phoneNumberIds:number[] = [1];

remove(i:number) {
  this.phoneNumberIds.splice(i, 1);
}

add() {
  this.phoneNumberIds.push(++this.count);
}

We must store a unique ID for each new phone number added, and in the *ngFor, track the phone number controls by their id (I admit it is not very nice, but until the Angular 4 team implements this feature, I’m afraid, it is the best we can do)

Okay, what do we have so far, we’ve added the Angular 4 supported form with inputs, added a specific grouping of the inputs (location and phone numbers) and exposed the form within the template. But what if we’d like to access the NgForm object in some method in the component? We’ll have a look at two ways of doing this.

For the first way, the NgForm, labeled myForm in the current example, can be passed as an argument to the function that will serve as a handler for the onSubmit event of the form. For better integration, the onSubmit event is wrapped by an Angular 4, NgForm-specific event named ngSubmit, and this is the right way to go if we want to execute some action on submit. So, the example now will look like this:

<form #myForm="ngForm" (ngSubmit)="register(myForm)">
  …
</form>

We must have a corresponding method register, implemented in the AppComponent. Something like:

register (myForm: NgForm) {
  console.log('Successful registration');
  console.log(myForm);
}

This way, by leveraging the onSubmit event, we have access to the NgForm component only when submit is executed.

The second way is to use a view query by adding the @ViewChild decorator to a property of the component.

@ViewChild('myForm')
private myForm: NgForm;

With this approach, we are allowed access to the form regardless if the onSubmit event was fired or not.

Great! Now we have a fully functioning Angular 4 form with access to the form in the component. But, do you notice something missing? What if the user enters something like “this-is-not-a-year” in the “years” input? Yeah, you got it, we are lacking Angular form validation of the inputs and we will cover that in the following section.

Angular 4 Forms Validation

Validation is really important for each application. We always want to validate the user input (we cannot trust the user) to prevent sending/saving invalid data and we must show some meaningful message about the error to properly guide the user to enter valid data.

For some validation rule to be enforced on some input, the proper validator must be associated with that input. Angular 4 already offers a set of common validators like: required, maxLength, minLength

So, how can we associate a validator with an input? Well, pretty easy; just add the validator directive to the control:

<input name="name" ngModel required/>

This example makes the “name” input mandatory. Let’s add some validations to all inputs in our example.

<form #myForm="ngForm" (ngSubmit)="actionOnSubmit(myForm)" novalidate>
  <p>Is "myForm" valid? {{myForm.valid}}</p>
  ..
  <input type="text" name="name" ngModel required/>
  ..
  <input type="text" name="birthYear" ngModel required pattern="\\d{4,4}"/>
  ..
  <div ngModelGroup="location">
    ..
    <input type="text" name="country" ngModel required/>
    ..
    <input type="text" name="city" ngModel/>
  </div>

  <div ngModelGroup="phoneNumbers">
    ..
    <input type="text" name="phoneNumber[{{phoneId}}]" ngModel required/>
    ..
  </div>
  ..
</form>

Note: novalidate is used to disable the browser’s native form validation.

We’ve made the “name” required, the “years” field is required and must consist of only numbers, the country input is required and also the phone number is required. Also, we print the status of the validity of the form with {{myForm.valid}}.

An improvement to this example would be to also show what is wrong with the user input (not just show the overall state). Before we continue to adding additional validation, I would like to implement a helper component that will allow us to print all the errors for a provided control.

// show-errors.component.ts
import { Component, Input } from '@angular/core';
import { AbstractControlDirective, AbstractControl } from '@angular/forms';

@Component({
 selector: 'show-errors',
 template: `
   <ul *ngIf="shouldShowErrors()">
     <li style="color: red" *ngFor="let error of listOfErrors()">{{error}}</li>
   </ul>
 `,
})
export class ShowErrorsComponent {

 private static readonly errorMessages = {
   'required': () => 'This field is required',
   'minlength': (params) => 'The min number of characters is ' + params.requiredLength,
   'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength,
   'pattern': (params) => 'The required pattern is: ' + params.requiredPattern,
   'years': (params) => params.message,
   'countryCity': (params) => params.message,
   'uniqueName': (params) => params.message,
   'telephoneNumbers': (params) => params.message,
   'telephoneNumber': (params) => params.message
 };

 @Input()
 private control: AbstractControlDirective | AbstractControl;

 shouldShowErrors(): boolean {
   return this.control &&
     this.control.errors &&
     (this.control.dirty || this.control.touched);
 }

 listOfErrors(): string[] {
   return Object.keys(this.control.errors)
     .map(field => this.getMessage(field, this.control.errors[field]));
 }

 private getMessage(type: string, params: any) {
   return ShowErrorsComponent.errorMessages[type](params);
 }

}

The list with errors is shown only if there are some existing errors and the input is touched or dirty.

The message for each error is looked up in a map of predefined messages errorMessages (I’ve added all messages up front).

This component can be used as follows:

<div>
  <label>Birth Year</label>
  <input type="text" name="birthYear" #birthYear="ngModel" ngModel required pattern="\\d{4,4}"/>
  <show-errors [control]="birthYear"></show-errors>
</div>

We need to expose the NgModel for each input and pass it to the component that renders all errors. You can notice that in this example we have used a pattern to check if the data is a number; what if the user enters “0000”? This would be an invalid input. Also, we are missing the validators for a unique name, the strange restriction of the country (if country=’France’, then city must be ‘Paris’), pattern for a correct phone number and the validation that at least one phone number exists. This is the right time to have a look at custom validators.

Angular 4 offers an interface that each custom validator must implement, the Validator interface (what a surprise!). The Validator interface basically looks like this:

export interface Validator {
   validate(c: AbstractControl): ValidationErrors | null;
   registerOnValidatorChange?(fn: () => void): void;
}

Where each concrete implementation MUST implement the ‘validate’ method. This validate method is really interesting on what can be received as input, and what should be returned as output. The input is an AbstractControl, which means that the argument can be any type that extends AbstractControl (FormGroup, FormControl and FormArray). The output of the validate method should be null or undefined (no output) if the user input is valid, or return a ValidationErrors object if the user input is invalid. With this knowledge, now we will implement a custom birthYear validator.

import { Directive } from '@angular/core';
import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms';


@Directive({
 selector: '[birthYear]',
 providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}]
})
export class BirthYearValidatorDirective implements Validator {

 validate(c: FormControl): ValidationErrors {
   const numValue = Number(c.value);
   const currentYear = new Date().getFullYear();
   const minYear = currentYear - 85;
   const maxYear = currentYear - 18;
   const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear;
   const message = {
     'years': {
       'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear
     }
   };
   return isValid ? null : message;
 }
}

There are a few things to explain here. First you may notice that we implemented the Validator interface. The validate method checks if the user is between 18 and 85 years old by the birth year entered. If the input is valid, then null is returned, or else an object containing the validation message is returned. And the last and most important part is declaring this directive as a Validator. That is done in the “providers” parameter of the @Directive decorator. This validator is provided as one value of the multi-provider NG_VALIDATORS. Also, don’t forget to declare this directive in the NgModule. And now we can use this validator as following:

<input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>

Yeah, as simple as that!

For the telephone number, we can validate the format of the phone number like this:

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms';


@Directive({
 selector: '[telephoneNumber]',
 providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}]
})
export class TelephoneNumberFormatValidatorDirective implements Validator {

 validate(c: FormControl): ValidationErrors {
   const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value);
   const message = {
     'telephoneNumber': {
       'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)'
     }
   };
   return isValidPhoneNumber ? null : message;
 }
}

Now come the two validations, for the country and the number of telephone numbers. Notice something common for both of them? Both require more than one control to perform proper validation. Well, you remember the Validator interface, and what we said about it? The argument of the validate method is AbstractControl, which can be a user input or the form itself. This creates the opportunity to implement a validator that uses multiple controls to determine the concrete validation status.

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms';


@Directive({
 selector: '[countryCity]',
 providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}]
})
export class CountryCityValidatorDirective implements Validator {

 validate(form: FormGroup): ValidationErrors {
   const countryControl = form.get('location.country');
   const cityControl = form.get('location.city');

   if (countryControl != null && cityControl != null) {
     const country = countryControl.value;
     const city = cityControl.value;
     let error = null;

     if (country === 'France' && city !== 'Paris') {
       error = 'If the country is France, the city must be Paris';
     }

     const message = {
       'countryCity': {
         'message': error
       }
     };

     return error ? message : null;
   }
 }
}

We’ve implemented a new validator, country-city validator. You can notice that now as an argument the validate method receives a FormGroup and from that FormGroup we can retrieve the inputs required for validation. The rest of the things are very similar to the single input validator.

The validator for the number of phone numbers will look like this:

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors, FormControl } from '@angular/forms';


@Directive({
 selector: '[telephoneNumbers]',
 providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}]
})
export class TelephoneNumbersValidatorDirective implements Validator {

 validate(form: FormGroup): ValidationErrors {

   const message = {
     'telephoneNumbers': {
       'message': 'At least one telephone number must be entered'
     }
   };

   const phoneNumbers = <FormGroup> form.get('phoneNumbers');
   const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0;

   return hasPhoneNumbers ? null : message;
 }
}

We can use them like this:

<form #myForm="ngForm" countryCity telephoneNumbers>
  ..
</form>

Same as the input validators, right? Just now applied to the form.

Do you remember the ShowErrors component? We implemented it to work with an AbstractControlDirective, meaning we could reuse it to show all errors associated directly with this form as well. Keep in mind that at this point the only directly associated validation rules with the form are the Country-city and Telephone numbers (the other validators are associated with the specific form controls). To print out all form errors, just do the following:

<form #myForm="ngForm" countryCity telephoneNumbers >
  <show-errors [control]="myForm"></show-errors>
  ..
</form>

The last thing left is the validation for a unique name. This is a bit different; to check if the name is unique, most probably a call to the back-end is needed to check all existing names. This classifies as an asynchronous operation. For this purpose, we can reuse the previous technique for custom validators, just make the validate return an object that will be resolved sometime in the future (promise or an observable). In our case, we will use a promise:

import { Directive } from '@angular/core';
import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms';


@Directive({
 selector: '[uniqueName]',
 providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}]
})
export class UniqueNameValidatorDirective implements Validator {

 validate(c: FormControl): ValidationErrors {
   const message = {
     'uniqueName': {
       'message': 'The name is not unique'
     }
   };

   return new Promise(resolve => {
     setTimeout(() => {
       resolve(c.value === 'Existing' ? message : null);
     }, 1000);
   });
 }
}

We are waiting for 1 second and then returning a result. Similar to the sync validators, if the promise is resolved with null, that means that the validation passed; if the promise is resolved with anything else, then the validation failed. Also notice that now this validator is registered to another multi-provider, the NG_ASYNC_VALIDATORS. One useful property of the forms regarding the async validators is the pending property. It can be used like this:

<button [disabled]="myForm.pending">Register</button>

It will disable the button until the async validators are resolved.

Here’s a Plunker containing the complete AppComponent, the ShowErrors component, and all Validators.

With these examples, we’ve covered most of the cases for working with template-driven forms. We’ve shown that template-driven forms are really similar to the forms in AngularJS (it will be really easy for AngularJS developers to migrate). With this type of form, it is quite easy to integrate Angular 4 forms with minimal programming, mainly with manipulations in the HTML template.

Reactive Forms

The reactive forms were also known as “model-driven” forms, but I like calling them “programmatic” forms, and soon you’ll see why. The reactive forms are a new approach towards the support of Angular 4 forms, so unlike the template-driven, AngularJS developers will not be familiar with this type.

We can start now, remember how the template-driven forms had a special module? Well, the reactive forms also have their own module, called ReactiveFormsModule and must be imported to activate this type of forms.

import {ReactiveFormsModule} from '@angular/forms'
import {NgModule} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'

import {AppComponent} from 'src/app.component';

@NgModule({
  imports: [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Also, don’t forget to bootstrap the application.

We can start off with the same AppComponent and template as in the previous section.

At this point, if the FormsModule is not imported (and please make sure that it isn’t), we have just a regular HTML form element with a couple of form controls, no Angular magic here.

We come to the point where you will notice why I like to call this approach “programmatic.” In order to enable Angular 4 forms, we must declare the FormGroup object manually and populate it with controls like this:

import { FormGroup, FormControl, FormArray, NgForm } from '@angular/forms';
import { Component, OnInit } from '@angular/core';

@Component({
 selector: 'my-app',
 templateUrl: 'src/app.component.html'
})
export class AppComponent implements OnInit {

 private myForm: FormGroup;

 constructor() {
 }

 ngOnInit() {
   this.myForm = new FormGroup({
     'name': new FormControl(),
     'birthYear': new FormControl(),
     'location': new FormGroup({
       'country': new FormControl(),
       'city': new FormControl()
     }),
     'phoneNumbers': new FormArray([new FormControl('')])
   });
 }

 printMyForm() {
   console.log(this.myForm);
 }

 register(myForm: NgForm) {
   console.log('Registration successful.');
   console.log(myForm.value);
 }

}

The printForm and register methods are the same from the previous examples, and will be used in the next steps. The key types used here are FormGroup, FormControl and FormArray. These three types are all that we need to create a valid FormGroup. The FormGroup is easy; it is a simple container of controls. The FormControl is also easy; it is any control (e.g., input). And last, the FormArray is the piece of the puzzle that we were missing in the template-driven approach. The FormArray allows for maintaining a group of controls without specifying a concrete key for each control, basically an array of controls (seems like the perfect thing for the phone numbers, right?).

When constructing any of these three types, remember this rule of 3’s. The constructor for each type receives three arguments—value, validator or list of validators, and async validator or list of async validators, defined in code:

constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);

For FormGroup, the value is an object where each key represents the name of a control and the value is the control itself.

For FormArray, the value is an array of controls.

For FormControl, the value is the initial value or the initial state (object containing a value and a disabled property) of the control.

We’ve created the FormGroup object, but the template is still not aware of this object. The linking between the FormGroup in the component and the template is done with four directives: formGroup, formControlName, formGroupName, and formArrayName, used like this:

<form [formGroup]="myForm" (ngSubmit)="register(myForm)">
  <div>
    <label>Name</label>
    <input type="text" name="name" formControlName="name">
  </div>

  <div>
    <label>Birth Year</label>
    <input type="text" name="birthYear" formControlName="birthYear">
  </div>

  <div formGroupName="location">
    <h3>Location</h3>
    <div>
      <label>Country</label>
      <input type="text" name="country" formControlName="country">
    </div>
    <div>
      <label>City</label>
      <input type="text" name="city" formControlName="city">
    </div>
  </div>

  <div formArrayName="phoneNumbers">
    <h3>Phone numbers</h3>
    <div *ngFor="let phoneNumberControl of myForm.controls.phoneNumbers.controls; let i=index;">
      <label>Phone number {{i + 1}}</label>
      <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i">
      <button type="button" (click)="remove(i)">remove</button>
    </div>
    <button type="button" (click)="add()">Add phone number</button>
  </div>

  <pre>{{myForm.value | json}}</pre>
  <button type="submit">Register</button>
  <button type="button" (click)="printMyForm()">Print to console</button>
</form>

Now that we have the FormArray, you can see that we can use that structure for rendering all phone numbers.

And now to add the support for adding and removing phone numbers (in the component):

remove(i: number) {
 (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i);
}

add() {
 (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl(''));
}

Now, we have a fully functioning Angular 4 reactive form. Notice the difference from the template-driven forms where the FormGroup was “created in the template” (by scanning the template structure) and passed to the component, in the reactive forms it is the other way around, the complete FormGroup is created in the component, then “passed to the template” and linked with the corresponding controls. But, again we have the same issue with the validation, an issue that will be resolved in the next section.

Validation

When it comes to validation, the reactive forms are much more flexible than the template-driven forms. With no additional changes, we can reuse the same validators that were implemented previously (for the template-driven). So, by adding the validator directives, we can activate the same validation:

<form [formGroup]="myForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate>

  <input type="text" name="name" formControlName="name" required uniqueName>
  <show-errors [control]="myForm.controls.name"></show-errors>
  ..
  <input type="text" name="birthYear" formControlName="birthYear" required birthYear>
  <show-errors [control]="myForm.controls.birthYear"></show-errors>
  ..
  <div formGroupName="location">
    ..
    <input type="text" name="country" formControlName="country" required>
    <show-errors [control]="myForm.controls.location.controls.country"></show-errors>
    ..
    <input type="text" name="city" formControlName="city">
    ..
  </div>

  <div formArrayName="phoneNumbers">
    <h3>Phone numbers</h3>
    ..
    <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i" required telephoneNumber>
    <show-errors [control]="phoneNumberControl"></show-errors>
    ..
  </div>
  ..
</form>

Keep in mind that now we don’t have the NgModel directive to pass to the ShowErrors component, but the complete FormGroup is already constructed and we can pass the correct AbstractControl for retrieving the errors.

Here’s a full working Plunker with this type of validation for reactive-forms.

But it wouldn’t be fun if we just reused the validators, right? We will have a look at how to specify the validators when creating the form group.

Remember the “3s rule” rule that we mentioned about the constructor for FormGroup, FormControl, and FormArray? Yes, we said that the constructor can receive validator functions. So, let’s try that approach.

First, we need to extract the validate functions of all validators into a class exposing them as static methods:

import { FormArray, FormControl, FormGroup, ValidationErrors } from '@angular/forms';

export class CustomValidators {

 static birthYear(c: FormControl): ValidationErrors {
   const numValue = Number(c.value);
   const currentYear = new Date().getFullYear();
   const minYear = currentYear - 85;
   const maxYear = currentYear - 18;
   const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear;
   const message = {
     'years': {
       'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear
     }
   };
   return isValid ? null : message;
 }

 static countryCity(form: FormGroup): ValidationErrors {
   const countryControl = form.get('location.country');
   const cityControl = form.get('location.city');

   if (countryControl != null && cityControl != null) {
     const country = countryControl.value;
     const city = cityControl.value;
     let error = null;

     if (country === 'France' && city !== 'Paris') {
       error = 'If the country is France, the city must be Paris';
     }

     const message = {
       'countryCity': {
         'message': error
       }
     };

     return error ? message : null;
   }
 }

 static uniqueName(c: FormControl): Promise<ValidationErrors> {
   const message = {
     'uniqueName': {
       'message': 'The name is not unique'
     }
   };

   return new Promise(resolve => {
     setTimeout(() => {
       resolve(c.value === 'Existing' ? message : null);
     }, 1000);
   });
 }

 static telephoneNumber(c: FormControl): ValidationErrors {
   const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value);
   const message = {
     'telephoneNumber': {
       'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)'
     }
   };
   return isValidPhoneNumber ? null : message;
 }

 static telephoneNumbers(form: FormGroup): ValidationErrors {

   const message = {
     'telephoneNumbers': {
       'message': 'At least one telephone number must be entered'
     }
   };

   const phoneNumbers = <FormArray>form.get('phoneNumbers');
   const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0;

   return hasPhoneNumbers ? null : message;
 }
}

Now we can change the creation of ‘myForm’ to:

this.myForm = new FormGroup({
    'name': new FormControl('', Validators.required, CustomValidators.uniqueName),
    'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]),
    'location': new FormGroup({
      'country': new FormControl('', Validators.required),
      'city': new FormControl()
    }),
    'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()])
  },
  Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers])
);

See? The rule of “3s,” when defining a FormControl, multiple validators can be declared in an array, and if we want to add multiple validators to a FormGroup they must be “merged” using Validators.compose (also Validators.composeAsync is available). And, that’s it, validation should be working completely. There’s a Plunker for this example as well.

This goes out to everybody that hates the “new” word. For working with the reactive forms, there’s a shortcut provided—a builder, to be more precise. The FormBuilder allows creating the complete FormGroup by using the “builder pattern.” And that can be done by changing the FormGroup construction like this:

constructor(private fb: FormBuilder) {
}

ngOnInit() {
  this.myForm = this.fb.group({
      'name': ['', Validators.required, CustomValidators.uniqueName],
      'birthYear': ['', [Validators.required, CustomValidators.birthYear]],
      'location': this.fb.group({
        'country': ['', Validators.required],
        'city': ''
      }),
      'phoneNumbers': this.fb.array([this.buildPhoneNumberComponent()])
    },
    {
      validator: Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers])
    }
  );
}

Not a very big improvement from the instantiation with “new,” but there it is. And, don’t worry, there’s a Plunker for this also.

In this second section, we had a look at reactive forms in Angular 4. As you may notice, it is a completely new approach towards adding support for forms. Even though it seems verbose, this approach gives the developer total control over the underlying structure that enables forms in Angular 4. Also, since the reactive forms are created manually in the component, they are exposed and provide an easy way to be tested and controlled, while this was not the case with the template-driven forms.

Nesting Forms

Nesting forms is in some cases useful and a required feature, mainly when the state (e.g., validity) of a sub-group of controls needs to determined. Think about a tree of components; we might be interested in the validity of a certain component in the middle of that hierarchy. That would be really hard to achieve if we had a single form at the root component. But, oh boy, it is a sensitive manner on a couple of levels. First, nesting real HTML forms, according to the HTML specification, is not allowed. We might try to nest <form> elements. In some browsers it might actually work, but we cannot be sure that it will work on all browsers, since it is not in the HTML spec. In AngularJS, the way to work around this limitation was to use the ngForm directive, which offered the AngularJS form functionalities (just grouping of the controls, not all form capabilities like posting to the server) but could be placed on any element. Also, in AngularJS, nesting of forms (when I say forms, I mean NgForm) was available out of the box. Just by declaring a tree of couple of elements with the ngForm directive, the state of each form was propagated upwards to the root element.

In the next section, we will have a look at a couple options on how to nest forms. I like to point out that we can differentiate two types of nesting: within the same component and across different components.

Nesting within the Same Component

If you take a look at the example that we implemented with the template-driven and the reactive approach, you will notice that we have two inner containers of controls, the “location” and the “phone numbers.” To create that container, to store the values in a separate property object, we used the NgModelGroup, FormGroupName, and the FormArrayName directives. If you have a good look at the definition of each directive, you may notice that each one of them extends the ControlContainer class (directly or indirectly). Well, what do you know, it turns out this is enough to provide the functionality that we require, wrapping up the state of all inner controls and propagating that state to the parent.

For the template-driven form, we need to do the following changes:

<form #myForm="ngForm" (ngSubmit)="register(myForm)" novalidate>
  ..
  <div ngModelGroup="location" #location="ngModelGroup" countryCity>
    ..
    <show-errors [control]="location"></show-errors>
  </div>

  <div ngModelGroup="phoneNumbers" #phoneNumbers="ngModelGroup" telephoneNumbers>
    ..
    <show-errors [control]="phoneNumbers"></show-errors>
  </div>

</form>

We added the ShowErrors component to each group, to show the errors directly associated with that group only. Since we moved the countryCity and telephoneNumbers validators to a different level, we also need to update them appropriately:

// country-city-validator.directive.ts
let countryControl = form.get('country');
let cityControl = form.get('city');

And telephone-numbers-validator.directive.ts to:

let phoneNumbers = form.controls;
let hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers).length > 0;

You can try the full example with template-driven forms in this Plunker.

And for the reactive forms, we will need some similar changes:

<form [formGroup]="myForm" (ngSubmit)="register(myForm)" novalidate>
  ..
  <div formGroupName="location">
    ..
    <show-errors [control]="myForm.controls.location"></show-errors>
  </div>

  <div formArrayName="phoneNumbers">
    ..
    <show-errors [control]="myForm.controls.phoneNumbers"></show-errors>
  </div>
  ..
</form>

The same changes from country-city-validator.directive.ts and telephone-numbers-validator.directive.ts are required for the countryCity and telephoneNumbers validators in CustomValidators to properly locate the controls.

And lastly, we need to modify the construction of the FormGroup to:

this.myForm = new FormGroup({
  'name': new FormControl('', Validators.required, CustomValidators.uniqueName),
  'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]),
  'location': new FormGroup({
    'country': new FormControl('', Validators.required),
    'city': new FormControl()
  }, CustomValidators.countryCity),
  'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()], CustomValidators.telephoneNumbers)
});

And there you have it—we’ve improved the validation for the reactive forms as well and as expected, the Plunker for this example.

Nesting across Different Components

It may come as a shock to all AngularJS developers, but in Angular 4, nesting of forms across different component doesn’t work out of the box. I’m going to be straight honest with you; my opinion is that nesting is not supported for a reason (probably not because the Angular 4 team just forgot about it). Angular4’s main enforced principle is a one-way data flow, top to bottom through the tree of components. The whole framework was designed like that, where the vital operation, the change detection, is executed in the same manner, top to bottom. If we follow this principle completely, we should have no issues, and all changes should be resolved within one full detection cycle. That’s the idea, at least. In order to check that one-way data flow is implemented correctly, the nice guys in the Angular 4 team implemented a feature that after each change detection cycle, while in development mode, an additional round of change detection is triggered to check that no binding was changed as a result of reverse data propagation. What this means, let’s think about a tree of components (C1, C2, C3, C4) as in Fig. 1, the change detection starts at the C1 component, continues at the C2 component and ends in the C3 component.

A tree of nested components holding a form.

If we have some method in C3 with a side effect that changes some binding in C1, that means that we are pushing data upwards, but the change detection for C1 already passed. When working in dev mode, the second round kicks in and notices a change in C1 that came as a result of a method execution in some child component. Then you are in trouble and you’ll probably see the “Expression has changed after it was checked” exception. You could just turn off the development mode and there will be no exception, but the problem will not be solved; plus, how would you sleep at night, just sweeping all your problems under the rug like that?

Once you know that, think about what are we doing if we aggregate the forms state. That’s right, data is pushed upwards the component tree. Even when working with single-level forms, the integration of the form controls (ngModel) and the form itself is not so nice. They trigger an additional change detection cycle when registering or updating the value of a control (it is done with using a resolved promise, but keep it a secret). Why is an additional round needed? Well for the same reason, data is flowing upwards, from the control to the form. But, maybe sometimes, nesting forms across multiple components is a required feature and we need to think of a solution for supporting this requirement.

With what we know so far, the first idea that comes to mind is using reactive forms, create the full form tree in some root component and then pass the child forms to the child components as inputs. This way you have tightly coupled the parent with the child components and cluttered the business logic of the root component with handling the creation of all child forms. Come on, we are professionals, I’m sure we can figure out a way to create totally isolated components with forms and provide a way the form to just propagate the state to whoever is the parent.

All this being said, here’s a directive that allows nesting Angular 4 forms (implemented because it was needed for a project):

import {
 OnInit,
 OnDestroy,
 Directive,
 SkipSelf,
 Optional,
 Attribute,
 Injector,
 Input
} from '@angular/core';
import { NgForm, FormArray, FormGroup, AbstractControl } from '@angular/forms';

const resolvedPromise = Promise.resolve(null);

@Directive({
 selector: '[nestableForm]'
})
export class NestableFormDirective implements OnInit, OnDestroy {

 private static readonly FORM_ARRAY_NAME = 'CHILD_FORMS';

 private currentForm: FormGroup;

 @Input()
 private formGroup: FormGroup;

 constructor(@SkipSelf()
             @Optional()
             private parentForm: NestableFormDirective,
             private injector: Injector,
             @Attribute('rootNestableForm') private isRoot) {
 }

 ngOnInit() {
   if (!this.currentForm) {
     // NOTE: at this point both NgForm and ReactiveFrom should be available
     this.executePostponed(() => this.resolveAndRegister());
   }
 }

 ngOnDestroy() {
   this.executePostponed(() => this.parentForm.removeControl(this.currentForm));
 }

 public registerNestedForm(control: AbstractControl): void {
   // NOTE: prevent circular reference (adding to itself)
   if (control === this.currentForm) {
     throw new Error('Trying to add itself! Nestable form can be added only on parent "NgForm" or "FormGroup".');
   }
   (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)).push(control);
 }

 public removeControl(control: AbstractControl): void {
   const array = (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME));
   const idx = array.controls.indexOf(control);
   array.removeAt(idx);
 }

 private resolveAndRegister(): void {
   this.currentForm = this.resolveCurrentForm();
   this.currentForm.addControl(NestableFormDirective.FORM_ARRAY_NAME, new FormArray([]));
   this.registerToParent();
 }

 private resolveCurrentForm(): FormGroup {
   // NOTE: template-driven or model-driven => determined by the formGroup input
   return this.formGroup ? this.formGroup : this.injector.get(NgForm).control;
 }

 private registerToParent(): void {
   if (this.parentForm != null && !this.isRoot) {
     this.parentForm.registerNestedForm(this.currentForm);
   }
 }

private executePostponed(callback: () => void): void {
 resolvedPromise.then(() => callback());
}
}

The example in the following GIF shows one main component containing form-1 and, inside that form, there’s another nested component, component-2. component-2 contains form-2, which has nested form-2.1, form-2.2, and a component (component-3) that has a tree of a reactive form in it and a component (component-4) that contains a form that is isolated from all other forms. Quite messy, I know, but I wanted to make a rather complex scenario to show the functionality of this directive.

A complex case of Angular form validation with multiple components

The example is implemented in this Plunker.

The features that it offers are:

  • Enables nesting by adding the nestableForm directive to elements: form, ngForm, [ngForm], [formGroup]

  • Works with template-driven and reactive forms

  • Enables building a tree of forms that spans multiple components

  • Isolates a sub-tree of forms with rootNestableForm=”true” (it will not register to the parent nestableForm)

This directive allows a form in a child component to register to the first parent nestableForm, regardless if the parent form is declared in the same component or not. We’ll go into the details of the implementation.

First off, let’s have a look at the constructor. The first argument is:

@SkipSelf()
@Optional()
private parentForm: NestableFormDirective

This looks up the first NestableFormDirective parent. @SkipSelf, to not match itself, and @Optional because it may not find a parent, in case of the root form. Now we have a reference to the parent nestable form.

The second argument is:

private injector: Injector

The injector is used to retrieve the current FormGroup provider (template or reactive).

And the last argument is:

@Attribute('rootNestableForm') private isRoot

to get the value that determines if this form is isolated from the tree of forms.

Next, on ngInit as a postponed action (remember the reverse data-flow?), the current FormGroup is resolved, a new FormArray control named CHILD_FORMS is registered to this FormGroup (where child forms will be registered) and as the last action, the current FormGroup is registered as a child to the parent nestable form.

The ngOnDestroy action is executed when the form is destroyed. On destroy, again as a postponed action, the current form is removed from the parent (de-registration).

The directive for nestable forms can be further customized for a specific need—maybe remove the support for reactive forms, register each child form under a specific name (not in an array CHILD_FORMS), and so on. This implementation of the nestableForm directive satisfied the project’s requirements and is presented here as such. It covers some basic cases like adding a new form or removing an existing form dynamically (*ngIf) and propagating the state of the form to the parent. This basically boils down to operations that can be resolved within one change detection cycle (with postponing or not).

If you want some more advanced scenario like adding a conditional validation to some input (e.g. [required]=”someCondition”) that would require 2 change detection rounds, it will not work because of the “one-detection-cycle-resolution” rule imposed by Angular 4.

Anyway, if you plan on using this directive, or implementing some other solution, be very careful regarding the things that were mentioned related to the change detection. At this point, this is how Angular 4 is implemented. It might change in the future—we cannot know. The current setup and enforced restriction in Angular 4 that was mentioned in this article might be a drawback or a benefit. It remains to be seen.

Forms Made Easy with Angular 4

As you can see, the Angular team has done a really good job at providing many functionalities related to forms. I hope that this post will serve as a complete guide to working with the different types of forms in Angular 4, also giving insight into some more advanced concepts like the nesting of forms and the process of change detection.

Despite all the different posts related to Angular 4 forms (or any other Angular 4 subject for that matter), in my opinion, the best starting point is the official Angular 4 documentation. Also, the Angular guys have nice documentation in their code. Many times, I’ve found a solution just by looking at their source code and the documentation there, no Googling or anything. About the nesting of forms, discussed in the last section, I believe that any AngularJS developer that starts learning Angular 4 will stumble upon this problem at some point, which was kind of my inspiration for writing this post.

As we’ve also seen, there are two types of forms, and there is no strict rule that you can’t use them together. It’s nice to keep the codebase clean and consistent, but sometimes, something can be done more easily with template-driven forms and, sometimes, it’s the other way around. So, if you don’t mind the slightly bigger bundle sizes, I suggest to use whatever you consider more appropriate case by case. Just don’t mix them within the same component because it will probably lead to some confusion.

Plunkers Used in This Post

Understanding the basics

  • What is AngularJS?

    Angular is an opinionated JavaScript framework for building dynamic web applications. It uses templating, dependency injection, and data binding to turn HTML into an application development oriented environment.

  • What is form validation?

    Form validation is the process of verifying that the user input is valid to be stored and acted upon. If the data fails validation, the user is presented with the form again to correct indicated input errors.

Hire a Toptal expert on this topic.
Hire Now
Igor Geshoski

Igor Geshoski

Verified Expert in Engineering
10 Years of Experience

Skopje, Macedonia

Member since June 8, 2016

About the author

Igor has been honing his algorithms and problem solving skills on large-scale Java based enterprise applications for about five years.

Read More
authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.