Tutorial

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

Draft updated on Invalid Date
Default avatar

By Jecelyn Yeen

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

This tutorial is out of date and no longer maintained.

Introduction

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.

View Angular 2 - Conditional Validation (final) scotch on plnkr

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 an error message when the field is invalid.

App Setup

As of RC.2 - RC.4, deprecated forms are 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 the new forms module, we need to npm install @angular/forms npm package and import the reactive forms module in the application module.

  1. 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);
	}
}
  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 provides us.
    • FormGroup. All the form is of type FormGroup. Therefore myForm is a type of 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;
}

...
  1. We split our initialization into 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>
...
  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();
			});
		}

	});
}
...
  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 into 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);
}

Conclusion

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

View Angular 2 - Conditional Validation (final) scotch on plnkr

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Jecelyn Yeen

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel