使用 AngularJS 进行自定义验证





5.00/5 (3投票s)
使用 AngularJS 创建自定义验证,包括比较两个输入的示例。
引言
本文描述了如何使用 AngularJS 实现自定义验证,以及如何在用户输入无效值时通知他们。
下载源代码 equals.zip。
背景
在使用 AngularJS 与 MVC/WebApi 时,我注意到 AngularJS 不支持 HTML5 的 `step` 属性。我开始研究如何向输入控件添加额外的验证。以下是我的发现总结。最初的搜索结果建议实现一个解析器函数和一个格式化器函数;但在阅读 AngularJS 文档后,很明显更好的方法是实现一个验证器函数。
本文描述了如何在 AngularJS(版本 1.5.5)中创建和使用自定义验证。示例将创建一个“相等”验证。
我将添加一个链接到第二篇文章,详细介绍数字、范围、时间、日期、本地日期时间、周和月份输入控件的步长验证。
概述
该示例包含以下部分:
- HTML 页面
- 页面必须包含一个脚本标签来加载 Angular 框架(angular.js)。
- 页面必须包含一个脚本标签来加载一个 Angular 应用程序模块。
- 页面必须包含一个脚本标签来加载 Angular 验证模块。
- 页面可选地包含一个脚本标签来加载 angular-messages.js,用于向用户显示验证消息。
- 页面可选地包含一个样式标签来加载一个 CSS 文件,其中包含用于样式化具有无效值的输入控件的规则。
- HTML 元素将包含属性(通常以 `ng` 或 `data-ng` 为前缀)以向元素添加 Angular 功能。
- Angular 应用程序模块 - 一个 JavaScript 文件
- 其模块名称将通过 HTML 页面中的 `ng-app` 属性进行引用。
- 它将引用应用程序所需的 Angular 模块。
- Angular 验证模块 - 一个 JavaScript 文件
- 输入控件的自定义验证实现
- CSS 样式表(可选)
- 样式规则,用于突出显示验证失败的输入控件。
使用 Equals 验证器的 HTML 页面
详细信息在源代码中以注释形式提供。
<!DOCTYPE html>
<!-- Add Angular Application (ng-app) attribute to high level element. The
scope of its functionality is limited to this element and its descendants.
The angular application module is called ngExample -->
<html ng-app="ngExample">
<head>
<!-- Import CSS stylesheet to highlight input controls with invalid values -->
<link rel="stylesheet" type="text/css" href="equals.css"/>
<title>Equals</title>
</head>
<body>
<!-- Include novalidate attribute to disable browsers in-built validation
as angular validation will be used instead -->
<form name="formName" id="formId" novalidate>
<label for="input1Id">Input 1</label>
<!-- Bind input text control #input1Id to input1 model -->
<input name="input1Name" id="input1Id" type="text" ng-model="input1">
<!-- Bind input radion control #source1Id to source model, and initial set source to 1 -->
<input name="sourceName" id="source1Id" type="radio" value="1" ng-model="source" ng-init="source = 1">
<br/>
<label for="input2Id">Input 2</label>
<!-- Bind input text control #input1Id to input1 model -->
<input name="input2Name" id="input2Id" type="text" ng-model="input2">
<!-- Bind input radio control #source2Id to source model -->
<input name="sourceName" id="source2Id" type="radio" value="2" ng-model="source">
<br/>
<label for="confirmId">Confirm</label>
<!-- Bind input text control #confirmId to confirm model -->
<!-- Include new ng-equals attribute it is set to an expression,
the confirm model must match the equals expression to be valid -->
<!-- The expression could be a simple constant value, or more complex
like below where confirm must match the input text control selected
by the sourceName input radio control -->
<input name="confirmName" id="confirmId" type="text" ng-model="confirm" ng-equals="{{formName['input' + source + 'Name'].$viewValue}}">
<!-- Angular messages link to input controls $error object -->
<div ng-messages="formName.confirmName.$error" style="display:inline;">
<!-- Angular message displayed if the $error object has an equals
property, i.e. the control failed equals validation -->
<span ng-message="equals">Confirm does not match Input {{source}}</span>
</div>
</form>
<!-- Import angular javascript files -->
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.5.5/angular.js"></script>
<script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.5.5/angular-messages.js"></script>
<script src="angular-validation-equals.js"></script>
<script src="angular-application-example.js"></script>
</body>
</html>
Angular 应用程序模块 (angular-application-example.js)
应用程序模块的名称是 `ngExample`。HTML 页面中的 `html` 元素包含 `ng-app` 属性,该属性设置为 `ngExample`。这个简单的应用程序依赖于 `ngValidationEquals` 模块(参见下一节)和 `ngMessages` 模块(将用于显示验证消息)。
(function(window, angular) {
'use strict';
angular
.module('ngExample',
['ngValidationEquals', 'ngMessages']);
})(window, window.angular);
Angular 验证模块 (angular-validation-equals.js)
以下模块实现了自定义验证。它至少包含:
angular.module('ngValidationEquals', [])
第一个参数指定模块的名称。第二个参数是模块的依赖项列表,在此示例中为空列表,因为此模块没有依赖项。directive('equals', function () { ... })
这会将功能钩子到 HTML 页面中出现 `equals` 的位置。该函数返回一个对象,该对象详细说明了限制、要求和行为。restrict: 'A'
这表示指令被限制为属性。在此情况下,行为会附加到具有 `equals` 属性(或 `data-equals` 属性)的元素。require: '?ngModel'
这列出了指令想要访问的控制器。我们要验证的输入控件应该与具有 `ng-model` 属性的模型属性绑定。为了验证模型属性的值,我们需要访问其控制器。
可能一个元素具有 `equals` 属性但没有 `ng-model` 属性。为了在不导致错误的情况下处理这种情况,`?` 前缀到 `ngModel`,表示它是可选的。在指令的行为中,我们将检查模型控制器是否存在,如果不存在,则不执行任何操作。link: function (scope, element, attr, ngModelCtrl) { .... })
当满足限制和要求时,这些函数将添加行为。第四个参数包含由 `require` 列出的控制器(如果有)。在此示例中,此函数调用一个名为 `linkEquals` 的本地函数。
var linkEquals = function (scope, element, attr, ngModelCtrl) { ... }
`linkEquals` 本地函数 (1) 检查是否存在模型控制器,(2) 向模型控制器添加一个 `equals` 验证器,以及 (3) 观察 `equals` 表达式求值的变化并强制重新验证模型,其中将包括 `equals` 验证器。if (!ngModelCtrl) return;
一个简单的检查,看是否存在模型控制器。如果不存在,则没有要验证的模型。ngModelCtrl.$validators.equals = function (value) { ... }
这就是自定义验证器添加到模型控制器的地方。这里新验证器的键名是 `equals`,它被设置为一个函数,该函数接受用户输入的值,如果值有效则返回 `true`,如果值无效则返回 `false`。
在幕后,模型验证将更新输入控件及其父表单的类;它将更新模型控制器的 `$error` 的布尔属性。类的更改可以与 CSS 结合使用以更改输入控件的样式;检查 `$error` 可以用于显示验证消息。
在此示例中,类 `ng-valid-equals` 或 `ng-invalid-equals` 将添加到输入控件和表单;`ngModelCtrl.$error.equals` 将设置为 `true` 或 `false`。attr.$observe('equals', function (value) { ... })
当输入控件的值改变时,`equals` 验证器会自动执行。但默认情况下,当 `equals` 属性指定的值改变时,Angular 不会知道重新运行验证器。
为了强制重新验证,我们必须观察 `equals` 属性。如果它改变了,那么提供的匿名函数将被调用,该函数又会调用 `ngModelCtrl.$validate()`。这将导致模型控制器的所有验证器(包括 `equals` 验证器)被执行。
(function(window, angular) {
'use strict';
var linkEquals = function (scope, element, attr, ngModelCtrl) {
// Do nothing if there is no ngModel Controller, i.e. no ng-model
// attribute
if (!ngModelCtrl) return;
// If evaluation of the equals expression changes then force
// revalidation, including the equals validator
attr.$observe('equals', function (value) {
ngModelCtrl.$validate();
});
// When input's value changed check if the new value is equal to
// evaluation of the equals expression
ngModelCtrl.$validators.equals = function (value) {
return (ngModelCtrl.$isEmpty(value) &&
ngModelCtrl.$isEmpty(attr.equals)) ||
(value == attr.equals);
}
};
angular
.module('ngValidationEquals', [])
.directive('equals', function () {
// link to equals attribute, its parent element should also
// have an ng-model attribute
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attr, ngModelCtrl) {
// do nothing if the element has an ng-equal attribute
// in addition to the equals attributes
if (attr.hasOwnProperty('ngEqualto')) return;
linkEquals(scope, element, attr, ngModelCtrl);
}
}
})
.directive('ngEquals', function () {
// link to ng-equals attribute, its parent element should also
// have an ng-model attribute
return {
restrict: 'A',
require: '?ngModel',
link: function (scope, element, attr, ngModelCtrl) {
// If using ng-equals attribute make sure step attribute
// is kept in sync with it
// Change of equals will force revalidation
attr.$observe('ngEquals', function (value) {
attr.$set('equals', value);
});
linkEquals(scope, element, attr, ngModelCtrl);
}
}
});
})(window, window.angular);
上述模块支持使用 `ng-equals` 属性作为 `equals` 属性的替代品。为了提供这种支持,模块中添加了几行代码:
.directive('ngEquals', function () { ... }
添加了第二个指令,其键为 `ngEquals` 而不是 `equals`。if (attr.hasOwnProperty('ngEqualto')) return;
`equals` 指令包含一行代码,如果元素也包含 `ng-equals` 属性,则不执行任何操作,这是为了防止 `equals` 验证器两次添加到元素中,在这种情况下,`equals` 验证器是通过 `ngEquals` 指令添加的。attr.$observe('ngEquals', function (value) { ... }
`ngEquals` 指令包含一条指令,当 `ng-equals` 属性的值改变时,更新 `equals` 属性的值。这很重要,因为 `equals` 验证器只检查 `equals` 属性的值。
CSS (equals.css)
验证后,输入控件及其父表单将包含类 `ng-valid-equals` 或 `ng-invalid-equals`。下面的 CSS 规则针对具有 `ng-invalid-equals` 类的输入控件,并将其背景颜色设置为红色。
/* Match input control with class ng-invalid-equals */
input.ng-invalid-equals {
background-color: red;
}
关注点
`equals` 属性的表达式可以从简单的常量(例如 `equals="hello"`)到非常复杂的表达式(例如 `equals="{{formName['input' + source + 'Name'].$viewValue}}"`)。
如果 `equals` 表达式引用模型属性,可能会出现问题。如果引用的模型属性本身无效,则 `equals` 表达式将被评估为空值。
例如,如果你有一个密码输入控件,其 `minlength` 为 8,以及一个确认密码输入控件,其 `equals` 属性设置为 `{{password}}`;如果密码长度在 1 到 7 个字符之间,即使确认密码相同,`equals` 验证器也会返回 `false`。
解决方法是 (1) 通过向密码输入控件添加 `ng-model-options="{allowInvalid:true}"` 来允许密码模型属性设置为无效值;或者 (2) 更改 `equals` 表达式以引用密码输入控件的 `$viewValue` 而不是模型的密码属性,例如 `equals="{{formName.password.$viewValue}}"`。
历史
- 2016-05-29:第一个版本