AngularJSのForm Validationでカスタムバリデーションのやり方あれこれ(個人的にはui-validateが好き)


最近はNexus7を使ってデュアルディスプレイ化している大橋です。

AngularJSで`required`や`ng-maxlength`を使った定義済みのForm Validationはよく見るのですが、
そんなものだけじゃはっきり言って足りないと思う今日このごろ。

そこでカスタムバリデーションを組み始めたのですが
普通にやると色々面倒だったり、とりあえずui-validate使ってればいいんじゃないという感じがしたので
まとめたいと思います。

# 結論的な

色んな所で使うカスタムバリデーションならDirectiveを作る
一箇所でしか使わないなら`ui-validate`を使うって感じがよいかなーと
あと細かくイベント制御できるのはDirectiveの方です。
値が変わるごと=フォームに入力があるごとにバリデーションが動いていいなら`ui-validate`という感じかと。

# 今回のお題

よくあるパスワード確認フォームとか、メールアドレス確認フォームとかの、
2つのテキストボックスで同じ値が入っているか確認するやーつを実装したいと思います。

見た目は以下です。


なお今回作ったものはPlunkerにおいてあります。
http://plnkr.co/edit/eIhqY4




# やり方
## やり方1 Directive作る(公式に載ってるやり方)

[公式](http://docs.angularjs.org/guide/forms)に載ってるカスタムバリデーションでの実装方法です。

やり方としては

1. ngModelをrequiredした独自Directive作る
2. linkプロセスでngModelControllerの$parsersに独自バリデーション処理を追加
3. ngModelController.$setValidityで結果を設定する

という感じ

### 作るもの

`same`という名の`directive`を作り属性値として、参照先のスコープのmodel名を渡します。
その中でカスタムバリデーションを行います。

### コード

#### html

`input01`,`input02`というテキストフィードを作って、`input02`の方に作成する`same` directiveを設定しています。


```html:index.html
    <div ng-controller="AppCtrl as ctrl" class="container-fluid">
      <div class="row-fluid">
        <div class="span12">
          <form name="hogeForm" class="form-horizontal">
            <fieldset>
              <legend>Controls Bootstrap supports</legend>
              <div class="control-group">
                <label for="input01" class="control-label">Email1</label>
                <div class="controls">
                  <input id="input01" type="text" name="input01" ng-model="ctrl.val.input01" class="input-xlarge">
                </div>
              </div>
              <div class="control-group">
                <label for="input02" class="control-label">Email2</label>
                <div class="controls">
                  <input id="input02" type="text" name="input02" ng-model="ctrl.val.input02" same="ctrl.val.input01" class="input-xlarge">
                </div>
              </div>{{ctrl.val}} {{hogeForm.input02.$error | json}}
            </fieldset>
          </form>
        </div>
      </div>
    </div>
    <script type="text/coffeescript" src="app.coffee"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.3/angular.min.js"></script>
    <script src="//angular-ui.github.io/ui-utils/dist/ui-utils.js"></script>

```

#### js (directive)

directiveでは`ngModel`を必須(#1)として、
`link`にて`NgModelController`(#2)を受け取るようにし、
`NgModelController`の`$parsers`に処理を追加、引数は現在入力されている値になります。(#3)
属性値で受け取った比べる対象のmodel値を取得(#4)し、
`NgModelController`の`$setValidity`にてエラーの設定を行います。
`$setValidity`の引数は第一引数がエラーの名称で、第二引数が結果(正常か否か)です。

また戻り値(#6)がngModelとしての値になります。
エラーの場合`undefined`で返却したり、
このタイミングで値の変換を行います。（多分もともと`$parsers`はそのためのもの？）


```coffeescript:app.coffee

angular.module "app", []
  .controller 'AppCtrl' , 
    [
      '$scope',
      class AppCtrl
        constructor : (@$scope)->
    ]
  .directive 'same', ()->
    require : 'ngModel' #1
    link : (scope, elem, attrs, modelCtrl)-> #2

      modelCtrl.$parsers.push (viewValue)-> #3
        
        return viewValue if !attrs.same?
        val = scope.$eval(attrs.same) #4
        return viewValue if !val?


        if val != viewValue 
          modelCtrl.$setValidity "same" , false #5
          return undefined #6
        else
          modelCtrl.$setValidity "same" , true #5
          return viewValue #6

```


このやり方の場合、今回のようなどこでも使いそうなdirectiveの場合は特に問題ないのですが、
Controllerの状態に強く紐づくものや
一箇所でしか使わないバリデーションの場合はちょっと面倒です。


## やり方2 ui-validate使う

ui-validateはui-bootstrapなどを作ってるAngularUIの中の一つ[ui-utils](http://angular-ui.github.io/ui-utils/index.tmp.html)に含まれているカスタムバリデーションを定義するためのdirectiveです。
なおui-utils全部入れなくても単体でも使えます。

先ほど上げた、自前でdirectiveを作るのに比べて

* わざわざディレクティブ作らなくて良い
* Controllerのデータが扱いやすい

などの利点があります。

### 作るもの

特に無いです。
ui-validateがどうにかしてくれます。
場合によってcontrollerにメソッドを追加します。

### コード

#### html

`ui-validate`を利用して`same`というエラー名で`$value`(ここではinput02の値)と`ctrl.val.input03`の値を比べています。
これぐらいの比較であればControllerにメソッドを設定する必要もありません。
細かい制御が必要な場合はControllerにメソッドを記載し、
それを呼び出すだけでよいです。

```html:index.html
<div ng-controller="AppCtrl as ctrl" class="container-fluid">
      <div class="row-fluid">
        <div class="span12">
          <form name="hogeForm" class="form-horizontal">
            <fieldset>
              <legend>Controls Bootstrap supports</legend>
              <div class="control-group">
                <label for="input01" class="control-label">Email1</label>
                <div class="controls">
                  <input id="input01" type="text" name="input01" ng-model="ctrl.val.input01" class="input-xlarge">
                </div>
              </div>
              <div class="control-group">
                <label for="input02" class="control-label">Email2</label>
                <div class="controls">
                  <input id="input02" type="text" name="input02" ng-model="ctrl.val.input02" ui-validate="{same : '$value==ctrl.val.input01'}" class="input-xlarge">
                </div>
              </div>{{ctrl.val}} {{hogeForm.input02.$error | json}}
            </fieldset>
          </form>
        </div>
      </div>
    </div>
    <script type="text/coffeescript" src="app.coffee"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.3/angular.min.js"></script>
    <script src="//angular-ui.github.io/ui-utils/dist/ui-utils.js"></script>
```

#### js(controller)

Controllerでは一応isSameというメソッドを用意しています。
これを呼び出す場合は上記の
`ui-validate="{same : '$value==ctrl.val.input01'}"`を
`ui-validate="{same : 'isSame($value,ctrl.val.input01)'}"`に変更するだけです。


```coffeescript:app.coffee
angular.module "app", ['ui.utils']
  .controller 'AppCtrl' , 
    [
      '$scope',
      class AppCtrl
        constructor : (@$scope)->
        isSame : ($value, value2)-> 
          return $value == value2
    ]
```

こちらのほうが個人的にはわかりやすいのとController側のデータが扱いやすいように思います。

ただElementに深く関わるようなことをしたり、
イベントに紐付かせてやる場合はDirectiveのほうが向いているのかなーと思います。

# まとめ

とりあえずAngularJSのカスタムバリデーションは結構素敵です。
処理とUIを分離できるのはコード的には書きやすいです。