Introduction
As an Angular 1 / ES5 developer, I was watching many news coming up about Angular 2, ES6, TypeScript and my first interest was on how to build a reusable form component that wraps certain behaviors and looks commonly appearing throughout my application and makes it to integrate well in a form using these new technologies. For example, here in New Zealand, bank account number is 15 or 16 digits comprised of bank / branch / body / suffix numbers and it has its validation rule defined. How can I wrap this custom validation behavior into Angular 2 attribute directive and / or how can I write Angular 2 component that captures bank account number and validates?
Using the code
We will see three ways to capture and validate a bank account number:
- Write an attribute directive and use it in a plain input element
- Write a model-driven form component with 4 input elements
- Write a template-driven form component with 4 input elements
Our final form will look like below and you can play on this plunker.
As a prerequisite, we are going to create an injectable AccountService
in bank-account.service.ts which exposes isValid
function and its signature looks like:
public isValid(bank: string, branch: string, body: string, suffix: string): boolean { }
It also exports BankAccount
interface:
export interface BankAccount { acc1: string, acc2: string, acc3: string, acc4: string }
We assume here that the only valid account number is “0865231954512001”, so isValid
will return true if the given number is exactly the same as above number, otherwise return false. Having this separated service would be a good practice as it makes our code testable and reusable.
Our form will capture 4 fields - name and 3 account numbers in 3 different ways and our domain model would look like:
vmName: string = 'Bob Lee';
vmAccount1: string = '';
vmAccount2: BankAccount = { acc1: '08', acc2: '6523', acc3: '1954512', acc4: '001' };
vmAccount3: BankAccount = { acc1: '', acc2: '', acc3: '', acc4: '' };
First way - attribute directive
Below is a simplified version of our form template in app.component.html:
<form #theForm="ngForm" name="theForm" novalidate (ngSubmit)="submit(theForm)">
<div>
<label>Name</label>
<input type="text" name="name" [(ngModel)]="vmName" />
</div>
<div>
<label>Account 1</label>
<input type="text" name="account1" [(ngModel)]="vmAccount1" />
</div>
<button type="submit">Submit</button>
</form>
This is a template-driven form, Name and Account1 fields are two-way-bound to our models vmName
and vmAccount1
by the “banana-in-the-box” ngModel, #theForm
in opening form tag is template reference variable which refers to NgForm instance of our form and (ngSubmit) is event binding syntax to handle submit event.
Now we want to add validation behavior to our Account 1 field. According to Angular 2 doc:
Quote:
An Attribute directive changes the appearance or behavior of a DOM element.
In this case, we need some bank-account-specific behaviors like accepting only numeric keys and validating entered number as bank account. Let’s create our attribute directive validateAccount
in bank-account-validator.directive.ts file and we want to use it on our Account 1 input element like:
<input type="text" name="account1" ngModel validateAccount />
Note that Angular 2 requires to put ngModel directive along with our custom directive to act as validator. A new file bank-account-validator.directive.ts looks like:
import { Directive } from '@angular/core';
@Directive({
selector: '[validateAccount][ngModel]',
})
export class AccountValidator {
}
To limit key press to accept only numeric keys, we need to handle keypress event on the input element and Angular 2 provides us HostListener decorator, so in AccountValidator
class, we can have onKeypress event handler and decorate it like:
@HostListener('keypress', ['$event'])
onKeypress(event) {
ignoreSome(event);
}
Account 1 field is ready to ignore non-numeric keys but still allow left, right, backspace keys.
Now to add validation behavior, we need to implement Validator interface. Angular 2 sources says:
export interface Validator { validate(c: AbstractControl): {[key: string]: any}; }
Our Validator implementation looks like:
export class AccountValidator {
validator: Function;
constructor(accountService: AccountService) {
this.validator = validateAccountFactory(accountService);
}
validate(c: FormControl) {
return this.validator(c);
}
}
function validateAccountFactory(accountService: AccountService) {
return (c: FormControl) => {
if (!c || !c.value) return null;
let invalid = { validateAccount: { valid: false } };
if (c.value.length === 15 || c.value.length === 16) {
let acc = c.value,
acc1 = acc.slice(0, 2),
acc2 = acc.slice(2, 6),
acc3 = acc.slice(6, 13),
acc4 = acc.slice(13);
if (accountService.isValid(acc1, acc2, acc3, acc4))
return null;
}
return invalid;
};
}
If you first look at validateAccountFactory
function, it returns a function that takes in FormControl (of our Account1 input field) as an input argument, then validates the FormControl’s value using AccountService.isValid
function.
AccountValidator
class gets AccountService
injected into its constructor and assigns a function from validateAccountFactory
to validator variable, then validate() just uses validator to return the boolean result.
Last step to make this directive to really work would be to hook our Validator implementation to NG_VALIDATORS token by adding below line to providers metadata. Further explanation on this can be found in this article:
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountValidator), multi: true }
What will happen from now on is that each time you enter any digit into the field, the form would promptly know if entered number is a valid bank account number or not. So when user submits the form, NgForm instance representing the form would know the form is invalid and one of the reasons would be invalid bank account. As submit handler logs the NgForm object, if you inspect it on dev tools console, you would see some interesting properties:
- NgForm.submitted: true
- NgForm.invalid: true
- NgForm.value: { account1: “0865231954514001”, name: “Bob Lee” }
- NgForms.controls.account1.errors.validateAccount = { value: false }
- NgForm.valueChanges: EventEmitter
On the template, we can use template reference variable #theForm
to access NgForm instance of the form, #account1
to access NgModel instance of Account1 input field. So if we want to show any validation errors on submit, we can use them like:
<div [hidden]="!theForm.submitted">
<div class="text-danger" [hidden]="account1.valid || !account1.errors.validateAccount">
Bank account number is not valid
</div>
</div>
Notice that bank-account-validator.directive.ts also exports validateAccounGroupFactory
function which is similar to validateAccountFactory
except it takes in FormGroup not FormControl. This function will be used in components that we will implement shortly.
Second way - model-driven component
In above, we have used one input element to capture a whole account number and added custom validation behavior by creating an attribute directive and adding it to the input element.
What if we want to use 4 input elements to capture bank / branch / body / suffix number separately?
Then it makes sense to create a custom component that has its own template and does the same validation as seen above. We can use the component in our form like:
<bank-account-model-driven name="account2" [(ngModel)]="vmAccount2">
</bank-account-model-driven>
The component is two way bound to our domain model vmAccount2
which is BankAccount
type. So when our page is loaded, vmAccount2
value will be shown in the view by property binding, when the form is submitted, vmAccount2
should have valid account number from the view by event binding:
{ acc1: '08', acc2: '6523', acc3: '1954512', acc4: '001' }
Let’s create AccountModelDrivenComponent
in the following two files:
- app/bank-account-model-driven.component.ts
- app/bank-account-model-driven.component.html
import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component ({
selector: 'bank-account-model-driven',
templateUrl: 'app/bank-account-model-driven.component.html'
})
export class AccountModelDrivenComponent {
}
<div [formGroup]="accountNumber">
<label>Account 2</label>
<div class="form-inline">
<input type="text" formControlName="acc1" />
<input type="text" formControlName="acc2" />
<input type="text" formControlName="acc3" />
<input type="text" formControlName="acc4" />
</div>
</div>
In the component template, we have one FormGroup accountNumber
and inside it, 4 FormControls - acc1
, acc2
, acc3
, acc4
. This is analogous to our domain model’s BankAccount
type.
Then in the component class, we explicitly build our form using FormBuilder:
export class AccountModelDrivenComponent implements OnInit {
accountNumber: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.accountNumber = this.formBuilder.group({
acc1: '',
acc2: '',
acc3: '',
acc4: ''
});
}
This is a new way of writing a form that Angular 2 introduces - model-driven form which is a bit different to template-driven form which should be familiar to Angular 1 developers. Basically in model-driven form, template tends to be simpler with no ngModel, component tends to be verbose as you have to express clearly what you want to do there.
To make this component to act as a single form control in parent form, we need to implement ControlValueAccessor interface. Angular 2 source says:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
}
Angular 2 would call writeValue() to propagate model value to view and call registerOnChange() to register a handler function that would propagate any change on view to model. Further explanation on this can be found in here.
Our ControlValueAccessor implementation looks like:
writeValue(value: BankAccount) {
if (value) {
this.accountNumber.setValue(value);
}
}
registerOnChange(fn: (value: any) => void) {
this.accountNumber.valueChanges.subscribe(fn);
}
Angular 2 provides us two important FormGroup properties here - setValue
and valueChanges
. FormGroup.setValue() would cleverly write values from the given BankAccount
model to each FormControl view. FormGroup.valueChanges is an observable that enables Angular 2 to update model value and validity on any view changes by subscribing its handler.
Now to make the component to validate bank account number, we implement Validator interface using validateAccounGroupFactory
function imported from bank-account-validator.directive:
@Input() myRequired: boolean;
validator: Function;
constructor(accountService: AccountService, private formBuilder: FormBuilder) {
this.validator = validateAccountGroupFactory(accountService);
}
validate(c: FormGroup) {
return this.validator(c, this.myRequired);
}
To hook our Validator implementation to NG_VALIDATORS token, add below line to providers metadata:
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountModelDrivenComponent), multi: true }
Now our component is ready to be dropped in the form to capture and validate a bank account number.
Third way - template-driven component
What if we want to do the same thing as we have done in the second way above but using our beloved template-driven way? This was my last challenge as well and thanks to guy answered my stackoverflow question.
Let’s create AccountTemplateDrivenComponent
in the following two files:
- app/bank-account-template-driven.component.ts
- app/bank-account-template-driven.component.html
import { Component } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
@Component ({
selector: 'bank-account-template-driven',
templateUrl: 'app/bank-account-template-driven.component.html'
})
export class AccountTemplateDrivenComponent {
}
<form>
<div ngModelGroup="accountNumber">
<label>Account 3</label>
<div class="form-inline">
<input type="text" name="acc1" [ngModel]="accountNumber.acc1" (ngModelChange)="change('acc1', $event)" />
<input type="text" name="acc2" [ngModel]="accountNumber.acc2" (ngModelChange)="change('acc2', $event)" />
<input type="text" name="acc3" [ngModel]="accountNumber.acc3" (ngModelChange)="change('acc3', $event)" />
<input type="text" name="acc4" [ngModel]="accountNumber.acc4" (ngModelChange)="change('acc4', $event)" />
</div>
</div>
</form>
In the component template, we have one ngModelGroup accountNumber
and inside it, 4 ngModels - acc1
, acc2
, acc3
, acc4
. Also instead of doing “banana-in-the-box”, I had to split into property binding and event binding to handle view changes on the component. ControlValueAccessor implementation of this component looks like:
export class AccountTemplateDrivenComponent implements ControlValueAccessor {
accountNumber = {
acc1: '',
acc2: '',
acc3: '',
acc4: ''
}
constructor(accountService: AccountService) {
this.validator = validateAccountGroupFactory(accountService);
}
change(prop, value) {
this.accountNumber[prop] = value;
this.propagateChange(this.accountNumber);
}
writeValue(value: BankAccount) {
if (value) {
this.accountNumber = value;
}
}
propagateChange = (_: any) => {};
registerOnChange(fn: (value: any) => void) {
this.propagateChange = fn;
}
Remember in model-driven way, we have subscribed to FormGroup.valueChanges observable to propagate view change and trigger validation? In template-driven way, we don’t have that facility and the same thing should be done kind of manually as shown here. Without handling ngModelChange event by change
function above, the component would propagate its view change to model ok but validation would not be triggered.
Validator implementation will be exactly the same as in the second way.
Summary
We have seen how to write an attribute directive and custom components in Angular 2 framework using model-driven form and template-driven form. To write a component, I would prefer model-driven way as its template becomes simpler and it seems like the component has better faciltity like setValue and valueChanges to express how the component should behave.
.NET developers should feel comfortable with TypeScript’s syntax around type, interface, class and it didn't take long that I feel like appreciate its compile-time checking, readability and explicit abstraction.
Thanks for reading.
History
- 30Aug16 Initial version
- 31Aug16 Fixed single / double quotes in code blocks and typo
- 03Sep16 Added third example using mode-driven template
- 24Sep16 Added decent example of template-driven form component