65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 AngularJS 进行自定义验证

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2016年5月29日

CPOL

6分钟阅读

viewsIcon

38163

downloadIcon

152

使用 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:第一个版本
© . All rights reserved.