Carpe Diem

備忘録

AngularでFormのCustom Validation

概要

AngularでのFormのカスタムバリデーションには主に以下の方法があります。

  • Directiveで用意する
  • ビルトインのValidatorsのような関数を用意する

今回は後者の実装例を紹介します。

validationロジック

今回はクレジットカードの簡易チェックをするvalidatorを導入します。
クレジットカードの番号はLuhnアルゴリズムに基づいています。
これによってわざわざサーバ通信を挟む前に、入力ミスなどによる不正な番号を弾くことができます。

環境

  • Angular 2.4.5
  • angular-cli 1.0.0-beta.28.3

成果物

今回の完成形は以下

github.com

実装

import { AbstractControl } from '@angular/forms';

export class CreditCardValidator {

  static luhn(c: AbstractControl) {
    const num = c.value;
    if (num.length < 13) {
      return {luhn: true};
    }

    const digits = num.split('').reverse();
    let digit;
    let sum = 0;

    for (let i = 0; i < digits.length; i++) {
      digit = digits[i];
      digit = parseInt(digit, 10);
      // if odd, multiplied by 2.
      if ((i + 1) % 2 === 0) {
        digit *= 2;
      }
      // if more than 10, subtract 9.
      if (digit > 9) {
        digit -= 9;
      }
      sum += digit;
    }

    if (sum % 10 !== 0) {
      return {luhn: true};
    }

    return null;
  }
}

ロジックはLuhnアルゴリズムの通りなので、それ以外の部分についてのポイントを指摘します。

  • AbstractControlを引数にとる
  • 成功時はnullを返す
  • 失敗時は{エラー名: true}の形で返す

3つ目ですが、これはAbstractControlhasError('エラー名')というメソッドを持つので、それを使ってバリデーションのエラーを細かく判別しやすくなるためです。

テストコード

次にテストコードの書き方を紹介します。

このCreditCardValidatorをnewしてテストするよりも、テスト用のComponentを用意してそこで実際にValidatorとして扱った方がテストしやすいです。

TestComponent

@Component({
  template: `
<form [formGroup]="fg">
  <input type="text" formControlName="card">
</form>
`
})
class TestComponent implements OnInit {
  public fg: FormGroup;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.fg = this.fb.group({
      'card': ['', CreditCardValidator.luhn],
    });
  }
}

こんな感じのテスト用のComponentを用意します。以前紹介したModel Drivenのフォームですね。
これにビルトインのValidatorsのようにCreditCardValidator.luhnをセットします。

テストスイート(describe)

次にテストの前準備の部分を書きます。

describe('Credit card validators', () => {
  let component: TestComponent;
  let fixture: ComponentFixture<TestComponent>;
  let card: AbstractControl;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        TestComponent
      ],
      imports: [
        ReactiveFormsModule
      ],
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
    card = component.fg.controls['card'];
  });

});

ポイントは以下です。

  • 先程のTestComponentをdeclarationsに入れる
  • ReactiveFormsModuleをimportしている
  • component.fg.controls['card']で要素を取得

テストケース

これを元に以下のようにテストケースを書いていきます。

  it('should be form invalid when empty', () => {
    expect(component.fg.valid).toBeFalsy();
  });

  it('should be card field invalid when empty', () => {
    expect(card.valid).toBeFalsy();
  });

  it('should be errors field invalid when empty', () => {
    expect(card.hasError('luhn')).toBeTruthy();
  });

  it('should be errors field invalid when number is short', () => {
    card.setValue('42424242');

    expect(card.hasError('luhn')).toBeTruthy();
  });

  it('should be errors field invalid when number is invalid', () => {
    card.setValue('4242424242424241');

    expect(card.hasError('luhn')).toBeTruthy();
  });

  it('should be valid', () => {
    card.setValue('4242424242424242');

    expect(card.valid).toBeTruthy();
  });

ポイントは以下

  • FormGroup(fg)のvalid、要素(card)のvalidでバリデーションチェック
  • hasError()で詳細なエラーを判別
  • setValue()で検証する値をセット

まとめ

カスタムバリデーションの実装例を紹介しました。
1つ作り方を覚えればメールのフォーマットチェックなど、色んなチェックをすることができるようになります。 またViewと違い簡単にテストも書けるので、こういったロジックメインの部分はなるべくテストを書いてバグを減らしていきたいですね。

ソース