Log In

Angular Reactive Forms VS Template Driven Forms


by Yariv Katz

Grabbing user input is a major part of almost any web application. We need to take different input, validate what the user entered, perform calculations on the input or maybe send it to the server. The following article will go over building forms with angular using something called Reactive Forms.

Ways to create forms in angular

There are 2 ways to deal with form with angular:
- template driven forms
- Reactive forms

Template driven forms

With template driven forms we are using angular template and forms directive to build our forms. You can build almost all forms using those tools. You use ngModel directive to attach the data to a class property in a 2 way binding way. You can add validation to each input and you can check that validation using template reference vars grabbing the ngModel directive or ngForm. Template driven forms are a bit less predicatable then reactive forms. Things often takes a few ticks to be updated which makes testing for the form a bit harder. Template driven forms are also a bit harder when there is a complex and dynamic forms. I guess for the majority of the simple forms we will tend to go with this option and be quite satisfied.

Reactive Forms

Reactive forms are forms driven by our model. It's a more declarative predicatable way for dealing with our forms. We declare FormControls classes, attach them to our template, add validation by code to our form control classes and control the form control value. The Forms state is immutable and the updates are made in a sync way which makes the entire process more predicatable. For me the decision to create a form using Reactive form is an easy process, I just think if I plan to add unit testing to that form, if the answer is yes I will go with Reactive forms.

Our goal in this tutorial Is to learn how we create a form with template driven forms and create the same with Reactive Forms. Comparing those two tools will help us better understand when to use what.

FormControl

FormControl represents an input in our form. Lets examine what we can do with a FormControl. Create a new angular application using the angular cli.

> npx @angular/cli new reactive-forms-tutorial

Let's create 2 components, one will be used to experiment with Reactive Forms and the other we will use to experiment with Template Driven Forms. Using angular cli create the components:

> ng g c ReactiveForms
> ng g c TemplateDriven

To use Reactive forms directives and template driven forms directive we will need to install the ReactiveFormsModule and FormsModule. Modify the app.module.ts to this:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {ReactiveFormsModule, FormsModule} from '@angular/forms';
import { ReactiveFormsComponent } from './reactive-forms/reactive-forms.component';
import { TemplateDrivenComponent } from './template-driven/template-driven.component';

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

We simply added the ReactiveFormsModule and FormsModule to the imports array.

Lets create our first input control that the user types his name in that control. In our reactive-forms component ts file reactive-forms.component.ts make it looks like so:

import { Component, OnInit } from '@angular/core';
import {FormControl, Validators} from '@angular/forms';

@Component({
    selector: 'app-reactive-forms',
    templateUrl: './reactive-forms.component.html',
    styleUrls: ['./reactive-forms.component.scss']
})
export class ReactiveFormsComponent {
    nameControl = new FormControl('', {
        validators: [Validators.required],
        updateOn: 'blur'
    });

    constructor() { }

}

We create a new instance of FormControl. The first argument is the value or state object. state object is the value and the disabled state, by default control is not disabled. angular treats the state as immutable, when we create the control angular creates the first state, in our case it is:
{value: '', disabled: false}
the state is immutable so on a different value it will replace the state object with a new state. The 2nd argument holds the options where we can specify sync/async validation, and also when the control is updated. We set the control to update on blur. Now lets attach this FormControl to our template. Modify the file reactive-forms.component.html

<div class="form-group">
    <label for="name">Your Name</label>
    <input [formControl]="nameControl" type="text" id="name" name="name" class="form-control" />
</div>

<hr/>
<h1>The name entered in the form:</h1>
<p>
    {{nameControl.value}}
</p>

to attach the FormControl to an input we have to place in our template the directive formControl. now our FormControl will update the state when the user is bluring from the input. We can use our FormControl instance to access the value, errors, state, etc. Angular also will add css classes to the input based on the state of the control. Those are the same css classes that are added when you add the ngModel directive:
- ng-touched / ng-untouched
- ng-pristine / ng-dirty
- ng-valid / ng-invalid
In case we want to print the errors we can access them directly from the FormControl. For example add the following to the bottom of reactive-forms.component.html

<p>
    {{nameControl.errors | json}}
</p>

Through the FormControl we can access the properties exposed to us in the FormControl class the common class which the building block of reactive forms are build from which is the class: AbstractControl. For performance reasons we decided that we will update our form control only on blur, if the form control is a part of a form we can also set to update on submit.

NgModel

Now let's create a similar text input with template driven forms. modify the template file of the template driven form component template-driven.component.html to this:

<div class="form-group">
    <label for="name">Your Name</label>
    <input 
        [(ngModel)]="name" 
        #nameNgModel="ngModel"
        [ngModelOptions]="{updateOn: 'blur'}"
        type="text" 
        id="name" 
        name="name" 
        required
        class="form-control" />
</div>

<hr/>
<h1>The name entered in the form:</h1>
<p>
    {{nameNgModel.value}}
</p>

<p>
    {{nameNgModel.errors | json}}
</p>

so this time to pass the validation we wrote it in the input like html attribute and the values are the same as html5 input validation attributes. And to access the control errors and value we can by just grabbing the ngModel directive instance and printing the properties.

Custom Validation

Let's create a custom validation for our control in the reactive form and on the template driven as well.. Our control is an input for a full name, so let's validate that the user input a string containing two words seperated by space. When we create validation we have to diffrentiate between 2 types:
- Sync Validation
- Async Validation
We will usually use the async validation when we need to validate the value with the our server. First angular will run the sync validations and only if they all pass it will run the async validation. To create a custom async validation we will need to create the type ValidatorFn. ValidatorFn is a function which gets as an argument AbstractControl and returns a key value object of the errors. Create a new folder in the app folder named validators and in that folder create a file called full-name.ts with the following code:

/**
 * Validator function that the user typed firstname and lastname seperated by space
 */

import { AbstractControl } from "@angular/forms";

export function fullNameValidation(control: AbstractControl) {
    const fullName = control.value;
    const nameArray = fullName.split(' ');
    if (nameArray.length === 1) {
        return {
            'full_name': 'We need your last name as well'
        }
    }
    if (nameArray.length > 2) {
        return {
            'full_name': 'Only first name and last name please'
        }
    }
}

The validation function takes the value from the control and validate the the value of the name contains a first and last name. Now to attach it on the Reactive Forms component modify reactive-forms.component.ts

import { Component, OnInit } from '@angular/core';
import {FormControl, Validators} from '@angular/forms';
import { fullNameValidation } from '../validators/full-name';

@Component({
    selector: 'app-reactive-forms',
    templateUrl: './reactive-forms.component.html',
    styleUrls: ['./reactive-forms.component.scss']
})
export class ReactiveFormsComponent {
    nameControl = new FormControl('', {
        validators: [Validators.required, fullNameValidation],
        updateOn: 'blur'
    });

    constructor() { }

}

we simply pass the validation in the validators option of the form control. Now lets see how we can use our custom validation function in the template driven form. In template driven form it is a bit more complicated cause we will need to wrap our validation function in a directive. Lets create the same custom validation to the template driven form. We will use angular cli to create our directive. In the terminal type:

> ng g d FullNameValidation

Now lets modify that directive to look like this:

import { Directive, forwardRef } from '@angular/core';
import { Validator, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms';
import { fullNameValidation } from './validators/full-name';

@Directive({
    selector: '[fullName][ngModel]',
    providers: [
        { 
        provide: NG_VALIDATORS, 
        useExisting: forwardRef(() => FullNameValidationDirective),
        multi: true
        }
    ]
})
export class FullNameValidationDirective implements Validator {
    validate(control: AbstractControl): ValidationErrors | null {
        return fullNameValidation(control);
    }

}

Our validation directive needs to implement the Validator interface, which force us to implement the method validate which is basically the ValidationFn we created earlier. We also need to register this directive as a validator so angular will know to add this directive to the list of validation directives. The selector of our directive will work if there is the attribute ngModel as well as the attribute fullName. So a bit more overhead to create custom validation on template driven forms.

Async Validation

We are not going to show the async validation process cause it's pretty similar to the validation before. But we will explain how it is done. For the reactive forms you simply need to implement a function of type: AsyncValidatorFn
This function returns a promise or observable with an error object. For the template driven form you will need to implement the interface AsyncValidator. the validate function will return a promise or observable. you will need to tell angular to add this validator to the async validators so you will need to add the class as a provider of NG_ASYNC_VALIDATORS

FormGroup

Lets examine how we make our control a part of a form. In Reactive Forms we define forms in our template as a FormGroup which contains several FormControls. Lets create a registration form where the user input his email as well as a password and repeat password field. The repeat password field has to match the password field. In the reactive-forms.component.ts add this as a class property:

registerForm = new FormGroup({
    email: new FormControl('', {
        updateOn: 'submit',
        validators: [Validators.email, Validators.required]
    }),
    passwordRepeat: new FormGroup({
        password: new FormControl('', {validators: [
            Validators.minLength(5),
            Validators.required
        ]}),
        repeat: new FormControl('', {validators: [
            Validators.minLength(5),
            Validators.required
        ]})
    }, {
        validators: [isPasswordRepeatMatch]
    })
});

The intresting thing here is that we can nest FormGroup inside a FormGroup. Since our password and repeat password have a validation that depends on two inputs and not just one, it makes sense to wrap it as a mini group inside our form group. The building blocks of our form are also FormControl so the email and password and repeat eventually map to a corresponding FormControl. Notice the the sub group of the password and repeat has a custom validation called isPasswordRepeatMatch which we still need to implements. In the validators function create a file called: password-repeat.ts with the following code:

import { FormGroup } from "@angular/forms";

export function isPasswordRepeatMatch(passwordRepeatGroup: FormGroup) {
    const {password, repeat} = passwordRepeatGroup.controls;
    if (password.value !== repeat.value) {
        return {
            'NO_MATCH': "the password and repeat don't match"
        }
    }
}

This time our validation function is getting the FormGroup that contains the password and repeat FormControls. We can just compare their value. Make sure that the reactive-forms.component.ts is pointing to this function. Now it's time to connect our form group to the template. In the reactive-forms.component.html add at the bottom of the file the following:

<h2>Register</h2>

<form [formGroup]="registerForm">
    <div class="form-group">
        <label>Email:</label>
        <input 
        type="email" 
        name="email" 
        formControlName="email"
        class="form-control" />
    </div>
    <ng-container formGroupName="passwordRepeat">
        <div class="form-group">
            <label>Password</label>
            <input 
                formControlName="password"
                type="password" 
                name="password" 
                class="form-control" />
        </div>
        <div class="form-group">
            <label>Repeat</label>
            <input 
                formControlName="repeat"
                type="password" 
                name="repeat" 
                class="form-control" />
        </div>
    </ng-container>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">Submit</button>
    </div>
</form>

{{ registerForm.controls.passwordRepeat.errors | json }}

we need to attach the directive formGroup with the formGroup we created (the papa one and not the sub form group). and all the keys of the form group with the directive formControlName / formGroupName. We wrapped the password and repeat in an ng-container so we will have a common parent for those two input where we can attach the formGroupName directive. and inside place the formControName according to the names of the children in the sub group. So the formControlName needs to be of the closes FormGroup that wraps then. at the bottom we are examining our custom validation and printing the errors. If one of the children of the form group is not passing validation then the entire form group is not passing validation. Now to achieve the same with template driven form we will have to create our password and repeat validation in a directive. Using angular cli create a new directive:

> ng g d PasswordRepeat

Now modify the created directive to look like this:

import { Directive, Input, forwardRef } from '@angular/core';
import { Validator, NgModel, AbstractControl, ValidationErrors, NG_VALIDATORS } from '@angular/forms';

@Directive({
    selector: '[passwordRepeat][ngModel]',
    providers: [
        {
            provide: NG_VALIDATORS, 
            useExisting: forwardRef(() => PasswordRepeatDirective),
            multi: true
        }
    ]
})
export class PasswordRepeatDirective implements Validator {
    @Input('passwordRepeat') otherControl: NgModel;
    
    validate = (control: AbstractControl): ValidationErrors | null => {
        if (control.value !== this.otherControl.value) {
            return {
                'NO_MATCH': "the password and repeat don't match"
            }
        }
    }
}

The thing to note here is that our directive needs to get as an input the ngModel of the other control to compare to. Now modify the template-driven.component.html

<h2>Register template driven</h2>

<form #registerForm="ngForm">
<div class="form-group">
    <label>Email:</label>
    <input 
        type="email" 
        name="email" 
        required
        email
        [(ngModel)]="email"
        class="form-control" />
</div>
<div class="form-group">
    <label>Password</label>
    <input 
        [(ngModel)]="password"
        required
        minlength="5"
        #passwordModel="ngModel"
        type="password" 
        name="password" 
        class="form-control" />
</div>
<div class="form-group">
    <label>Repeat</label>
    <input 
        [(ngModel)]="repeat"
        required
        minlength="5"
        [passwordRepeat]="passwordModel"
        #repeatModel="ngModel"
        type="password" 
        name="repeat" 
        class="form-control" />
</div>
<div class="form-group">
    <button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>

{{ repeatModel.errors | json }}

The thing to note here is that we attached our custom password validation directive to the repeat password input. We are also passing the password ngModel when we are calling the directive. Below the form we are examining the errors on the ngModel of the repeat input and we should see when the passwords dont match the error object with the NO_MATCH key. It's easy to see that even this simple custom validation is much harder to implement and test using the template driven approach.

FormBuilder

While creating the register form with ReactiveForms, it's easy to notice the amount of code, and all the instances of FormControl and FormGroup we had to create. The advantage of ReactiveForms will be shown on complex forms so it's meant to create complex forms easier, which means that we suppose to write even complexer forms than what we did here, but with our simple form there is so much code what will happen on more complex forms. To easily create our reactive forms, there is a service we can use called FormBuilder. Creating the same registration form with FormBuilder will look like this: Modify the constructor of the reactive-forms.component.ts

constructor(formBuilder: FormBuilder) { 
    this.registerForm = formBuilder.group({
        email: formBuilder.control('', [Validators.email, Validators.required]),
        passwordRepeat: formBuilder.group({
            password: formBuilder.control('', [
                Validators.minLength(5),
                Validators.required
            ]),
            repeat: formBuilder.control('', [
                Validators.minLength(5),
                Validators.required
            ])
        }, {validator: isPasswordRepeatMatch})
    })
}

So there are no need to create instances and the form builder will do that for us.

Summary

Forms are an important part of every web application and due to their popularity we have to get the proper tools to deal with them. We can choose to create a certain form using template driven or reactive. In my opinion complex forms with custom sync/async validation are easier to create using the reactive approach plus adding tests to those tests are much predicatable with reactive. On simple forms it's might be quicker to go with the template driven. Hope this article helped you guys. Happy Coding...