import { Component } from '@angular/core';
import { FormGroup, FormControl, FormBuilder } from '@angular/forms';

import { AccountService } from './bank-account.service';

@Component({
  selector: 'my-app',
  templateUrl: 'app/app.component.html'})
export class AppComponent { 
  
  constructor() { }
  
  vmName: string = "Bob Lee";
  vmAccount1: string = '';
  vmAccount2: BankAccount = { acc1: '08', acc2: '6523', acc3: '1954512', acc4: '001' };
  vmAccount3: BankAccount = { acc1: '', acc2: '', acc3: '', acc4: '' };
  
  submit(value: any) {
    console.log('AppComponent.submit()', value);
  }
}

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

import { AppComponent }  from './app.component';
import { AccountService } from './bank-account.service';
import { AccountValidator } from './bank-account-validator.directive';
import { AccountModelDrivenComponent } from './bank-account-model-driven.component';
import { AccountTemplateDrivenComponent } from './bank-account-template-driven.component';

@NgModule({
  imports: [ BrowserModule, FormsModule, ReactiveFormsModule ],
  declarations: [ 
    AppComponent, 
    AccountValidator,
    AccountModelDrivenComponent,
    AccountTemplateDrivenComponent
  ],
  providers: [ AccountService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app.module';

const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);


/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
<!DOCTYPE html>
<html>
  <head>
    <title>Angular 2 QuickStart</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link href="//netdna.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet">

    <!-- 1. Load libraries -->
     <!-- Polyfill(s) for older browsers -->
    <script src="https://unpkg.com/core-js/client/shim.min.js"></script>

    <script src="https://unpkg.com/zone.js@0.6.23?main=browser"></script>
    <script src="https://unpkg.com/reflect-metadata@0.1.3"></script>
    <script src="https://unpkg.com/systemjs@0.19.27/dist/system.src.js"></script>

    <!-- 2. Configure SystemJS -->
    <script src="systemjs.config.js"></script>
    <script>
      System.import('app').catch(function(err){ console.error(err); });
    </script>
  </head>

  <!-- 3. Display the application -->
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>


<!-- 
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
-->
### Angular 2 component for bank account with validation

- custom form component with custom validation
- ignores key press other than numeric (0..9)
- validates bank account number
- assumes the only valid number: 08-6523-1954512-001

/**
 * PLUNKER VERSION
 * (based on systemjs.config.js in angular.io)
 * System configuration for Angular 2 samples
 * Adjust as necessary for your application needs.
 */
(function (global) {
  System.config({
    // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER
    transpiler: 'ts',
    typescriptOptions: {
      tsconfig: true
    },
    meta: {
      'typescript': {
        "exports": "ts"
      }
    },
    paths: {
      // paths serve as alias
      'npm:': 'https://unpkg.com/'
    },
    // map tells the System loader where to look for things
    map: {
      // our app is within the app folder
      app: 'app',

      // angular bundles
      '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
      '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
      '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
      '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
      '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
      '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
      '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
      '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
      '@angular/upgrade': 'npm:@angular/upgrade/bundles/upgrade.umd.js',

      // other libraries
      'rxjs':                       'npm:rxjs',
      'angular2-in-memory-web-api': 'npm:angular2-in-memory-web-api',
      'ts':                         'npm:plugin-typescript@4.0.10/lib/plugin.js',
      'typescript':                 'npm:typescript@2.0.2/lib/typescript.js',

    },
    // packages tells the System loader how to load when no filename and/or no extension
    packages: {
      app: {
        main: './main.ts',
        defaultExtension: 'ts'
      },
      rxjs: {
        defaultExtension: 'js'
      },
      'angular2-in-memory-web-api': {
        main: './index.js',
        defaultExtension: 'js'
      }
    }
  });
})(this);


/*
Copyright 2016 Google Inc. All Rights Reserved.
Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}
<h3>Angular 2 component for bank account with validation</h3>

<ul>
  <li>custom form component with custom validation</li>
  <li>ignores key press other than numeric (0..9)</li>
  <li>validates bank account number</li>
  <li>assumes the only valid number: 08-6523-1954512-001</li>
</ul>

<form #theForm="ngForm" name="theForm" novalidate (ngSubmit)="submit(theForm)">

  <div class="form-group" [ngClass]="{ 'has-error' : name.invalid }">
    <label class="control-label col-md-3">Name</label>
    <div class="col-md-9">
      <input class="form-control text-box single-line" type="text" name="name" #name="ngModel" [(ngModel)]="vmName" required />

      <div class="text-danger" [hidden]="name.valid || !name.errors.required">Name is required</div>
    </div>
  </div>

  <div class="form-group">
    <label class="control-label col-md-3">Account 1</label>
    <div class="col-md-9" [ngClass]="{ 'has-error' : account1.invalid }">
      <input class="form-control text-box single-line" type="text" name="account1" 
        #account1="ngModel" [(ngModel)]="vmAccount1" validateAccount required
        placeholder="bank account number" />

      <div class="text-danger" [hidden]="account1.valid || !account1.errors.required">Bank account is required</div>
      <div class="text-danger" [hidden]="account1.valid || !account1.errors.validateAccount">Bank account number is not valid</div>
    </div>
  </div>

  <bank-account-model-driven name="account2" 
    #account2="ngModel" [myModel]="account2" [myForm]="theForm" [(ngModel)]="vmAccount2"
    [myRequired]="true">
  </bank-account-model-driven>

  <bank-account-template-driven name="account3" 
    #account3="ngModel" [myModel]="account3" [myForm]="theForm" [(ngModel)]="vmAccount3"
    [myRequired]="true">
  </bank-account-template-driven>
  
  <button type="submit" class="btn btn-default btn-primary" >Submit</button>
  
</form>

<!--
-->
<pre>theForm.valid={{theForm.valid}}, account1.valid={{account1.valid}}, account2.valid={{account2.valid}}, account3.valid={{account3.valid}}</pre>
<pre>theForm.value={{theForm.value | json}}</pre>
/*
  Reference document: 
    Non-Resident Withholding Tax And Resident Withholding Tax Specification Document, 
    section 8, BANK ACCOUNT NUMBER VALIDATION,
    Inland Revenue
*/
import { Injectable } from '@angular/core';

export interface BankAccount { acc1: string, acc2: string, acc3: string, acc4: string }
interface range { from: string, to: string }

@Injectable()
export class AccountService {

  private banks: { [id: string]: range[] } = {};
  private types: { [id: string]: range[] } = {};
  private algorithms: { [id: string]: range[] } = {};

  constructor() {
    let ranges01 = [];
    ranges01.push({ from: '0001', to: '0999' });
    ranges01.push({ from: '1100', to: '1199' });
    ranges01.push({ from: '1800', to: '1899' });
    this.banks['01'] = ranges01;
    // ...
  }

  public isValid(bank: string, branch: string, body: string, suffix: string): boolean { // assumes correct length inputs (2 for bank, 4 for branch, 7 for body, 2/3 for suffix)

    if (!bank && !branch && !body && !suffix) return true; // empty is valid

    var valid = bank && bank.length === 2 &&
                branch && (branch.length === 3 || branch.length === 4) &&
                body && body.length === 7 &&
                suffix && (suffix.length === 2 || suffix.length === 3);
        
    if (!valid) return false;
    
    // real implementation is out of interest here, just check against the only valid number '08-6523-1954512-001'
    
    return bank === '08' && branch === '6523' && body === '1954512' && suffix === '001';
  }
}
import { Component, forwardRef, Input } from '@angular/core';
import { 
  FormControl,
  FormGroup,
  FormBuilder,
  ControlValueAccessor, 
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS
} from '@angular/forms';

import { AccountService } from './bank-account.service';
import { 
  ignoreSome, 
  validateAccountGroupFactory 
} from './bank-account-validator.directive';

@Component ({
  selector: 'bank-account-model-driven',
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AccountModelDrivenComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountModelDrivenComponent), multi: true }
  ],
  templateUrl: 'app/bank-account-model-driven.component.html'
})

export class AccountModelDrivenComponent implements ControlValueAccessor, OnInit {
  accountNumber: FormGroup;

  @Input() myForm: FormGroup;
  @Input() myModel: NgModel;
  @Input() myRequired: boolean;
  validator: Function;
  
  constructor(accountService: AccountService, private formBuilder: FormBuilder) { 
    this.validator = validateAccountGroupFactory(accountService);
  }
  
  ngOnInit() {
    this.accountNumber = this.formBuilder.group({
      acc1: '',
      acc2: '',
      acc3: '',
      acc4: ''
    });
  }
  
  onKeypress(event) {
    ignoreSome(event);
  }

  validate(c: FormGroup) {
    return this.validator(c, this.myRequired);
  }

  writeValue(value: BankAccount) {
    if (value) {
      this.accountNumber.setValue(value);
    }
  }
  
  registerOnChange(fn: (value: any) => void) {
    this.accountNumber.valueChanges.subscribe(fn);
  }
  
  registerOnTouched() {}
}

<div class="form-group" [formGroup]="accountNumber">

  <label class="control-label col-md-3">Account 2</label>
  
  <div class="col-md-9" [ngClass]="{ 'has-error' : myModel.invalid }">
    <div class="form-inline">
      <input class="form-control" type="text" formControlName="acc1" (keypress)="onKeypress($event)" [attr.size]="acc1Size" [attr.maxlength]="acc1Size" placeholder="bank" />
      <input class="form-control" type="text" formControlName="acc2" (keypress)="onKeypress($event)" [attr.size]="acc2Size" [attr.maxlength]="acc2Size" placeholder="branch" />
      <input class="form-control" type="text" formControlName="acc3" (keypress)="onKeypress($event)" [attr.size]="acc3Size" [attr.maxlength]="acc3Size" placeholder="account" />
      <input class="form-control" type="text" formControlName="acc4" (keypress)="onKeypress($event)" [attr.size]="acc4Size" [attr.maxlength]="acc4Size" placeholder="suffix" />
    </div>
    
    <div class="text-danger" [hidden]="myModel.valid || !myModel.errors.myRequired">Bank account is required</div>
    <div class="text-danger" [hidden]="myModel.valid || !myModel.errors.validateAccount">Bank account number is not valid</div>
  </div>

</div>
import { Component, forwardRef, Input } from '@angular/core';
import { 
  FormControl,
  FormGroup,
  FormBuilder,
  ControlValueAccessor, 
  NG_VALUE_ACCESSOR,
  NG_VALIDATORS
} from '@angular/forms';

import { AccountService } from './bank-account.service';
import { 
  ignoreSome, 
  validateAccountGroupFactory 
} from './bank-account-validator.directive';

@Component ({
  selector: 'bank-account-template-driven',
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AccountTemplateDrivenComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountTemplateDrivenComponent), multi: true }
  ],
  templateUrl: 'app/bank-account-template-driven.component.html'
})

export class AccountTemplateDrivenComponent implements ControlValueAccessor, OnInit {
  accountNumber = {
    acc1: '',
    acc2: '',
    acc3: '',
    acc4: ''
  }

  @Input() myForm: FormGroup;
  @Input() myModel: NgModel;
  @Input() myRequired: boolean;
  validator: Function;

  constructor(accountService: AccountService) { 
    this.validator = validateAccountGroupFactory(accountService);
  }
  
  ngOnInit() { }
  
  change(prop, value) {
    this.accountNumber[prop] = value; 
    this.propagateChange(this.accountNumber);
  }
  
  onKeypress(event) {
    ignoreSome(event);
  }

  validate(c: FormGroup) {
    return this.validator(c, this.myRequired);
  }

  writeValue(value: BankAccount) {
    if (value) {
      this.accountNumber = value;
    }
  }
  
  propagateChange = (_: any) => {};

  registerOnChange(fn: (value: any) => void) {
    this.propagateChange = fn;
  }
  
  registerOnTouched() {}
}
<form>

<div class="form-group" ngModelGroup="accountNumber">

  <label class="control-label col-md-3">Account 3</label>
  
  <div class="col-md-9" [ngClass]="{ 'has-error' : myModel.invalid }">
    <div class="form-inline">
      <input class="form-control" type="text" name="acc1" [ngModel]="accountNumber.acc1" (ngModelChange)="change('acc1', $event)" (keypress)="onKeypress($event)" [attr.size]="acc1Size" [attr.maxlength]="acc1Size" placeholder="bank" />
      <input class="form-control" type="text" name="acc2" [ngModel]="accountNumber.acc2" (ngModelChange)="change('acc2', $event)" (keypress)="onKeypress($event)" [attr.size]="acc2Size" [attr.maxlength]="acc2Size" placeholder="branch" />
      <input class="form-control" type="text" name="acc3" [ngModel]="accountNumber.acc3" (ngModelChange)="change('acc3', $event)" (keypress)="onKeypress($event)" [attr.size]="acc3Size" [attr.maxlength]="acc3Size" placeholder="account" />
      <input class="form-control" type="text" name="acc4" [ngModel]="accountNumber.acc4" (ngModelChange)="change('acc4', $event)" (keypress)="onKeypress($event)" [attr.size]="acc4Size" [attr.maxlength]="acc4Size" placeholder="suffix" />
    </div>
  
    <div class="text-danger" [hidden]="myModel.valid || !myModel.errors.myRequired">Bank account is required</div>
    <div class="text-danger" [hidden]="myModel.valid || !myModel.errors.validateAccount">Bank account number is not valid</div>
  </div>
</div>

</form>

import { Directive, forwardRef, HostListener } from '@angular/core';
import { NG_VALIDATORS, FormControl } from '@angular/forms';

import { AccountService } from './bank-account.service';

@Directive({
  selector: '[validateAccount][ngModel]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => AccountValidator), multi: true }
  ]
})
export class AccountValidator {
  validator: Function;
  
  constructor(accountService: AccountService) { 
    this.validator = validateAccountFactory(accountService);
  }
  
  @HostListener('keypress', ['$event'])
  onKeypress(event) {
    ignoreSome(event);
  }
  
  validate(c: FormControl) {
    return this.validator(c);
  }
}

export function ignoreSome(event) {
  if (event.key !== undefined) {
    let k = event.key;
    if (k !== '0' && k !== '1' && k !== '2' && k !== '3' && k !== '4' && k !== '5' && k !== '6' && k !== '7' && k !== '8' && k !== '9'
      && k !== 'Backspace' && k !== 'Tab' && k !== 'Delete' && k !== 'ArrowLeft' && k !== 'ArrowRight') { // Firefox
      console.log(`ignoreSome(key ${k})`);
      event.preventDefault(); 
    }
  } else if (event.keyCode !== undefined) {
    let k = event.keyCode;
    if (k === 32 || // space
      k < 48 || k > 57) { // not decimal
      console.log(`ignoreSome(keyCode ${k})`);
      event.preventDefault(); 
    }
  }
}

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;
  };
}

export function validateAccountGroupFactory(accountService: AccountService) {
  return (c: FormGroup, required: boolean) => {
    
    let invalid = { validateAccount: { valid: false } },
      myRequired = { myRequired: { valid: false } };

    if (!c || !c.value || (!c.value.acc1 && !c.value.acc2 && !c.value.acc3 && !c.value.acc4)) { // empty
      if (required) return myRequired;
      else return null;
    }
    
    return accountService.isValid(c.value.acc1, c.value.acc2, c.value.acc3, c.value.acc4) ? null : invalid;
  };
}