こんにちは中川です。
先日、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;
});
}
};
});
})();
今回のバージョンアップで、ますます使いやすくなったと思いますので、
ぜひみなさん試してみてはいかがでしょうか。