Card image cap

Working with Reactive Forms in Angular

Angular  • Posted 3 months ago

In a previous article, we discussed about what Template-driven forms are and how we can develop Forms for simpler use cases using Template-driven forms. We have also looked at the shortcomings of these forms and why we would need something much more powerful than these forms. In this article, let's talk about another kind of Forms that we can develop in Angular, called Reactive Forms. We shall look at how different Reactive Forms are when compared with Template-driven Forms and how we can build powerful Form interactions using Reactive Forms.

What are Reactive Forms?

Reactive Forms provide a Model based approach to handle form inputs and offer an immutable approach for handling the form state at any given point of time. This facilitates having consistent and predictable form state data for easy testing.

In a Reactive Form, the structure and behavior of the Form are defined by a "Model" designed in the Component Class, and it is wired up with the Template View - HTML. This is in contrast with the View handling the form state in the Template-driven forms.

We get the form data in the form of Observable streams of form values, and we can access these synchronously.

How Reactive Forms are helpful?

  • Reactive Forms provide a better solution for creating complex forms which require complex or customized validation logics.
  • These work on the concept of Observables which emit a new immutable value of the form state each time a form state changes, which is a lot better than a single mutable ngModel state that changes each time form is updated
  • Since the control of the form happens from the back-end instead of the HTML itself, its much more scalable and testable.

How Reactive Forms are different from Template-driven Forms?

In Template-driven forms, we have the input elements bound to an ngModel, which is responsible for managing the input values and the form state, and these Models are mutable - the original state changes without any backtracking.

Whereas in Reactive forms, we have no ngModel interaction - the form is built based on how we configure the FormControls and group them into FormGroups. The value changes over the time are maintained by these FormControls and FormGroups which provide immutable values - meaning that a new value state is created each time user input changes, and the results are published over Observable streams. This results in effective and clean way of working with the form state.

Working with Reactive Forms - Example:

To better understand how effective Reactive Forms are, let's create a User SignUp form in our SocialApp which accepts three inputs from the user - EmailAddress, Password and ConfirmPassword fields. Expectation is such that as soon as a user enters his EmailAddress in the form, the application must query the database whether this EmailAddress has already been registered or not and show respective message to the user.

To begin with, let's create a new component in our application which holds this SignUp form.

> ng generate component Signup

This creates a new component Signup with its component.ts, component.html, component.css added to the application.

Since we're working with Reactive Forms here, the modeling of the Forms happens in the component.ts first opposite to the Template-driven forms which rely on the ngModel for the modeling and state management.

"We need to now include Reactive Forms module inside the Module.ts as an import, in contrast to FormsModule used in Template-driven forms."

@NgModule({
    declarations: [
        SignupComponent
    ],
    imports: [
        ReactiveFormsModule,
        FormsModule,
        -----
    ]
})
export class AppModule { }

Building blocks - FormGroups and FormControls:

In Reactive Forms, we have FormControls and FormGroups.

  • FormControls - Input elements like a TextBox which the user interacts with.
  • FormGroups - A Logical grouping of one or more FormControls, which provides an encapsulation on the same. Helps us in adding logic which are applied across all controls within that group.

In a general scenario, a Form is a logical grouping of one or more Groups. So we have a parent FormGroup which contains one or more FormControls or FormGroups.

In our Signup Form, we use a single FormGroup which contains three FormControls namely - EmailAddress, Password and RetypePassword.

To create a FormGroup we use a FormBuilder, which is a built-in service provided by the Reactive Forms module.

The declaration as a whole looks like below:

@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {

  signupForm: FormGroup;

  constructor(
      private formBuilder: FormBuilder
      -----
    ) { }

  ngOnInit(): void {
    this.signupForm = this.formBuilder.group({
      email: new FormControl(
        '',
        [Validators.email, Validators.required, Validators.pattern("[^ @]*@[^ @]*")]
      ),
      password: new FormControl('',
        [Validators.required]),
      retypepassword: new FormControl('',
        [Validators.required,])
    });
  }
}

Let's try to understand what's happening here: we have the this.formBuilder.group() method which returns a FormGroup object assigned to signupForm field. The formBuilder.group() takes an object where we add the FormControls as "key-value" pairs of formControlName and the FormControl it points to.

Each FormControl() object takes two parameters - one is the default value and the second is an array of Validators which are to be applied on to the input which is mapped to this FormControl.

Angular provides us with several built-in Validators as well as option to create our own Validators: which we shall look at soon.

With this declaration, we have created three FormControls email, password and retypepassword which point to respective FormControl instances.

Complementing these FormControls, let's also add a few Getter properties which return if the FormControl is in a valid state or not.

get isInvalidEmail() {
    let emailCtrl: AbstractControl = this.signupForm.get('email');
    return emailCtrl.invalid && this.isEmailDirty;
}

get isEmailDirty() {
    let emailCtrl: AbstractControl = this.signupForm.get('email');
    return emailCtrl.dirty;
}

get isInvalidPassword() {
    let pwdCtrl: AbstractControl = this.signupForm.get('password');
    return pwdCtrl.invalid && pwdCtrl.dirty;
}

get isInequalPasswords() {
    let pwd = this.signupForm.get('password');
    let rep_pwd = this.signupForm.get('retypepassword');
    return pwd.dirty && rep_pwd.dirty && pwd.value != rep_pwd.value
}

Observe how we're accessing each FormControl within the FormGroup represented by signupForm field. To access a single FormControl we can use the FormGroup.get('[formControlName]') method which accepts the formControlName (Key) as the parameter and returns an "AbstractControl" which is the base type of the FormControl.

In the HTML, we use these formControlNames defined within the FormGroup to map the HTML input element to their respective FormControls.

The final HTML looks like below. Not to worry about the weird properties and conditions we've added in the HTML - we'd get to there in a moment.

<div class="row">
    <div class="col-md-6 col-md-offset-6 mx-auto bg-light p-4">
        <form (ngSubmit)="signup()" class="form" [formGroup]="signupForm">
            <div class="input-group mb-3">
                <input formControlName="email" type="email"
                    [ngClass]="{
                            'border-danger':isEmailDirty && (isInvalidEmail || isEmailAlreadyExists),
                            'border-success':isEmailDirty && !(isInvalidEmail || isEmailAlreadyExists)
                        }" 
                    
                    class="form-control"
                    placeholder="user@example.com" name="email" aria-describedby="basic-addon2">
                <div class="input-group-append" *ngIf="isEmailDirty && !isInvalidEmail && !isEmailAlreadyExists">
                    <span class="input-group-text bg-success text-light" id="basic-addon2">&#10003;</span>
                </div>
                <div class="input-group-append" *ngIf="isEmailDirty && isEmailAlreadyExists">
                    <span class="input-group-text bg-danger text-light" id="basic-addon2">&#10005;</span>
                </div>
            </div>
            <div class="form-group">
                <input formControlName="password" type="password" class="form-control" placeholder="Password"
                    name="password">
                <span *ngIf="isInvalidPassword" class="text-danger">invalid Password!</span>
            </div>
            <div class="form-group">
                <input formControlName="retypepassword" type="password" class="form-control"
                    placeholder="Retype Password" name="retypepassword">
                <span *ngIf="isInequalPasswords" class="text-danger">Passwords don't match!</span>
            </div>
            <button [disabled]="signupForm.invalid" class="btn btn-primary" type="submit">Signup</button>
        </form>
    </div>
</div>

Observe that we use the directive "formControlName" to assign a HTML element to its representing FormControl inside the FormGroup. Also, at the form level we have the "formGroup" directive which we bind to the signupForm field in the component.ts class.

When we run this setup, the form looks like below:

data/Admin/2020/8/signup-form-1.png

Creating Custom Validators:

Next, we shall create and bind our own validator function that triggers when the user is entering passwords - to check whether the two passwords match or not.

A Custom Validator function takes an AbstractControl as a parameter and does some operation. The validation result is done like this - if the condition is met, the validator function returns null, otherwise it returns an object which is added to the FormControl.errors array.

export function checkIfPasswordsMatchValidator(c: AbstractControl) 
{
    if (c.get('password').value === c.get('retypepassword').value)
        return null;
    else
        return { 'passwordsMatch': false };
}

The FormGroup now looks like this:

this.signupForm = this.formBuilder.group({
    email: new FormControl(
    '',
    [
        Validators.email, 
        Validators.required, 
        Validators.pattern("[^ @]*@[^ @]*")],
    ),
    password: new FormControl('',
    [Validators.required]),
    retypepassword: new FormControl('',
    [Validators.required,])
    }, { validators: checkIfPasswordsMatchValidator });

Since this Validator works at the FormGroup level, we pass it to the FormGroup validators and hence the Validator function gets the FormGroup instance as a parameter and our logic works.

Implementing Async Valiations - AsyncValidators:

Now that the initial setup is done, let's get back to our requirement - checking if the EmailAddress is already taken, which we shall bind to the mark at the end of the EmailAddress TextBox.

To do this, we shall create an Asynchronous Validator, which would make a HTTP API call to the server to validate the EmailAddress (since the API holds the logic to query on the database) and display the validation response accordingly.

Let's start by creating two services which take the responsibility of validating incoming data by means of a HTTP API call. The methods look like below:

@Injectable({ providedIn: "root" })
export class AuthService {
    private apiUri = "http://localhost:3000/api";
    constructor(private http: HttpClient) { }
    
    checkIfEmailAlreadyTaken(email: string): Observable<boolean> {
        return this.http.post<EmailCheckResponse>(`${this.apiUri}/auth/isuniquemail`, {
            "EmailAddress": email
        }).pipe(map((res) => {
            return res.res;
        }));
    }
}
@Injectable({ providedIn: "root" })
export class CheckIfEmailExistsValidator {
    constructor(private authService: AuthService) { }

    checkIfEmailAlreadyTaken(email: string): Observable<boolean> {
        return this.authService.checkIfEmailAlreadyTaken(email);
    }
}

The CheckIfEmailExistsValidator.checkIfEmailAlreadyTaken() method takes the EmailAddress input and then internally calls the AuthService.checkIfEmailAlreadyTaken() method which encapsulates the HttpClient logic.

The response is of type EmailCheckResponse which has a single boolean property res. The checkIfEmailAlreadyTaken() method returns the boolean property value we receive from the API call.

export interface EmailCheckResponse {
    res: boolean;
}

Now we need to link this CheckIfEmailExistsValidator service in our EmailAddress FormControl so that the FormControl calls this service on input.

To do this, let's add a method in the Signup component that takes in an AbstractControl input and then performs the validation.

isEmailExists(control: AbstractControl) {
    return this.emailValidator.checkIfEmailAlreadyTaken(control.value)
    .pipe(map((res: boolean) => res ? null : { isEmailAlreadyExists: true }));
}

Observe the content of the pipe(map()) method, where we're receiving the boolean response from the checkIfEmailAlreadyTaken() method. This is the Validator function which performs the Asynchronous validation logic on the FormControl.

The validator works in this way: if the Validator returns null, it means that the input is valid; otherwise the input is invalid and the FormControl would add 'isEmailAlreadyExists' in its errors property.

To link this method inside the FormControl(), we use the fourth parameter in the FormControl() constructor which takes in an async validator.

this.signupForm = this.formBuilder.group({
    email: new FormControl(
    '',
    [Validators.email, Validators.required, Validators.pattern("[^ @]*@[^ @]*")],
    this.isEmailExists.bind(this)
    ),
    password: new FormControl('',
    [Validators.required]),
    retypepassword: new FormControl('',
    [Validators.required,])
    }, { validators: checkIfPasswordsMatchValidator });

The method this.isEmailExists.bind(this), passes the FormControl it is being invoked in - which is EmailAddress in this case and binds itself to the control. The ensures that the Validator is now "linked" to the FormControl.

Complementing this validation, we shall add a few more Getter properties that represent whether the EmailAddress is now taken or not.

get isEmailAlreadyExists() {
    let emailCtrl: AbstractControl = this.signupForm.get('email');
    console.log(emailCtrl.errors);
    return emailCtrl.hasError('isEmailAlreadyExists') && this.isEmailDirty;
}

get isInequalPasswords() {
    let pwd = this.signupForm.get('password');
    let rep_pwd = this.signupForm.get('retypepassword');
    return pwd.dirty && rep_pwd.dirty && pwd.value != rep_pwd.value
}

We use these properties in our HTML to simplify the validation conditions to show up the message in our Signup form. In this way, we can create powerful interactive user input forms which can also hold complex and customized validation logic over the user input using the Reactive Forms. We can polish these forms a bit more further, which shall transform this setup into something much more decoupled and configurable, called Dynamic Forms - which is a topic for some other day.

The complete example is available in the repo: https://github.com/referbruv/reactive-forms-in-angular-example

Enjoying my posts?
You can now show me your support! 😊

What is the difference between parseInt() and number()?

parseInt() and Number() are both used to convert a string into a number. * parseInt() parses the value of the string and converts to number till th ...


How can you work with cookies in Angular?

To work with Cookies, we can make use of the CookieService which is a part of the ngx-cookie-service module. * Install: `npm install --save ngx-coo ...


What is the difference between double equals (==) and triple equals (===) ?

In JavaScript, double equal operator (==) stands for value compare. The operands on either sides are converted into a common type and are compared for ...


What are tree shakeable providers?

When we register providers in @NgModule(), angular internally includes all those related classes in the final bundle, irrespective of their actual use ...


What are Pure Pipes and Impure Pipes?

Pipes by default are "Pure" - they can't detect changes when a value of the primitive type or the reference of the complex type changes. Example: when ...


We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept