This article covers all important features of Reactive Forms in Angular from basic to advanced scenarios.
Below is the list of topics that are covered in this article,
- Introduction to forms in Angular and their differences
- A basic example of Reactive Forms
- Inbuilt and Custom Validations
- Async validations
- Conditionally managing validations
- Dynamically adding form fields
So, let’s see each and every topic one by one.
Introduction
Angular is providing two ways to work with forms: template-driven forms and reactive forms. These both ways work differently.
The below information will help you to decide which type of form works best for your situation.
- Reactive forms are more robust: they're more scalable, reusable, and testable. If forms are a key part of your application, use Reactive forms.
- Template-driven forms are useful for adding a simple form to an app, such as an email list, signup form. They're easy to add to an app, but they don't scale as much as reactive forms. If you have very basic form requirements and logic, use template-driven forms.
Key differences
The table below summarizes the key differences between reactive and template-driven forms.
|
REACTIVE
|
TEMPLATE-DRIVEN
|
Setup (form model)
|
More explicit, created in the component class
|
Less explicit, created by directives
|
Data model
|
Structured
|
Unstructured
|
Predictability
|
Synchronous
|
Asynchronous
|
Form validation
|
Functions
|
Directives
|
Mutability
|
Immutable
|
Mutable
|
Scalability
|
Low-level API access
|
Abstraction on top of APIs
|
A basic example of Reactive Forms
What are Reactive forms?
At first sight, Reactive forms might seem complicated but they can be very useful when you actually get it. They are really powerful and flexible.
With Reactive forms, we don’t depend on design (template) for creating our forms but instead, here we use the API’s provided by angular which is called the Reactive API. These APIs can be used by importing a module called ReactiveModule to create the forms.
With Reactive Forms, all the declaration and structuring of the forms is to be done in the code (TS file). This model is then used in the template (HTML).
“Reactive forms provide a model-driven approach to handling form inputs whose values change over time.”
Here’s how to get started with Reactive Forms,
- Import Reactive forms module
- Define the form(formGroup) in a component class.
- Connect this form to your HTML form.
Import Reactive forms module
Firstly, we have to import ReactiveFormsModule instead of FormsModule in our app.module.ts or in your desired module.
- import { ReactiveFormsModule } from '@angular/forms';
Define the form (formGroup) in a component class
In our component class, we will define the formGroup and also formControls within our formGroup.
- formGroup
Entire form will be treated as formGroup and here we can give a name to the formGroup.
- formControl
Each ‘input’ in our form will be mapped with formControl. This formControl is like our input box, select box, radio buttons etc. This form control will contain the value and validations of the form field.
In short, what we have to do is, we will first create formGroup’s object and then inside this formGroup we will create different formControl’s objects as per our need (each formControl object for each input, we require from user).In this way, our formGroup will contain a group (collection) of form controls. And by doing this our entire form is ready to be use in our template (HTML).
Now, one thing to be noted is that we want this form to be there when we load the page so we have to write this formGroup on our TS file’s ngOnInit() method and before starting we have to give a name or declare the formGroup.
So, here we are with the code.
signupForm: FormGroup; //declaring our form variable
- ngOnInit() {
- this.signupForm = new FormGroup({
- user_name: new FormControl(null),
- user_email: new FormControl(null),
- password_group: new FormGroup({
- user_password: new FormControl(null),
- user_confirmPassword: new FormControl(null),
- }),
- user_phone: new FormControl(null),
- user_gender: new FormControl('Male'),
- user_city: new FormControl('Ahmedabad'),
- user_notification: new FormControl('email')
- });
- }
In the above code, we have initialized formGroup’s object with different form fields(formControl’s objects). And these formControls are having one argument that is ‘null’ for initializing our form fields. So initially all the form fields would be blank(null value). And if we want to assign some initial value like Gender and City in our case then we can give those values too.
In above code notice the ‘password_group’ field of the form. This is a formGroup inside our main formGroup(nesting of FormGroups). We have taken password_group because we want to consider password and confirm password fields as one group. So this way we can combine these formControls and make them one formGroup.
Connect this form to your HTML form
Now, our Form is ready to be bind with our HTML. So moving to HTML file, we can bind the form like below:
- <form [formGroup]="signupForm" (ngSubmit)="onSubmit()"></form>
formGroup - Binds the Form to the FormGroup we created in component i.e. with the signupform.
ngSubmit - This event will be triggered when we submit the form.
formControlName - Each form fields (inputs) should have formControlName, which is exactly the same as given in TS file.
- <input type="text" placeholder="Name" formControlName="user_name">
So, in short our entire HTML can look like this,
Note
In this example I have used Bootstrap for design purpose so you will find HTML according this.
And lastly, the onSubmit() method in our TS file will just submit the form and inside this method we will just print the values entered using the form.
- onSubmit() {
- console.log(this.signupForm);
- console.log(this.signupForm.get('user_name').value);
- }
By doing all these, we are able to see the form as below.
And now, we will fill the form and click on the “Sign Up” button. The form will get submitted and onSubmit()method will be called so this will print the consoles in your browser as below,
Note
- console.log(this.signupForm); ==> This line will print the entire form’s object in the console. Here, you can see that entire form’s Status is Valid and Values is displaying all the data you have entered in the Form.
- console.log(this.signupForm.get('user_name').value); ==> This line will print individual fields of Form like UserName in this example.
Inbuilt and Custom Validations
Now it’s time to move on to the next topic and that is Validation in Reactive Forms.
Validation is the most basic requirement when you are working with the forms. Here, we will see how to validate the user input and will display validation messages accordingly.
Validator functions
There are two types of validator functions: sync validators and async validator.
- Sync validators - these functions will require form control’s instance and immediately returns either validation errors or null.
- Async validators - these functions will require form control’s instance and returns a Promise or Observable that later emits validation errors or null.
In this topic, we will focus only on Sync validations. For validators to work, we have to import Validators class on our TS file like below.
- import { FormGroup, FormControl, Validators } from '@angular/forms';
And now let’s see how to use validations:
- ngOnInit() {
- this.signupForm = new FormGroup({
- user_name: new FormControl(null, Validators.required),
- user_email: new FormControl(null, [Validators.email, Validators.required]),
-
- password_group: new FormGroup({
- user_password: new FormControl(null, [Validators.required]),
- user_confirmPassword: new FormControl(null, [Validators.required]),
- }),
-
- user_phone: new FormControl(null),
- user_gender: new FormControl('Male'),
- user_city: new FormControl('Ahmedabad', [Validators.required]),
- user_notification: new FormControl('email')
- });
- }
As you can see Form Control’s instance will have second parameter as validation (single value or an array). This second parameter only accepts sync validations and custom validations. And if we want to assign Async validations then we have to give it as a third parameter to Form Control.
By now we have applied validations to our form’s inputs but here we also need to show the validation message to the end user. So for that in HTML file we will fetch (check) the validation errors and according to that will display the message.
- <p class="lblError" *ngIf="signupForm.get('user_name').hasError('required')
- && !signupForm.get('user_name').pristine">
- Name is Required
- </p>
Above <p> tag is showing validation message and this <p> tag should be visible to end user if and only if the validation error occurs. So for that we have to apply ‘IF’ condition on <p> tag. Here notice that we can reach to a control with either signupForm.get(‘user_name’) or with signupForm.controls.user_name and then we can reach to errors with either .hasError(‘required’) or with .errors?.required
Here, in IF condition, we have also included .prisitine method. This indicates the state of the input field. Angular provides various states as below mentioned:
- valid: This property returns true if the element’s contents are valid and false otherwise.
- invalid: This property returns true if the element’s contents are invalid and false otherwise.
- pristine: This property returns true if the element’s contents have not been changed (controls just loaded then returns true).
- dirty: This property returns true if the element’s contents have been changed.
- untouched: This property returns true if the user has not visited the element.
- touched: This property returns true if the user has visited the element.
So based on our requirement we can use any of above. In our example, we have used .pristine because we do not want to show the validation message when user has not changed the control’s value or we can say when controls are just loaded on the screen.
So, our entire form with validations can look like this.
Also, notice how we disabled the Submit button if the entire form in invalid.
Time to run the application..!!
By default, our Submit button is disabled as we have not entered any values to the inputs but if we enter any value and then remove it and make the control blank again then it will display the validation message as shown above.
Custom Validations
If we need some specific validations then we can create our own custom validators also. These custom validators are just functions that we can create in our component file.
Consider a scenario where we want to create a custom validator that will take the user name and we will check that user name should not fall within the invalid name’s array declared by us. If so, then it will give error otherwise not.
In TS file create a dummy array like below:
- invalidNamesArr: string[] = ['Hello', 'Angular'];
So, when a user tries to enter a user name like ‘Hello’ or ‘Angular’ it will indicate the error.
Now, we will create a custom validator function as below,
- invalidNameValidation(control: AbstractControl): {[key: string]: boolean} {
- if (this.invalidNamesArr.indexOf(control.value) >= 0) {
- return {invalidName: true};
- }
- return null;
- }
Now, we just need to call this validation function with username FormControl inside ngOnInit() method,
user_name: new FormControl(null, [Validators.required, this.invalidNameValidation.bind(this)])
Ok, so our validation has been applied so we can move to HTML file now and show the error message like below,
- <p class="lblError" *ngIf="signupForm.get('user_name').hasError('invalidName') && !signupForm.get('user_name').pristine">
- Name is Invalid
- </p>
Let’s check it out!
Async Validations
Let’s create a simple Async validation that checks against all the Email-ids of existing users and does not allow to enter the same Email-id to the new user.
It is always a best practice to keep the logic centralized as much as you can and put the reusable code in a separate file. So here we are creating a Utility folder and inside this folder, we are creating a normal TS file named as duplicateEmailCheck.ts.
In this file, we are having checkEmail method that will make the HTTP Get call using the object of UserService and that will return all the records of user from the database. Now, in this result, we will check that Email-Id entered by the new user matches with the existing data or not. If matched, then we will indicate duplicateEmail validation error.
Let us look at the actual code as below: (duplicateEmailCheck.ts)
- import { UserService } from "../signup/user.service";
- import { AsyncValidatorFn, AbstractControl } from "@angular/forms";
- import { Observable, of } from "rxjs";
- import { map } from "rxjs/operators";
- import { UserModel } from "../signup/userModel";
-
- export class DuplicateEmailCheck {
- static checkEmail(_serviceObj: UserService): AsyncValidatorFn {
- return (c: AbstractControl): Observable<{ [key: string]: boolean } | null> => {
-
- if (c.value != null && c.value != '') {
- return _serviceObj.getAllUsers().pipe(
-
- map((res: UserModel[]) => {
- if (res.length != 0) {
-
- let matched: boolean = false;
- for (let index = 0; index < res.length; index++) {
- if (res[index].user_email == c.value) {
- matched = true;
- break;
- }
- }
-
- if (matched) {
- return { duplicateEmail: true };
- } else {
- return null;
- }
-
- } else {
- return null;
- }
-
- })
- );
- }
-
- return of(null);
- };
- }
- }
The above checkEmail method is accepting instance of a UserService as its argument and also we are having the value of input control on which we have applied this validator.
Now, it is time to bind this validation to the form control UserEmail! So let’s move to signup.component.ts file. Here, inside ngOnInit() method user_email FormControl will have third argument as a async validator.
- user_email: new FormControl(null, [Validators.email, Validators.required],DuplicateEmailCheck.checkEmail(this._userServiceObj))
And finally we will move to HTML (signup.component.html) file and show the validation message.
- <p class="lblError" style="color:blue" *ngIf="(signupForm.get('user_email').status == 'PENDING' && !signupForm.get('user_email').pristine)">Checking...</p>
- <p class="lblError" style="color:green" *ngIf="(signupForm.get('user_email').status == 'VALID' && !signupForm.get('user_email').pristine && (!signupForm.get('user_email').value == '' ))">Email is Available</p>
- <p class="lblError" style="color:red" *ngIf="signupForm.get('user_email').hasError('duplicateEmail')">Email is already Taken</p>
You can see that we are displaying different messages depending on the value of the status property of the email form control. And the possible values for status are Valid, Invalid, Pending and Disabled. And main thing is error message for duplicateEmail, this message is for async validation applied over here.
Conditionally managing validations
Have you noticed the two radio buttons Email and Phone on our HTML? Yes, these radio buttons are there to ask the user to select their choice to get the notification from Admin. Here, if the user selects Email then Email’s input box will have required validation and if the user selects Phone then Phone’s input box will have required validation. In short, we are conditionally managing the validation and we will display the error message based on which radio button has been selected by the user.
First of all, we will check that radio buttons’ values are changed by the user or not and if yes then we will invoke our validation method that will apply (bind) the validation accordingly.
- this.signupForm.get('user_notification').valueChanges.subscribe(
- x => this.setNotificationValidation(x));
Above line of code should be placed inside ngOnInit() method but after the completion of FomGroup’s object’s initialization. Notice that here setNotificationValidation() is our customized validation method that will apply the validation.
- setNotificationValidation(value: string) {
- const phoneControl = this.signupForm.get('user_phone');
- const emailControl = this.signupForm.get('user_email');
- if (value == 'phone')
- {
- phoneControl.setValidators(Validators.required);
- emailControl.clearValidators();
- emailControl.setValidators(Validators.email);
- }
- else
- {
- phoneControl.clearValidators(); emailControl.setValidators([Validators.email,Validators.required]);
- emailControl.setAsyncValidators(DuplicateEmailCheck.checkEmail(this._userServiceObj));
- }
- phoneControl.updateValueAndValidity();
- emailControl.updateValueAndValidity();
- }
Above method will first check the value of radio button is ‘Phone’ or not. If it is ‘Phone’ then we will apply (set) the validation for ‘Phone’ and remove (clear) the validation of ‘Email’ and vice versa. Notice the last two lines of above method. This updateValueAndValidity() method is inbuilt and this method will actually modify the form control’s validations and reflect the changes.
Ok. So, let me show you how it will actually look.
Dynamically adding form fields
We are having a form and now we would like to add form fields dynamically to form. So this is much easier with Reactive forms and FormArray. FormArray is much like a FormGroup but the difference is that it is an array of FormControls or FormGroups.
For this, we are having an example where we will ask the user to fill their ‘Hobbies’ and will allow the user to add their new ‘Hobbies’ dynamically one by one.
First in order to use this, we have to import FormArray to our component (signup.component.ts).
- import { FormGroup, FormControl, FormArray } from '@angular/forms';
And now inside ngOnInit() we will simply have FormArray with its name:
- user_hobbies: new FormArray([])
And then, in the HTML (signup.component.html), we will add following.
- <div class="form-group">
- <!-- Hobbies -->
- <label for="hobbies">Hobbies</label>
- <div class="controls">
- <button id="hobbies" type="button" class="btn btn-secondary" (click)="onAddHobbiesClick()" style="width: 8%"> Add</button>
- <div formArrayName="user_hobbies" *ngFor="let item of getControls();let i = index">
- <input type="text" [formControlName]="i" class="form-control">
- <button type="button" class="btn btn-danger btn-sm pull" (click)="onRemoveHobbiesClick(i)">Remove</button>
- </div>
- </div>
- </div>
At first, we will have ‘Add’ button. By clicking this button it will create input box for Hobby run time. So, user can add as much Hobbies as they want. And with each input box of Hobby we are also providing ‘Remove’ button. So if user wants to remove the Hobby then they can do so by clicking ‘Remove’ button.
Also, notice getControls() method inside *ngFor. This method is having all the array elements or controls generated in user_hobbies array. So we are just iterating through all the controls and displaying them.
For ‘Add’ and ‘Remove’ buttons we have to specify their methods accordingly.
So, below is the code for signup.component.ts,
- getControls() {
- return (<FormArray>this.signupForm.get('user_hobbies')).controls;
- }
-
- onAddHobbiesClick() {
- if (this.signupForm.get('user_hobbies').value.length < 3) {
- const control = new FormControl(null);
- (this.signupForm.get('user_hobbies') as FormArray).push(control);
- } else {
- alert('You can add maximum 3 Hobbies');
- }
- }
-
- onRemoveHobbiesClick(i) {
- (this.signupForm.get('user_hobbies') as FormArray).removeAt(i);
- }
Now as you can see above, interesting part is that we can treat FormArray just like a normal array and push and remove items from it.
Also, notice how we have restricted the user to add only 3 Hobbies. After adding 3 Hobbies if the user tries to add more, then it will show an alert message to him.
Thanks for reading this article. Hope it helps. You can find the full example on Github here,
Script For user table (MySql)
- CREATE TABLE `user_tbl` (`user_id` int(11) NOT NULL,`user_name` varchar(100) NOT NULL,`user_email` varchar(150) DEFAULT NULL,`user_password` varchar(100) NOT NULL,`user_phone` int(11) DEFAULT NULL,`user_gender` varchar(50) DEFAULT NULL,`user_hobbies` varchar(500) DEFAULT NULL,`user_city` varchar(50) DEFAULT NULL)