How to Implement Conditional Validation in Angular 2 Model-driven Forms

How to Implement Conditional Validation in Angular 2 Model-driven Forms

Free Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

In this article, we will learn about how to handle conditional validation in our model-driven form using the latest forms module. If you are new to Angular 2 model-driven forms, please refer to Using Angular 2's Model-Driven Forms to get to know the basics.

Introduction

We will build a form to capture a customer payment method based on this interface.

// customer.interface.ts

export interface Customer {
    name: stirng;
    paymentMethod: {
        type: string; // must be either 'bank' or 'card'
        card: {
            cardNo: string; // must be visa, master, amex
            cardHolder: string;
            expiry: string; // must be format MM/YY
        },
        bank: {
            accountNo: string;
            accountHolder: string;
            routingNo: string;
        }
    }
}

Here is how the UI will look:

Select Payment Method Bank

Select Payment Method Card

Requirements

  1. A customer must select either bank or card type.
  2. Set default payment method type to bank.
  3. All the bank fields are mandatory when bank is selected.
  4. All the card fields are mandatory when card is selected.
  5. Show error message when the field is invalid.

App Setup

As of RC.2 - RC.4, deprecated forms is enabled by default.

Here's our file structure:

|- app/
    |- app.component.html
    |- app.component.ts
    |- app.module.ts
    |- main.ts
    |- customer.interface.ts
|- index.html
|- styles.css
|- tsconfig.json

In order to use new forms module, we need to npm install @angular/forms npm package and import the reactive forms module in application module.

$ npm install @angular/forms --save

Here's the module for our application app.module.ts:

// app.module.ts

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

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

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

export class AppModule { }

The App Component

Let's move on to create our app component.

// app.component.ts

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 
import { Customer } from './customer.interface'; // our customer model interface

@Component({
    moduleId: module.id,
    selector: 'app-root',
    templateUrl: 'app.component.html'
})
export class AppComponent implements OnInit {
    public myForm: FormGroup; // our model driven form

    // standing data
    public PAYMENT_METHOD_TYPE = {
    BANK: 'bank',
    CARD: 'card'
    };

    constructor(private _fb: FormBuilder) { } // form builder simplify form initialization

    ngOnInit() {
        // we will initialize our form model here
    }

    save(model: Customer, isValid: boolean) {
        // call API to save
        // ...
        console.log(model, isValid);
    }
}

Notes

  1. Notice that we import FormGroup, FormBuilder, Validators,
    • FormBuilder as its given name, we use this to build our form.
    • Validators contains all the default validators (e.g. required, minlength, etc) that Angular provide us.
    • FormGroup. All the form is of type FormGroup. Therefore myForm is a of type FormGroup. We import this to strong type our form model.
  2. We defined an object PAYMENT_METHOD_TYPE to hold the payment method type information.

The HTML View

This is how our HTML view will look:

<!-- app.component.html -->
<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm.value, myForm.valid)">

    <!-- We'll add our form controls here -->
    <div>
        <button type="submit" [disabled]="!myForm.valid">Submit</button>
    </div>
</form>

Implementation

All set! Now let's implement our model-driven form.

Initialize the Form Model

// app.component.ts
...

ngOnInit() {
    // we will initialize our form model here
    this.myForm = this._fb.group({
        name: ['Jane Doe'],
        paymentMethod: this.initPaymentMethodFormGroup()
    });
}

initPaymentMethodFormGroup() {
    // initialize payment method form group
    const group = this._fb.group({
        type: [''],
        card: this._fb.group(this.initPaymentMethodCardModel()),
        bank: this._fb.group(this.initPaymentMethodBankModel()),
    });

    return group;
}

initPaymentMethodCardModel() {
    // initialize card model

    // regex for master, visa, amex card
    // you get valid testing credit card from http://www.getcreditcardnumbers.com/
    const cardNoRegex = `^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$`;

    // regex for expiry format MM/YY
    const expiryRegex = `^(0[1-9]|1[0-2])\/?([0-9]{4}|[0-9]{2})$`;

    const model = {
        cardNo: ['', [Validators.required, Validators.pattern(cardNoRegex)]],
        cardHolder: ['', Validators.required],
        expiry: ['', [Validators.required, Validators.pattern(expiryRegex)]]
    };

    return model;
}

initPaymentMethodBankModel() {
    // initialize bank model
    const model = {
        accountNo: ['', Validators.required],
        accountHolder: ['', Validators.required],
        routingNo: ['', Validators.required]
    };

    return model;
}

...

Notes

  1. We split our initialization to a few different functions.
    • initPaymentMethodCardModel and initPaymentMethodBankModel will initialize the card/ bank fields' value and validations.
    • initPaymentMethodFormGroup initializes the payment method form group.
    • We then initialize our form model in ngOnInit and assign it to myForm.

Adding Set Payment Method Type function

When a user clicks on the Card or Bank button, we need to update the payment method type field accordingly. Let's create a new function in our component.

// app.component.ts
..
setPaymentMethodType(type: string) {
    // update payment method type value
    const ctrl: FormControl = (<any>this.myForm).controls.paymentMethod.controls.type;
    ctrl.setValue(type);
}

..

Whenever the user clicks on the Card or Bank button, we will pass the selected payment method type to setPaymentMethodType. Later we will bind this in our HTML view.

Adding Controls to the View

Let’s add all of the controls to our view.

<!-- app.component.html -->
...

<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm.value, myForm.valid)">
    <!-- We'll add our form controls here -->

    <!--name-->
    <div>
        <label>Name</label>
        <input type="text" formControlName="name">
    </div>

    <!--payment method-->
    <div>
        <label>Payment Method</label>
    </div>

    <div formGroupName="paymentMethod">

        <!--payment method type button-->
        <div class="row">
            <div class="col-xs-6">
                <button type="button" (click)="setPaymentMethodType(PAYMENT_METHOD_TYPE.BANK)">
                {{ PAYMENT_METHOD_TYPE.BANK }}
                </button>
            </div>
            <div class="col-xs-6">
                <button type="button" (click)="setPaymentMethodType(PAYMENT_METHOD_TYPE.CARD)">
                {{ PAYMENT_METHOD_TYPE.CARD }}
                </button>
            </div>
        </div>

        <!--payment method: BANK-->
        <div *ngIf="myForm.controls.paymentMethod.controls.type.value === PAYMENT_METHOD_TYPE.BANK">
            <div class="panel-body">

                <!--Bank account no-->
                <div formGroupName="bank">
                    <label>Account no.</label>
                    <input type="text" formControlName="accountNo">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.bank.controls.accountNo.valid">
                    Required.
                    </small>
                </div>

                <!--Bank routing no-->
                <div formGroupName="bank">
                    <label>Routing no.</label>
                    <input type="text" formControlName="routingNo">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.bank.controls.routingNo.valid">
                    Required.
                    </small>
                </div>

                <!--Bank account holder-->
                <div formGroupName="bank">
                    <label>Name</label>
                    <input type="text" formControlName="accountHolder">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.bank.controls.accountHolder.valid">
                    Required.
                    </small>
                </div>
            </div>
        </div>

        <!--payment method: CARD-->
        <div  *ngIf="myForm.controls.paymentMethod.controls.type.value === PAYMENT_METHOD_TYPE.CARD">
            <div class="panel-body">

                <!--Card no-->
                <div formGroupName="card">
                    <label>Card no.</label>
                    <input type="text" formControlName="cardNo">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.card.controls.cardNo.valid">
                    Required (Must be valid card number).
                    </small>
                </div>

                <!--Card expiry-->
                <div formGroupName="card">
                    <label>Expiry</label>
                    <input type="text" formControlName="expiry">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.card.controls.expiry.valid" 
                    class="text-danger">
                    Required (Must be in format MM/YY).
                    </small>
                </div>

                <!--Card holder-->
                <div formGroupName="card">
                    <label>Name</label>
                    <input type="text" formControlName="cardHolder">
                    <small *ngIf="!myForm.controls.paymentMethod.controls.card.controls.cardHolder.valid">
                    Required.
                    </small>
                </div>

            </div>
        </div>
    </div>

    <div>
        <button type="submit" [disabled]="!myForm.valid">Submit</button>
    </div>
</form>

...

Notes

  1. We bind the setPaymentMethodType function to the Card and Bank button click event.
  2. We bind paymentMethod to the formGroupName,
  3. Each form controls bind to input with formControlName directive and show an error message when it's invalid.

Refer to Using Angular 2's Model-Driven Forms for more explanation on formGroup, formGroupName and formControlName.

Apply Conditional Validation when Payment Method Type Change

Now, let's implement our conditional validation function. As mentioned in Using Angular 2's Model-Driven Forms, each form control exposes the valueChanges event, which we can subscribe to. We will also subscribe to the payment method type value change event. Each time the payment method type changes, we will update our validation accordingly. Let's implement the subscribePaymentTypeChanges function.

// app.component.ts
..

subscribePaymentTypeChanges() {
    // controls
    const pmCtrl = (<any>this.myForm).controls.paymentMethod;
    const bankCtrl = pmCtrl.controls.bank;
    const cardCtrl = pmCtrl.controls.card;

    // initialize value changes stream
    const changes$ = pmCtrl.controls.type.valueChanges;

    // subscribe to the stream
    changes$.subscribe(paymentMethodType => {
        // BANK
        if (paymentMethodType === this.PAYMENT_METHOD_TYPE.BANK) {
            // apply validators to each bank fields, retrieve validators from bank model
            Object.keys(bankCtrl.controls).forEach(key => {
                bankCtrl.controls[key].setValidators(this.initPaymentMethodBankModel()[key][1]);
                bankCtrl.controls[key].updateValueAndValidity();
            });

            // remove all validators from card fields
            Object.keys(cardCtrl.controls).forEach(key => {
                cardCtrl.controls[key].setValidators(null);
                cardCtrl.controls[key].updateValueAndValidity();
            });
        }

        // CARD
        if (paymentMethodType === this.PAYMENT_METHOD_TYPE.CARD) {
            // remove all validators from bank fields
            Object.keys(bankCtrl.controls).forEach(key => {
                bankCtrl.controls[key].setValidators(null);
                bankCtrl.controls[key].updateValueAndValidity();
            });

            // apply validators to each card fields, retrieve validators from card model
            Object.keys(cardCtrl.controls).forEach(key => {
                cardCtrl.controls[key].setValidators(this.initPaymentMethodCardModel()[key][1]);
                cardCtrl.controls[key].updateValueAndValidity();
            });
        }

    });
}

...

Notes

  1. Remember we split the initPaymentMethodCardModel and initPaymentMethodBankModel to individual functions at the beginning. We can reuse the model here to retrieve the validation.
  2. FormControl exposes a function call setValidators. We will use this function to update the validation rules.
  3. Calling the setValidators DOESN'T trigger any update or value change event. Therefore, we need to call updateValueAndValidity to trigger the update.

For example, by default, Bank type is selected and all its fields are mandatory. If we enter values to all the bank fields, the form status will be updated to VALID. Then, we click on Card button. The form status should be updated to INVALID because we have not entered any values to card fields yet.

If we did not apply updateValueAndValidity, the form status will remain as VALID because we only updated the validation rules of each field. We did not trigger any updates.

Subscribe changes and Set Default Payment Method Type

Now that we have declared subscribePaymentTypeChanges() function. Let's apply it during ngOnInit() and set the default payment method type to bank.

// app.component.ts
..

ngOnInit() {
    ...
    // after form model initialization

    // subscribe to payment method type changes
    this.subscribePaymentTypeChanges();

    // set default type to BANK
    this.setPaymentMethodType(this.PAYMENT_METHOD_TYPE.BANK);
}

Summary

That's it. This is how we can handle conditional validation in model driven form. Happy coding!

Jecelyn Yeen

Coder. Diver. Board Game Lover.

Speak English, Mandarin, JavaScript, Typescript, C# and more.

Problem solver at @iflix.