最近はNexus7を使ってデュアルディスプレイ化している大橋です。
AngularJSでrequired
やng-maxlength
を使った定義済みのForm Validationはよく見るのですが、
そんなものだけじゃはっきり言って足りないと思う今日このごろ。
そこでカスタムバリデーションを組み始めたのですが
普通にやると色々面倒だったり、とりあえずui-validate使ってればいいんじゃないという感じがしたので
まとめたいと思います。
結論的な
色んな所で使うカスタムバリデーションならDirectiveを作る
一箇所でしか使わないならui-validate
を使うって感じがよいかなーと
あと細かくイベント制御できるのはDirectiveの方です。
値が変わるごと=フォームに入力があるごとにバリデーションが動いていいならui-validate
という感じかと。
今回のお題
よくあるパスワード確認フォームとか、メールアドレス確認フォームとかの、
2つのテキストボックスで同じ値が入っているか確認するやーつを実装したいと思います。
見た目は以下です。
なお今回作ったものはPlunkerにおいてあります。
http://plnkr.co/edit/eIhqY4
やり方
やり方1 Directive作る(公式に載ってるやり方)
公式に載ってるカスタムバリデーションでの実装方法です。
やり方としては
- ngModelをrequiredした独自Directive作る
- linkプロセスでngModelControllerの$parsersに独自バリデーション処理を追加
- ngModelController.$setValidityで結果を設定する
という感じ
作るもの
same
という名のdirective
を作り属性値として、参照先のスコープのmodel名を渡します。
その中でカスタムバリデーションを行います。
コード
html
input01
,input02
というテキストフィードを作って、input02
の方に作成するsame
directiveを設定しています。
<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
はそのためのもの?)
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に含まれているカスタムバリデーションを定義するためのdirectiveです。
なおui-utils全部入れなくても単体でも使えます。
先ほど上げた、自前でdirectiveを作るのに比べて
- わざわざディレクティブ作らなくて良い
- Controllerのデータが扱いやすい
などの利点があります。
作るもの
特に無いです。
ui-validateがどうにかしてくれます。
場合によってcontrollerにメソッドを追加します。
コード
html
ui-validate
を利用してsame
というエラー名で$value
(ここではinput02の値)とctrl.val.input03
の値を比べています。
これぐらいの比較であればControllerにメソッドを設定する必要もありません。
細かい制御が必要な場合はControllerにメソッドを記載し、
それを呼び出すだけでよいです。
<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)'}"
に変更するだけです。
angular.module "app", ['ui.utils']
.controller 'AppCtrl' ,
[
'$scope',
class AppCtrl
constructor : (@$scope)->
isSame : ($value, value2)->
return $value == value2
]
こちらのほうが個人的にはわかりやすいのとController側のデータが扱いやすいように思います。
ただElementに深く関わるようなことをしたり、
イベントに紐付かせてやる場合はDirectiveのほうが向いているのかなーと思います。
まとめ
とりあえずAngularJSのカスタムバリデーションは結構素敵です。
処理とUIを分離できるのはコード的には書きやすいです。