アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

AngularJS 1.3のフォームまわりの機能強化

こんにちは中川です。

先日、AngularJS 1.3 がリリースされましたね。

動作速度の改善や、メモリ消費量の削減などパフォーマンス面での改善もうれしいところですが、
機能的にはフォーム関連の機能強化が特にうれしく感じましたので、紹介したいと思います。

■ ngModel.$validators



https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

ngModel.$validators を使うと、独自のバリデーション関数を簡単に定義することができるようになりました。

以下の例のように、入力値を引数で受け取り、返り値で真偽値を返す関数を$validatorsオブジェクトに定義します。
$validatorsのキー(ここではvalidCharacters)が、エラーメッセージ表示時などの参照用に利用できます。



ngModel.$validators.validCharacters = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value)  & &
       /[a-z]+/.test(value)  & &
       /[A-Z]+/.test(value)  & &
       /\W+/.test(value);
};


■ ngModel.$asyncValidators



サーバへの問い合わせが必要な場合など、非同期の処理がある場合のバリデーションにも対応しています。
以下の例のように、ngModel.$asyncValidators にpromiseを返す関数を定義します。



ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;

// Lookup user by username
return $http.get('/api/users/' + value).
   then(function resolved() {
     //username exists, this means validation fails
     return $q.reject('exists');
   }, function rejected() {
     //username does not exist, therefore this validation passes
     return true;
   });
};


今まで、パスワード確認入力やユーザー名重複のサーバ問い合わせなど、
controller(service)の値を組み合わせたバリデーションをdirectiveで
指定するのが面倒で、ui-validateというcontrollerの関数定義を指定できる外部モジュールを利用していました。
ui-vaidateでも今まで対応できていたので、別にそれでもいいといえばいいのですが、
しかし、1.3からは以下のような汎用的なdirectiveを定義すれば、とても素直に指定することができます。



app.directive('appValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appValidators: '=',
        },
        link: function (scope, elem, attrs, ctrl) {
            var validators = scope.appValidators || {};
            angular.forEach(validators, function (val, key) {
                ctrl.$validators[key] = val;
            });
        }
    };
});

app.directive('appAsyncValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appAsyncValidators: '='
        },
        link: function (scope, elem, attrs, ctrl) {
            var asyncValidators = scope.appAsyncValidators || {};
            angular.forEach(asyncValidators, function (val, key) {
                ctrl.$asyncValidators[key] = val;
            });
        }
    };
});


利用時は以下のように、controllerでオブジェクトをテンプレート側のapp-validators属性で渡せます。



app.controller('AppCtrl', function($scope) {
	$scope.user_name_validators = {
		hoge: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'hoge';
		},
		fuga: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'fuga';
		}
	};
});



<input type="text" ng-model="user_name" app-validators="user_name_validators">


■ ngMessages



https://docs.angularjs.org/api/ngMessages

フォームのエラーメッセージの表示対応について、
従来のng-showやng-ifで行う方法では、同時に複数エラーが出た場合にもひとつだけ表示するような制御が面倒でしたが、
ngMessagesを利用すると、ずいぶん簡単に記述できるようになりました。

ngMessagesを利用するには別途angular-messages.jsを読み込む必要があります。


<script src="angular.js"></script>
<script src="angular-messages.js"></script>

<form name="myForm">
<input type="text" ng-model="field" name="myField" required minlength="5" />
<div ng-messages="myForm.myField.$error">
  <div ng-message="required">You did not enter a field</div>
  <div ng-message="minlength">The value entered is too short</div>
</div>
</form>


■サンプル



これらの機能を利用したフォームのサンプルを作ってみました。
※ユーザー名:「aaaa」「bbbb」「cccc」を重複エラーとしています。




<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-messages.js"></script>
        <script src="app.js"></script>
        <style>
            .container { margin-top: 50px;}
        </style>
    </head>
    <body ng-controller="AppCtrl">

        <div class="container">
            <div class="row">
                <div class="col-xs-6">
                    <form name="userForm" novalidate ng-submit="submit()">
                        <div class="form-group" ng-class="{'has-error': userForm.user_name.$dirty  & & userForm.user_name.$invalid}">
                            <label class="control-label">ユーザー名</label>
                            <span class="help-inline text-danger" ng-messages="userForm.user_name.$error" ng-if="userForm.user_name.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="pattern">不正な値です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="duplicate">既に利用されているユーザー名です</span>
                            </span>
                            <input type="text" class="form-control"
                                   name="user_name" ng-model="user.user_name"
                                   required minlength="4" ng-pattern="/^[a-zA-Z0-9]+$/" app-async-validators="asyncValidators.user_name" />
                            <p class="help-block">※必須、英数字4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password.$dirty  & & userForm.password.$invalid}">
                            <label class="control-label">パスワード</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password.$error" ng-if="userForm.password.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="joe">ユーザー名と一緒はダメー</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password" ng-model="user.password"
                                   required minlength="4" app-validators="validators.password" />
                            <p class="help-block">※必須、4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password_confirm.$dirty  & & userForm.password_confirm.$invalid}">
                            <label class="control-label">パスワード(確認)</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password_confirm.$error" ng-if="userForm.password_confirm.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="confirm">パスワード確認が一致しません</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password_confirm" ng-model="user.password_confirm"
                                   required minlength="4" app-validators="validators.password_confirm" />
                        </div>

                        <button class="btn btn-primary btn-block" ng-disabled="userForm.$dirty  & & userForm.$invalid">送信</button>
                    </form>
                </div>
                <div class="col-xs-6">
                    <pre>user = {{user|json}}</pre>
                    <pre>userForm.$error = {{userForm.$error|json}}</pre>
                </div>
            </div>
        </div>

    </body>
</html>




(function() {
    'use strict';

    var app = angular.module('app', ['ngMessages']);

    app.controller('AppCtrl', function ($scope, $q) {
        // モデル
        $scope.user = {};

        // バリデータ
        $scope.validators = {
            password: {
                // ユーザー名とパスワードは一緒はダメ
                joe: function (modelValue, viewValue) {
                    var val = modelValue || viewValue;
                    var user = $scope.user || {};

                    return val != user.user_name;
                }
            },
            password_confirm: {
                // パスワード確認
                confirm: function (modelValue, viewValue) {
                    var user = $scope.user || {};
                    var val = modelValue || viewValue;

                    return val == user.password;
                }
            }
        };

        // 非同期バリデータ
        $scope.asyncValidators = {
            user_name: {
                duplicate: function (modelValue, viewValue) {
                    var users = ['aaaa', 'bbbb', 'cccc'];
                    var val = modelValue || viewValue;
                    return $q(function (resolve, reject) {
                        setTimeout(function () {
                            if (users.indexOf(val) === -1) {
                                resolve('ok');
                            } else {
                                reject('ng');
                            }
                        }, 1000);
                    });
                }
            }
        };

        // user_name != password判定のため
        $scope.$watch('user.user_name', function() {
            $scope.userForm.password.$validate();
        });

        // password == password_confirm判定のため
        $scope.$watch('user.password', function() {
            $scope.userForm.password_confirm.$validate();
        });

        // 送信ボタンイベント
        $scope.submit = function () {
            // 何も変更しないで、送信ボタン時にエラーを表示してあげる
            if ($scope.userForm.$invalid) {
                $scope.userForm.$setDirty();
                return;
            }

            // 成功!!
            console.log($scope.user);
            alert('成功');
        };
    });

    /**
     * validators
     */
    app.directive('appValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appValidators: '=',
            },
            link: function (scope, elem, attrs, ctrl) {
                var validators = scope.appValidators || {};
                angular.forEach(validators, function (val, key) {
                    ctrl.$validators[key] = val;
                });
            }
        };
    });

    /**
     * asyncValidators
     */
    app.directive('appAsyncValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appAsyncValidators: '='
            },
            link: function (scope, elem, attrs, ctrl) {
                var asyncValidators = scope.appAsyncValidators || {};
                angular.forEach(asyncValidators, function (val, key) {
                    ctrl.$asyncValidators[key] = val;
                });
            }
        };
    });

})();


今回のバージョンアップで、ますます使いやすくなったと思いますので、
ぜひみなさん試してみてはいかがでしょうか。