Asial Blog

Recruit! Asialで一緒に働きませんか?

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

カテゴリ :
フロントエンド(HTML5)
タグ :
Tech
JavaScript
angularjs
こんにちは中川です。

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

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

■ ngModel.$validators



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

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

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

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

■ ngModel.$asyncValidators



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

  1. ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
  2. var value = modelValue || viewValue;
  3.  
  4. // Lookup user by username
  5. return $http.get('/api/users/' + value).
  6.    then(function resolved() {
  7.      //username exists, this means validation fails
  8.      return $q.reject('exists');
  9.    }, function rejected() {
  10.      //username does not exist, therefore this validation passes
  11.      return true;
  12.    });
  13. };

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

  1. app.directive('appValidators', function () {
  2.     return {
  3.         require: 'ngModel',
  4.         scope: {
  5.             appValidators: '=',
  6.         },
  7.         link: function (scope, elem, attrs, ctrl) {
  8.             var validators = scope.appValidators || {};
  9.             angular.forEach(validators, function (val, key) {
  10.                 ctrl.$validators[key] = val;
  11.             });
  12.         }
  13.     };
  14. });
  15.  
  16. app.directive('appAsyncValidators', function () {
  17.     return {
  18.         require: 'ngModel',
  19.         scope: {
  20.             appAsyncValidators: '='
  21.         },
  22.         link: function (scope, elem, attrs, ctrl) {
  23.             var asyncValidators = scope.appAsyncValidators || {};
  24.             angular.forEach(asyncValidators, function (val, key) {
  25.                 ctrl.$asyncValidators[key] = val;
  26.             });
  27.         }
  28.     };
  29. });

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

  1. app.controller('AppCtrl', function($scope) {
  2.   $scope.user_name_validators = {
  3.     hoge: function (modelValue, viewValue) {
  4.       var val = modelValue || viewValue;
  5.       return val == 'hoge';
  6.     },
  7.     fuga: function (modelValue, viewValue) {
  8.       var val = modelValue || viewValue;
  9.       return val == 'fuga';
  10.     }
  11.   };
  12. });
  1. <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を読み込む必要があります。
  1. <script src="angular.js"></script>
  2. <script src="angular-messages.js"></script>
  3.  
  4. <form name="myForm">
  5. <input type="text" ng-model="field" name="myField" required minlength="5" />
  6. <div ng-messages="myForm.myField.$error">
  7.   <div ng-message="required">You did not enter a field</div>
  8.   <div ng-message="minlength">The value entered is too short</div>
  9. </div>
  10. </form>

■サンプル



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


  1. <!DOCTYPE html>
  2. <html lang="en" ng-app="app">
  3.     <head>
  4.         <meta charset="UTF-8">
  5.         <title></title>
  6.         <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
  7.         <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
  8.         <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-messages.js"></script>
  9.         <script src="app.js"></script>
  10.         <style>
  11.             .container { margin-top: 50px;}
  12.         </style>
  13.     </head>
  14.     <body ng-controller="AppCtrl">
  15.  
  16.         <div class="container">
  17.             <div class="row">
  18.                 <div class="col-xs-6">
  19.                     <form name="userForm" novalidate ng-submit="submit()">
  20.                         <div class="form-group" ng-class="{'has-error': userForm.user_name.$dirty && userForm.user_name.$invalid}">
  21.                             <label class="control-label">ユーザー名</label>
  22.                             <span class="help-inline text-danger" ng-messages="userForm.user_name.$error" ng-if="userForm.user_name.$dirty || userForm.$submitted">
  23.                                 <span ng-message="required">必須です</span>
  24.                                 <span ng-message="pattern">不正な値です</span>
  25.                                 <span ng-message="minlength">4文字以上</span>
  26.                                 <span ng-message="duplicate">既に利用されているユーザー名です</span>
  27.                             </span>
  28.                             <input type="text" class="form-control"
  29.                                    name="user_name" ng-model="user.user_name"
  30.                                    required minlength="4" ng-pattern="/^[a-zA-Z0-9]+$/" app-async-validators="asyncValidators.user_name" />
  31.                             <p class="help-block">※必須、英数字4文字以上</p>
  32.                         </div>
  33.  
  34.                         <div class="form-group" ng-class="{'has-error': userForm.password.$dirty && userForm.password.$invalid}">
  35.                             <label class="control-label">パスワード</label>
  36.                             <span class="help-inline text-danger" ng-messages="userForm.password.$error" ng-if="userForm.password.$dirty || userForm.$submitted">
  37.                                 <span ng-message="required">必須です</span>
  38.                                 <span ng-message="minlength">4文字以上</span>
  39.                                 <span ng-message="joe">ユーザー名と一緒はダメー</span>
  40.                             </span>
  41.                             <input type="password" class="form-control"
  42.                                    name="password" ng-model="user.password"
  43.                                    required minlength="4" app-validators="validators.password" />
  44.                             <p class="help-block">※必須、4文字以上</p>
  45.                         </div>
  46.  
  47.                         <div class="form-group" ng-class="{'has-error': userForm.password_confirm.$dirty && userForm.password_confirm.$invalid}">
  48.                             <label class="control-label">パスワード(確認)</label>
  49.                             <span class="help-inline text-danger" ng-messages="userForm.password_confirm.$error" ng-if="userForm.password_confirm.$dirty || userForm.$submitted">
  50.                                 <span ng-message="required">必須です</span>
  51.                                 <span ng-message="confirm">パスワード確認が一致しません</span>
  52.                             </span>
  53.                             <input type="password" class="form-control"
  54.                                    name="password_confirm" ng-model="user.password_confirm"
  55.                                    required minlength="4" app-validators="validators.password_confirm" />
  56.                         </div>
  57.  
  58.                         <button class="btn btn-primary btn-block" ng-disabled="userForm.$dirty && userForm.$invalid">送信</button>
  59.                     </form>
  60.                 </div>
  61.                 <div class="col-xs-6">
  62.                     <pre>user = {{user|json}}</pre>
  63.                     <pre>userForm.$error = {{userForm.$error|json}}</pre>
  64.                 </div>
  65.             </div>
  66.         </div>
  67.  
  68.     </body>
  69. </html>

  1. (function() {
  2.     'use strict';
  3.  
  4.     var app = angular.module('app', ['ngMessages']);
  5.  
  6.     app.controller('AppCtrl', function ($scope, $q) {
  7.         // モデル
  8.         $scope.user = {};
  9.  
  10.         // バリデータ
  11.         $scope.validators = {
  12.             password: {
  13.                 // ユーザー名とパスワードは一緒はダメ
  14.                 joe: function (modelValue, viewValue) {
  15.                     var val = modelValue || viewValue;
  16.                     var user = $scope.user || {};
  17.  
  18.                     return val != user.user_name;
  19.                 }
  20.             },
  21.             password_confirm: {
  22.                 // パスワード確認
  23.                 confirm: function (modelValue, viewValue) {
  24.                     var user = $scope.user || {};
  25.                     var val = modelValue || viewValue;
  26.  
  27.                     return val == user.password;
  28.                 }
  29.             }
  30.         };
  31.  
  32.         // 非同期バリデータ
  33.         $scope.asyncValidators = {
  34.             user_name: {
  35.                 duplicate: function (modelValue, viewValue) {
  36.                     var users = ['aaaa', 'bbbb', 'cccc'];
  37.                     var val = modelValue || viewValue;
  38.                     return $q(function (resolve, reject) {
  39.                         setTimeout(function () {
  40.                             if (users.indexOf(val) === -1) {
  41.                                 resolve('ok');
  42.                             } else {
  43.                                 reject('ng');
  44.                             }
  45.                         }, 1000);
  46.                     });
  47.                 }
  48.             }
  49.         };
  50.  
  51.         // user_name != password判定のため
  52.         $scope.$watch('user.user_name', function() {
  53.             $scope.userForm.password.$validate();
  54.         });
  55.  
  56.         // password == password_confirm判定のため
  57.         $scope.$watch('user.password', function() {
  58.             $scope.userForm.password_confirm.$validate();
  59.         });
  60.  
  61.         // 送信ボタンイベント
  62.         $scope.submit = function () {
  63.             // 何も変更しないで、送信ボタン時にエラーを表示してあげる
  64.             if ($scope.userForm.$invalid) {
  65.                 $scope.userForm.$setDirty();
  66.                 return;
  67.             }
  68.  
  69.             // 成功!!
  70.             console.log($scope.user);
  71.             alert('成功');
  72.         };
  73.     });
  74.  
  75.     /**
  76.      * validators
  77.      */
  78.     app.directive('appValidators', function () {
  79.         return {
  80.             require: 'ngModel',
  81.             scope: {
  82.                 appValidators: '=',
  83.             },
  84.             link: function (scope, elem, attrs, ctrl) {
  85.                 var validators = scope.appValidators || {};
  86.                 angular.forEach(validators, function (val, key) {
  87.                     ctrl.$validators[key] = val;
  88.                 });
  89.             }
  90.         };
  91.     });
  92.  
  93.     /**
  94.      * asyncValidators
  95.      */
  96.     app.directive('appAsyncValidators', function () {
  97.         return {
  98.             require: 'ngModel',
  99.             scope: {
  100.                 appAsyncValidators: '='
  101.             },
  102.             link: function (scope, elem, attrs, ctrl) {
  103.                 var asyncValidators = scope.appAsyncValidators || {};
  104.                 angular.forEach(asyncValidators, function (val, key) {
  105.                     ctrl.$asyncValidators[key] = val;
  106.                 });
  107.             }
  108.         };
  109.     });
  110.  
  111. })();

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