Difference between Template-Driven and Reactive Forms in Angular
The template-driven approach would be familiar to those coming from AngularJS 1 background and the Reactive approach makes the template code quite clean.
May 21, 2019 • 17 Minute Read
Introduction
Forms are a very common kind of feature used in any app. In this guide, we'll learn about the two techniques used by Angular to create forms - Template-driven and reactive forms. We'll also have a look at how we can add validations using both of these approaches.
High-level Differences between Template-driven and Reactive Forms
Below are some of the high-level differences between the two types:
- Template-driven forms make use of the "FormsModule", while reactive forms are based on "ReactiveFormsModule".
- Template-driven forms are asynchronous in nature, whereas Reactive forms are mostly synchronous.
- In a template-driven approach, most of the logic is driven from the template, whereas in reactive-driven approach, the logic resides mainly in the component or typescript code. Let us get started by generating a component and then we'll update our form code.
Create Component for the Form
We'll generate separate components for both the types using the following command:
ng generate component template-forms
ng generate component reactive-forms
Template-Driven Forms
To enable template-driven forms, i.e. to make ngModel and other form-related directives available for use in our project, we have to import them explicitly to our AppModule. This is how our app.module.ts should look like after adding the import:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import { TemplateFormsComponent } from './template-forms.component';
@NgModule({
declarations: [
AppComponent,
TemplateFormsComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Now, that we have our basic setup for the forms ready, let's start adding our form code.
We have a screen to add a new course. Below is how the template for that would look like:
<div class="row">
<div class="col-xs-12">
<form>
<div class="row">
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control">
</div>
<div class="col-sm-2 form-group">
<label for="courseDesc">Course Description</label>
<input
type="text"
id="courseDesc"
class="form-control">
</div>
<div class="col-sm-2 form-group">
<label for="courseAmount">Course Amount</label>
<input
type="number"
id="courseAmount"
class="form-control">
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button
class="btn btn-success"
type="submit">Add</button>
<button
class="btn btn-danger"
type="button">Delete</button>
<button class="btn btn-primary" type="button">Clear</button>
</div>
</div>
</form>
</div>
</div>
We have the basic HTML form created with Course Name, Course description and Amount fields and we also have the buttons to add, delete, or clear the form.
While Angular would be able to detect the <form> element in the above code (since we have the imported the FormsModule in app.module.ts), we need to register the form controls manually. To do that, we need to add "ngModel" to the form control and also add a "name" attribute to the control. The "ngModel" would bind our input fields to properties on our data model. Also to submit the form, we'll use the (ngSubmit) directive which would get fired whenever the form is submitted.
This is how our updated HTML would then look like:
<div class="row">
<div class="col-xs-12">
<form (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control"
name="courseName"
ngModel>
</div>
<div class="col-sm-2 form-group">
<label for="courseDesc">Course Description</label>
<input
type="text"
id="courseDesc"
class="form-control"
name="courseDesc"
ngModel>
</div>
<div class="col-sm-2 form-group">
<label for="courseAmount">Course Amount</label>
<input
type="number"
id="courseAmount"
class="form-control"
name="courseAmount"
ngModel>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button
class="btn btn-success"
type="submit">Add</button>
<button
class="btn btn-danger"
type="button">Delete</button>
<button class="btn btn-primary" type="button">Clear</button>
</div>
</div>
</form>
</div>
</div>
In case we want to assign any default value to any of the fields (e.g. course name), we can bind to the ngModel as shown below:
<input
type="text"
id="courseName"
class="form-control"
name="courseName"
[ngModel]="'default course name'">
Now that we have our form created, let's try and submit the form. We'll also add some basic validations to the form (e.g. we'll mark the Course Name input field as required). Also, we'll add a local reference on the form element, so that we get access to the form in our Typescript code.
<div class="row">
<div class="col-xs-12">
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<div class="row">
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control"
name="courseName"
ngModel
required>
</div>
<div class="col-sm-2 form-group">
<label for="courseDesc">Course Description</label>
<input
type="text"
id="courseDesc"
class="form-control"
name="courseDesc"
ngModel>
</div>
<div class="col-sm-2 form-group">
<label for="courseAmount">Course Amount</label>
<input
type="number"
id="courseAmount"
class="form-control"
name="courseAmount"
ngModel>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button
class="btn btn-success"
type="submit">Add</button>
<button
class="btn btn-danger"
type="button">Delete</button>
<button class="btn btn-primary" type="button" (click)="onClear()">Clear</button>
</div>
</div>
</form>
</div>
</div>
Now that we have the handler defined for the form submit, let's add in the code to handle this in our Typescript component. We'll also reset the form when the user clicks on "Clear" button. For this, we'll get access to the form in the Typescript code via the @ViewChild. Below is how our Component typescript code would look like:
import {
Component,
OnInit,
OnDestroy
} from '@angular/core';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-template-forms',
templateUrl: './template-forms.component.html',
styleUrls: ['./template-forms.component.css']
})
export class TemplateFormsComponent implements OnInit, OnDestroy {
constructor() { }
ngOnInit() {
}
@ViewChild('f') courseForm: NgForm;
onSubmit(form: NgForm) {
console.log("Course Name is : " + form.value.courseName);
console.log("Course Desc is : " + form.value.courseDesc);
console.log("Course Amount is : " + form.value.courseAmount);
}
onClear() {
// Now that we have access to the form via the 'ViewChild', we can access the form and clear it.
this.courseForm.reset();
}
onDelete() {
}
ngOnDestroy() {
}
}
Thus, we can see that an object of type "NgForm" gets created and we can access the values which the user entered by accessing them on the "value" property.
Finally, let's just add the code to display an error message if the required field (Course Name, here) is not entered by the user. Also, keep in mind, this error should not be shown by default i.e. on form load, but only if the user tabs out without entering any value. Thus, we'll make use of the "dirty" and "touched" properties. Let us have a look at the relevant template code:
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control"
name="courseName"
ngModel
required
#courseName="ngModel">
</div>
<div style="color:red"
*ngIf="courseName.errors && (courseName.dirty || courseName.touched)">
<p *ngIf="courseName.errors.required">
Course Name is required
</p>
</div>
So, we are just adding a div element for the validation messages. The *ngIf on the div element is rendered only when there are validation errors on our courseName field, which actually points to the ngModel instance. Also, we want to display this div only when our courseName field is dirty or touched. Inside that div, we can add some more validations for different types of errors by accessing courseName.errors property e.g. in this case, we are checking for courseName.errors.required.
Reactive Forms
Let's have a look at the other approach now - Reactive Forms, which are also known as model-driven forms. In this approach, we design our forms in the component (Typescript code) and then bind them or relate them to our HTML template. They need the "ReactiveFormsModule" imported in app.module.ts. So this is how our AppModule would look like for a reactive form.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule} from '@angular/forms';
import { AppComponent } from './app.component';
import { ReactiveFormsComponent } from './reactive-forms.component';
@NgModule({
declarations: [
AppComponent,
ReactiveFormsComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Let's now start with our form. We'll try with the same example for adding courses. Let's first start with our typescript code. Here we'll define a new instance of FormGroup class and inside that, we'll define our controls using FormControl class. Below is our component code:
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-reactive-forms',
templateUrl: './reactive-forms.component.html',
styleUrls: ['./reactive-forms.component.css']
})
export class ReactiveFormsComponent implements OnInit {
courseForm: FormGroup;
constructor() {
}
ngOnInit() {
this.initForm();
}
onSubmit() {
}
private initForm() {
this.courseForm = new FormGroup({
'courseName': new FormControl(null, Validators.required),
'courseDesc': new FormControl(null),
'courseAmount': new FormControl(null)
});
}
}
Thus, we have created a new instance of "FormGroup" and assigned it to the "courseForm" property on the component. We have also defined form controls using the "FormControl" class. We can define a default value for the control as the first argument. In the above example, we are defining it as null (empty field).
Since we want the course name field to be required, we pass 'Validators.required' as the second argument for the 'courseName' form control. In case we want to make use of any async validation (e.g. check if courseName is already existing in the database, we can make use of the third argument which is for defining async validations).
However, at this point, Angular does not know which of our Typescript controls relate to which inputs in our template code. We make use of the "formControlName " directive in the template to associate individual controls in the template to controls on the FormGroup instance.
Below is how our template would look like:
<div class="row">
<div class="col-xs-12">
<form [formGroup]="courseForm" (ngSubmit)="onSubmit()">
<div class="row">
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control"
formControlName="courseName">
</div>
<div class="col-sm-2 form-group">
<label for="courseDesc">Course Description</label>
<input
type="text"
id="courseDesc"
class="form-control"
formControlName="courseDesc">
</div>
<div class="col-sm-2 form-group">
<label for="courseAmount">Course Amount</label>
<input
type="number"
id="courseAmount"
class="form-control"
formControlName="courseAmount">
</div>
</div>
<div class="row">
<div class="col-xs-12">
<button
class="btn btn-success"
type="submit">Add</button>
<button
class="btn btn-danger"
type="button">Delete</button>
<button class="btn btn-primary" type="button">Clear</button>
</div>
</div>
</form>
</div>
</div>
It is important to use the same name for the "formControlName" as the one used in the typescript code. Also, the "formGroup" directive tells Angular to take our FormGroup, instead of creating one for us automatically. This directive synchronizes the form we had created in Typescript earlier.
Let's also add a message to show error messages below the course name field. To do that, we'll update the template as shown below;
<div class="col-sm-5 form-group">
<label for="courseName">Course Name</label>
<input
type="text"
id="courseName"
class="form-control"
formControlName="courseName">
</div>
<div *ngIf courseForm.get('courseName').valid && courseForm.get('courseName').touched>
Please enter valid course name !
</div>
Let's also update our typescript form to update "courseDesc" as a required field and add minimum length validation for the field. We'll also output the form object during submit. Below is the relevant code:
onSubmit() {
// Since we have access to the FormGroup instance we can directly output the same
console.log(this.courseForm);
}
private initForm() {
this.courseForm = new FormGroup({
'courseName': new FormControl(null, Validators.required),
'courseDesc': new FormControl([Validators.required, Validators.minLength(100)]),
'courseAmount': new FormControl(null)
});
}
As you can see, since we now have two validations on the 'courseDesc', we define them as an array of Validators.
Conclusion
We have seen both the ways to build forms in Angular. The template-driven approach would be familiar to those coming from AngularJS 1 background and thus makes it easy for migrating their app to the latest Angular version. The Reactive approach removes the core validation logic from the template and hence makes the template code quite clean. From a unit testing perspective, it is easier to write unit tests with Reactive forms, since the logic is contained inside our component.
Learn More
Explore these React and Angular courses from Pluralsight to continue learning: