This tutorial is out of date and no longer maintained.
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.
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:
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.
- npm install @angular/forms --save
Here’s the module for our application 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 { }
Let’s move on to create our app component.
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);
}
}
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.PAYMENT_METHOD_TYPE
to hold the payment method type information.This is how our HTML view will look:
<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>
All set! Now let’s implement our model-driven form.
...
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;
}
...
initPaymentMethodCardModel
and initPaymentMethodBankModel
will initialize the card/bank fields’ value and validations.initPaymentMethodFormGroup
initializes the payment method form group.ngOnInit
and assign it to myForm
.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.
...
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.
Let’s add all of the controls to our view.
...
<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>
...
setPaymentMethodType
function to the Card and Bank button click event.paymentMethod
to the formGroupName
,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
.
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.
...
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();
});
}
});
}
...
initPaymentMethodCardModel
and initPaymentMethodBankModel
to individual functions at the beginning. We can reuse the model here to retrieve the validation.FormControl
exposes a function call setValidators
. We will use this function to update the validation rules.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.
Now that we have declared subscribePaymentTypeChanges()
function. Let’s apply it during ngOnInit()
and set the default payment method type to bank.
...
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);
}
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.
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!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.