Click here to Skip to main content
13,634,008 members
Click here to Skip to main content
Add your own
alternative version

Stats

38.9K views
12 bookmarked
Posted 29 Aug 2016
Licenced CPOL

Angular 2 custom component with validation for bank account number

, 23 Sep 2016
Rate this:
Please Sign up or sign in to vote.
Building Angular 2 attribute directive and two components in model-driven and template-driven way to capture and validate bank account number across multiple input fields

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:

  1. Write an attribute directive and use it in a plain input element
  2. Write a model-driven form component with 4 input elements
  3. Write a template-driven form component with 4 input elements

Our final form will look like below and you can play on this plunker.

Final form

 

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:

// An interface that can be implemented by classes that can act as validators.
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; // empty is valid
    
    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; // valid
    }
    
    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?

Multiple inputs

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:

// A bridge between a control and a native element.
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

bob.bumsuk.lee
Software Developer
New Zealand New Zealand
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionAccess native input element from validator? Pin
ALGR7-Dec-17 8:05
memberALGR7-Dec-17 8:05 
QuestionHow do we use the second example inside a form? Pin
Member 1095575726-Jun-17 8:34
memberMember 1095575726-Jun-17 8:34 
SuggestionMissing module information Pin
Thava Rajan28-Apr-17 16:08
professionalThava Rajan28-Apr-17 16:08 
QuestionBoth model & template driven Pin
Member 1307184420-Mar-17 7:02
memberMember 1307184420-Mar-17 7:02 
QuestionA very good article Pin
yaroslavya2-Oct-16 8:41
memberyaroslavya2-Oct-16 8:41 
AnswerRe: A very good article Pin
bob.bumsuk.lee2-Oct-16 9:22
memberbob.bumsuk.lee2-Oct-16 9:22 
GeneralRe: A very good article Pin
yaroslavya2-Oct-16 9:26
memberyaroslavya2-Oct-16 9:26 
QuestionThoughts Pin
koo91-Sep-16 10:31
memberkoo91-Sep-16 10:31 
AnswerRe: Thoughts Pin
bob.bumsuk.lee1-Sep-16 11:16
memberbob.bumsuk.lee1-Sep-16 11:16 
QuestionVery NIce Pin
M,AqibShehzad29-Aug-16 18:44
professionalM,AqibShehzad29-Aug-16 18:44 
AnswerRe: Very NIce Pin
bob.bumsuk.lee29-Aug-16 19:23
memberbob.bumsuk.lee29-Aug-16 19:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web03 | 2.8.180712.1 | Last Updated 24 Sep 2016
Article Copyright 2016 by bob.bumsuk.lee
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid