AngularJS 输入验证

AI4天前发布 beixibaobao
4 0 0

AngularJS 输入验证学习笔记(详细版)


一、输入验证概述

AngularJS 提供了一套声明式的客户端输入验证机制,通过在 HTML 元素上添加验证属性,结合 ng-model 自动完成验证逻辑,无需手动编写大量验证代码。

1.1 验证工作原理

用户输入
  │
  ▼
ng-model 捕获输入值
  │
  ▼
$validators / $asyncValidators 执行验证
  │
  ▼
更新 $error 对象 + 自动添加 CSS 类
  │
  ▼
视图根据状态显示提示信息

1.2 前提条件

<!-- 1. 必须有 ng-model —— 没有它验证不会生效 -->
<input ng-model="user.name" required>
<!-- 2. 必须有 name 属性 —— 否则无法通过表单访问控件状态 -->
<form name="myForm">
    <input name="username" ng-model="user.name" required>
    <!-- 可通过 myForm.username 访问验证状态 -->
</form>
<!-- 3. 建议添加 novalidate —— 禁用浏览器原生验证 -->
<form name="myForm" novalidate>

二、内置验证属性详解

2.1 required / ng-required

必填验证,字段不能为空。

<!-- 静态必填 -->
<input type="text" name="username" ng-model="user.username" required>
<!-- 动态必填:根据条件决定是否必填 -->
<input type="text"
       name="companyName"
       ng-model="user.companyName"
       ng-required="user.type === 'enterprise'">
<!-- 当用户类型为企业时,公司名称才必填 -->

$error 键名: required

<span ng-show="myForm.username.$error.required">此字段必填</span>

验证规则:

  • undefinednull''(空字符串)→ 验证失败
  • 0false → 验证通过(有值)
  • 空格字符串 ' ' → 验证通过(如需去除空格,配合 ng-trim
<!-- ng-trim 自动去除首尾空格(默认 true) -->
<input type="text" ng-model="user.name" ng-trim="true" required>
<!-- 输入 "   " 会被 trim 为 "",验证失败 -->
<!-- 关闭 trim -->
<input type="text" ng-model="user.name" ng-trim="false" required>
<!-- 输入 "   " 不会被 trim,验证通过 -->

2.2 minlength / ng-minlength

最小长度验证

<!-- 静态最小长度 -->
<input type="text" name="username" ng-model="user.username" ng-minlength="3">
<!-- 动态最小长度 -->
<input type="text" name="password" ng-model="user.password" ng-minlength="minLen">
$scope.minLen = 6; // 可动态修改

$error 键名: minlength

<span ng-show="myForm.username.$error.minlength">至少输入3个字符</span>

注意: minlength(HTML5 原生属性)与 ng-minlength(AngularJS 属性)的区别:

属性 行为
minlength="3" HTML5 原生验证,AngularJS 不识别
ng-minlength="3" AngularJS 验证,正确更新 $error

始终使用 ng-minlength 而非 minlength


2.3 maxlength / ng-maxlength

最大长度验证

<!-- 静态最大长度 -->
<input type="text" name="username" ng-model="user.username" ng-maxlength="20">
<!-- 动态最大长度 -->
<input type="text" name="bio" ng-model="user.bio" ng-maxlength="maxLen">

$error 键名: maxlength

<span ng-show="myForm.username.$error.maxlength">最多输入20个字符</span>

重要区别:

属性 行为
maxlength="20" HTML5 原生属性,物理限制输入,超过20字符无法输入
ng-maxlength="20" AngularJS 验证,允许输入但标记为无效
<!-- ❌ 用户无法输入超过20个字符,体验差 -->
<input type="text" ng-model="user.name" maxlength="20">
<!-- ✅ 用户可以输入,但超过20字符时显示错误 -->
<input type="text" ng-model="user.name" ng-maxlength="20">

2.4 min / max

数值范围验证,适用于 type="number"type="date"

<!-- 数值范围 -->
<input type="number" name="age" ng-model="user.age" min="1" max="150">
<!-- 日期范围 -->
<input type="date" name="birthday" ng-model="user.birthday" min="1900-01-01" max="2024-12-31">

$error 键名: minmax

<span ng-show="myForm.age.$error.min">年龄不能小于1</span>
<span ng-show="myForm.age.$error.max">年龄不能大于150</span>

注意: min/max 同时被 HTML5 和 AngularJS 识别,无需加 ng- 前缀。


2.5 pattern / ng-pattern

正则表达式验证

<!-- 静态正则(内联写法,用 /.../ 包裹) -->
<input type="text" name="code" ng-model="user.code" ng-pattern="/^[A-Z]{2}d{4}$/">
<!-- 动态正则(绑定到 scope 变量) -->
<input type="text" name="phone" ng-model="user.phone" ng-pattern="phonePattern">
$scope.phonePattern = /^1[3-9]d{9}$/;

$error 键名: pattern

<span ng-show="myForm.code.$error.pattern">格式:2个大写字母+4位数字</span>

常见正则示例:

// 手机号
/1[3-9]d{9}$/
// 邮箱(简化版)
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/
// 身份证号
/d{17}[dXx]$/
// 邮政编码
/d{6}$/
// 密码(至少8位,含字母和数字)
/(?=.*[A-Za-z])(?=.*d)[A-Za-zd]{8,}$/
// IP 地址
/((25[0-5]|2[0-4]d|[01]?dd?).){3}(25[0-5]|2[0-4]d|[01]?dd?)$/
// 中文姓名
/[u4e00-u9fa5]{2,6}$/

2.6 type 属性验证

通过设置 type 属性,AngularJS 自动进行格式验证:

type 值 验证规则 $error 键名
email 邮箱格式 email
url URL 格式 url
number 数字格式 number
date 日期格式 date
time 时间格式 time
week 周格式 week
month 月格式 month
datetime-local 本地日期时间 datetimelocal
<!-- 邮箱验证 -->
<input type="email" name="email" ng-model="user.email" required>
<span ng-show="myForm.email.$error.email">邮箱格式不正确</span>
<!-- URL 验证 -->
<input type="url" name="website" ng-model="user.website">
<span ng-show="myForm.website.$error.url">URL 格式不正确</span>
<!-- 数字验证 -->
<input type="number" name="amount" ng-model="user.amount">
<span ng-show="myForm.amount.$error.number">请输入有效数字</span>

type=“email” 验证规则:

  • 基本格式:xxx@xxx.xxx
  • 不是严格的 RFC 5322 验证,而是实用简化版
  • 空值时如果无 required,验证通过

type=“number” 的特殊行为:

  • 非数字输入时,ng-model 不会被更新(值为 undefined
  • $error.numbertrue
  • 这意味着用户输入 “abc” 时,模型值不会变成 “abc”

三、$error 对象深入解析

3.1 控件级 $error

每个带 ng-modelname 的控件都有自己的 $error 对象:

// myForm.username.$error
{
    required: true,      // 未填写
    minlength: true,     // 长度不足
    pattern: false,      // 正则通过
    maxlength: false     // 长度未超限
}
  • 值为 true → 该验证未通过
  • 值为 false 或不存在 → 该验证通过或不适用

3.2 表单级 $error

表单的 $error 按验证类型分组,值为未通过该验证的控件数组

// myForm.$error
{
    required: [
        myForm.username,   // username 未填写
        myForm.email       // email 未填写
    ],
    email: [
        myForm.email       // email 格式错误
    ],
    minlength: [
        myForm.username    // username 长度不足
    ]
}

3.3 利用 $error 的实用技巧

<!-- 统计表单错误数 -->
<p>共有 {{ countErrors(myForm.$error) }} 个验证错误</p>
$scope.countErrors = function(formError) {
    var count = 0;
    angular.forEach(formError, function(controls) {
        count += controls.length;
    });
    return count;
};

四、验证状态与 CSS 类

4.1 自动添加的 CSS 类

状态 添加的 CSS 类 移除的 CSS 类
未修改 ng-pristine ng-dirty
已修改 ng-dirty ng-pristine
验证通过 ng-valid ng-invalid
验证失败 ng-invalid ng-valid
未失焦 ng-untouched ng-touched
已失焦 ng-touched ng-untouched
特定验证通过 ng-valid-xxx
特定验证失败 ng-invalid-xxx

示例: <input name="email" type="email" ng-model="user.email" required>

验证失败时,该元素会有以下 CSS 类:

ng-dirty ng-invalid ng-invalid-required ng-invalid-email ng-touched

验证通过时:

ng-dirty ng-valid ng-valid-required ng-valid-email ng-touched

4.2 实用 CSS 样式

/* 基础样式 */
input, select, textarea {
    border: 2px solid #ddd;
    padding: 8px 12px;
    transition: border-color 0.3s;
}
/* 已修改且有效 —— 绿色 */
input.ng-dirty.ng-valid,
select.ng-dirty.ng-valid,
textarea.ng-dirty.ng-valid {
    border-color: #4caf50;
}
/* 已修改且无效 —— 红色 */
input.ng-dirty.ng-invalid,
select.ng-dirty.ng-invalid,
textarea.ng-dirty.ng-invalid {
    border-color: #f44336;
}
/* 原始状态不显示验证颜色 */
input.ng-pristine,
select.ng-pristine,
textarea.ng-pristine {
    border-color: #ddd;
}
/* 特定验证失败的样式 */
input.ng-invalid-required {
    /* 可针对 required 做特殊样式 */
}
/* 错误消息动画 */
.error-message {
    color: #f44336;
    font-size: 0.85em;
    animation: fadeIn 0.3s;
}
@keyframes fadeIn {
    from { opacity: 0; transform: translateY(-5px); }
    to { opacity: 1; transform: translateY(0); }
}

五、ngMessages 验证消息管理

5.1 传统方式的问题

<!-- ❌ 繁琐:每个错误都要写 ng-show 判断 -->
<span ng-show="myForm.email.$dirty && myForm.email.$error.required">邮箱必填</span>
<span ng-show="myForm.email.$dirty && myForm.email.$error.email">邮箱格式错误</span>
<span ng-show="myForm.email.$dirty && myForm.email.$error.minlength">太短</span>
<span ng-show="myForm.email.$dirty && myForm.email.$error.maxlength">太长</span>

5.2 ngMessages 方式

<!-- ✅ 简洁 -->
<div ng-messages="myForm.email.$error" ng-if="myForm.email.$dirty">
    <div ng-message="required">邮箱必填</div>
    <div ng-message="email">邮箱格式错误</div>
    <div ng-message="minlength">太短</div>
    <div ng-message="maxlength">太长</div>
</div>

核心优势:

  • 默认只显示第一条错误(优先级由上到下)
  • 代码量大幅减少
  • 支持模板复用

5.3 消息优先级

<div ng-messages="myForm.username.$error" ng-if="myForm.username.$dirty">
    <!-- 优先级从上到下,只显示第一条匹配的错误 -->
    <div ng-message="required">用户名必填</div>        <!-- 优先级1 -->
    <div ng-message="minlength">用户名太短</div>        <!-- 优先级2 -->
    <div ng-message="maxlength">用户名太长</div>        <!-- 优先级3 -->
    <div ng-message="pattern">格式不正确</div>          <!-- 优先级4 -->
</div>

设计原则: 把最重要的错误消息放在最前面。

5.4 显示所有错误

<div ng-messages="myForm.username.$error"
     ng-if="myForm.username.$dirty"
     ng-messages-multiple>
    <!-- ng-messages-multiple:显示所有匹配的错误 -->
    <div ng-message="required">用户名必填</div>
    <div ng-message="minlength">用户名太短</div>
    <div ng-message="pattern">格式不正确</div>
</div>

5.5 复用消息模板

<!-- 定义通用模板 -->
<script type="text/ng-template" id="validation-messages.html">
    <div ng-message="required">此字段为必填项</div>
    <div ng-message="minlength">输入内容太短</div>
    <div ng-message="maxlength">输入内容太长</div>
    <div ng-message="email">邮箱格式不正确</div>
    <div ng-message="url">URL 格式不正确</div>
    <div ng-message="number">请输入有效数字</div>
    <div ng-message="min">值太小</div>
    <div ng-message="max">值太大</div>
    <div ng-message="pattern">格式不符合要求</div>
    <div ng-message="date">日期格式不正确</div>
</script>
<!-- 引用模板 -->
<div ng-messages="myForm.email.$error"
     ng-if="myForm.email.$dirty"
     ng-messages-include="validation-messages.html">
</div>
<!-- 引用模板 + 追加自定义消息(自定义消息优先于模板) -->
<div ng-messages="myForm.password.$error"
     ng-if="myForm.password.$dirty"
     ng-messages-include="validation-messages.html">
    <div ng-message="pattern">密码需至少8位,包含字母和数字</div>
</div>

六、自定义验证

6.1 同步验证器($validators)

// 验证年龄是否为偶数(示例)
app.directive('evenNumber', [function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            ngModelCtrl.$validators.evenNumber = function(modelValue, viewValue) {
                if (ngModelCtrl.$isEmpty(modelValue)) {
                    return true; // 空值交给 required 处理
                }
                return modelValue % 2 === 0;
            };
        }
    };
}]);
<input type="number" name="age" ng-model="user.age" even-number>
<div ng-messages="myForm.age.$error" ng-if="myForm.age.$dirty">
    <div ng-message="evenNumber">年龄必须是偶数</div>
</div>

$validators 规则:

  • 返回 true → 验证通过
  • 返回 false → 验证失败,$error[验证名] = true
  • 函数参数:(modelValue, viewValue)
  • 空值时应返回 true,让 required 单独处理

6.2 异步验证器($asyncValidators)

适用于需要后端验证的场景,如检查用户名是否已存在。

app.directive('uniqueUsername', ['$q', '$http', function($q, $http) {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            ngModelCtrl.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
                if (ngModelCtrl.$isEmpty(modelValue)) {
                    return $q.resolve();
                }
                // 返回 Promise
                return $http.get('/api/check-username', {
                    params: { username: modelValue }
                }).then(function(response) {
                    if (response.data.exists) {
                        return $q.reject('用户名已存在'); // 验证失败
                    }
                    return true; // 验证通过
                }, function() {
                    return $q.reject('验证服务不可用'); // 请求失败
                });
            };
        }
    };
}]);
<input type="text"
       name="username"
       ng-model="user.username"
       required
       unique-username>
<div ng-messages="myForm.username.$error" ng-if="myForm.username.$dirty">
    <div ng-message="required">用户名必填</div>
    <div ng-message="uniqueUsername">该用户名已被注册</div>
</div>
<!-- 显示验证中状态 -->
<span ng-show="myForm.username.$pending">验证中...</span>

$asyncValidators 规则:

  • 返回 resolved Promise → 验证通过
  • 返回 rejected Promise → 验证失败
  • 验证进行中时,$pending 对象中会包含该验证器
  • 异步验证器只在同步验证通过后才执行

6.3 验证器执行顺序

输入值变化
  │
  ▼
1. $parsers 处理(viewValue → modelValue 转换)
  │
  ▼
2. $validators 同步验证
  │  ── 任一失败则停止,不执行异步验证
  ▼
3. $asyncValidators 异步验证
  │  ── 并行执行所有异步验证器
  ▼
4. 更新 $error 和 CSS 类

6.4 复杂自定义验证:密码强度

app.directive('passwordStrength', [function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            // 密码强度等级
            var STRENGTH = {
                WEAK: 'weak',
                MEDIUM: 'medium',
                STRONG: 'strong'
            };
            function getStrength(value) {
                if (!value) return null;
                var score = 0;
                if (value.length >= 8) score++;
                if (value.length >= 12) score++;
                if (/[A-Z]/.test(value)) score++;
                if (/[a-z]/.test(value)) score++;
                if (/d/.test(value)) score++;
                if (/[^A-Za-z0-9]/.test(value)) score++;
                if (score <= 2) return STRENGTH.WEAK;
                if (score <= 4) return STRENGTH.MEDIUM;
                return STRENGTH.STRONG;
            }
            // 验证器:至少中等强度
            ngModelCtrl.$validators.passwordStrength = function(modelValue) {
                if (ngModelCtrl.$isEmpty(modelValue)) return true;
                var strength = getStrength(modelValue);
                return strength !== STRENGTH.WEAK;
            };
            // 在 scope 上暴露强度值,供视图显示
            scope.$watch(function() {
                return ngModelCtrl.$viewValue;
            }, function(newVal) {
                scope.passwordStrength = getStrength(newVal);
            });
        }
    };
}]);
<input type="password"
       name="password"
       ng-model="user.password"
       required
       password-strength>
<!-- 强度指示器 -->
<div ng-if="user.password" ng-switch="passwordStrength">
    <div ng-switch-when="weak" style="color:red"></div>
    <div ng-switch-when="medium" style="color:orange"></div>
    <div ng-switch-when="strong" style="color:green"></div>
</div>
<div ng-messages="myForm.password.$error" ng-if="myForm.password.$dirty">
    <div ng-message="required">密码必填</div>
    <div ng-message="passwordStrength">密码强度不足,至少需包含大小写字母和数字</div>
</div>

6.5 比较验证:确认密码

app.directive('matchField', [function() {
    return {
        require: 'ngModel',
        scope: {
            matchField: '='
        },
        link: function(scope, element, attrs, ngModelCtrl) {
            ngModelCtrl.$validators.matchField = function(modelValue) {
                if (ngModelCtrl.$isEmpty(modelValue) && ngModelCtrl.$isEmpty(scope.matchField)) {
                    return true;
                }
                return modelValue === scope.matchField;
            };
            // 当比较字段变化时,重新验证
            scope.$watch('matchField', function() {
                ngModelCtrl.$validate();
            });
        }
    };
}]);
<input type="password" name="password" ng-model="user.password" required>
<input type="password"
       name="confirmPassword"
       ng-model="user.confirmPassword"
       required
       match-field="user.password">
<div ng-messages="myForm.confirmPassword.$error" ng-if="myForm.confirmPassword.$dirty">
    <div ng-message="required">请确认密码</div>
    <div ng-message="matchField">两次密码不一致</div>
</div>

七、$setValidity 与手动验证控制

7.1 $setValidity

// 在指令中
ngModelCtrl.$setValidity('customError', false);  // 设置为无效
ngModelCtrl.$setValidity('customError', true);   // 设置为有效
// 在控制器中通过表单访问
$scope.myForm.email.$setValidity('serverError', false);
<div ng-message="serverError">服务器验证失败:{{ serverErrorMsg }}</div>

7.2 服务器端验证反馈

$scope.submit = function() {
    $http.post('/api/register', $scope.user)
        .then(function(response) {
            // 成功
        }, function(error) {
            // 服务器返回字段级错误
            if (error.data.field === 'email') {
                $scope.myForm.email.$setValidity('serverError', false);
                $scope.serverErrorMsg = error.data.message;
            }
        });
};

7.3 $validate 手动触发验证

// 手动触发某个控件的所有验证
$scope.myForm.email.$validate();
// 通常在程序修改模型值后使用
$scope.user.email = 'new@example.com';
$scope.myForm.email.$validate();

八、验证时机控制

8.1 默认行为

AngularJS 在每次 ng-model 值变化时都会触发验证,这意味着用户还没输入完就可能看到错误提示。

8.2 只在失焦时显示错误

<!-- 方式一:用 $touched 判断 -->
<div ng-messages="myForm.email.$error" ng-if="myForm.email.$touched">
    <div ng-message="required">邮箱必填</div>
    <div ng-message="email">邮箱格式错误</div>
</div>

8.3 只在提交后显示错误

<form name="myForm" ng-submit="submit()" novalidate>
    <input type="email" name="email" ng-model="user.email" required>
    <div ng-messages="myForm.email.$error" ng-if="myForm.$submitted">
        <div ng-message="required">邮箱必填</div>
        <div ng-message="email">邮箱格式错误</div>
    </div>
    <button type="submit">提交</button>
</form>

8.4 自定义显示策略指令

// 只在失焦或提交后显示错误
app.directive('showErrors', [function() {
    return {
        restrict: 'A',
        require: '^form',
        link: function(scope, element, attrs, formCtrl) {
            var inputEl = element[0].querySelector('[name]');
            var inputName = attrs.showErrors || inputEl.getAttribute('name');
            var inputCtrl = formCtrl[inputName];
            scope.$watch(function() {
                return {
                    dirty: inputCtrl.$dirty,
                    touched: inputCtrl.$touched,
                    submitted: formCtrl.$submitted,
                    invalid: inputCtrl.$invalid
                };
            }, function(state) {
                var show = state.invalid && (state.dirty || state.touched || state.submitted);
                element.toggleClass('has-error', show);
            }, true);
        }
    };
}]);
<div show-errors>
    <label>邮箱</label>
    <input type="email" name="email" ng-model="user.email" required>
    <div ng-messages="myForm.email.$error" ng-if="myForm.email.$invalid && (myForm.email.$dirty || myForm.email.$touched || myForm.$submitted)">
        <div ng-message="required">邮箱必填</div>
        <div ng-message="email">邮箱格式错误</div>
    </div>
</div>

九、各表单元素验证要点

9.1 input[type=“text”]

<input type="text"
       name="username"
       ng-model="user.username"
       required
       ng-minlength="3"
       ng-maxlength="20"
       ng-pattern="/^[a-zA-Z0-9_]+$/">

9.2 input[type=“email”]

<input type="email" name="email" ng-model="user.email" required>
<!-- $error.email:邮箱格式验证 -->

9.3 input[type=“number”]

<input type="number" name="age" ng-model="user.age" required min="0" max="150">
<!-- $error.number:非数字 -->
<!-- $error.min / $error.max:范围 -->

注意: 非数字输入时 ng-model 为 undefined,不会更新视图值。

9.4 input[type=“url”]

<input type="url" name="website" ng-model="user.website">
<!-- $error.url:URL 格式验证 -->
<!-- 合法格式:http://example.com -->

9.5 input[type=“checkbox”]

<input type="checkbox" name="agree" ng-model="user.agree" required>
<!-- required 验证:必须勾选(值为 true) -->
<span ng-show="myForm.agree.$error.required">必须同意条款</span>

9.6 input[type=“radio”]

<!-- radio 的 required 验证:必须选择一个 -->
<input type="radio" name="gender" ng-model="user.gender" value="male" required><input type="radio" name="gender" ng-model="user.gender" value="female" required><span ng-show="myForm.gender.$error.required">请选择性别</span>

9.7 select

<select name="city" ng-model="user.city" required>
    <option value="">请选择</option>
    <option value="bj">北京</option>
    <option value="sh">上海</option>
</select>
<span ng-show="myForm.city.$error.required">请选择城市</span>
<!-- 使用 ng-options(推荐) -->
<select name="city"
        ng-model="user.city"
        ng-options="c.code as c.name for c in cities"
        required>
    <option value="">请选择</option>
</select>

9.8 textarea

<textarea name="bio"
          ng-model="user.bio"
          required
          ng-minlength="10"
          ng-maxlength="500"
          rows="4">
</textarea>

十、验证与 $parsers / $formatters

10.1 管道流程

视图值 (viewValue)
    │
    ▼  $parsers (数组,按顺序执行)
    │  ── 验证通常在此进行
    │  ── 返回值传递给下一个 parser
    ▼
模型值 (modelValue)
    │
    ▼  $formatters (数组,按顺序执行)
    │  ── 模型→视图的转换
    ▼
视图值 (viewValue)

10.2 使用 $parsers 添加验证

app.directive('noSpaces', [function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            ngModelCtrl.$parsers.push(function(viewValue) {
                if (/s/.test(viewValue)) {
                    ngModelCtrl.$setValidity('noSpaces', false);
                    return undefined; // 不更新模型
                }
                ngModelCtrl.$setValidity('noSpaces', true);
                return viewValue; // 正常传递
            });
        }
    };
}]);

$parsers vs $validators:

特性 $parsers $validators
执行时机 viewValue → modelValue 转换时 模型值变化后
能否修改值 可以 不可以
推荐程度 旧方式(1.3 之前) 推荐(1.3+)
返回值 返回处理后的值或 undefined 返回 boolean

建议: 优先使用 $validators,仅在需要转换值时使用 $parsers


十一、完整实战:多字段注册表单

<!DOCTYPE html>
<html ng-app="regApp">
<head>
    <meta charset="UTF-8">
    <title>注册表单 - 输入验证</title>
    <script src="angular.js"></script>
    <script src="angular-messages.js"></script>
    <style>
        * { box-sizing: border-box; }
        body { font-family: Arial, sans-serif; max-width: 500px; margin: 40px auto; }
        .form-group { margin-bottom: 20px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
        input, select, textarea {
            width: 100%; padding: 10px; border: 2px solid #ddd;
            border-radius: 4px; font-size: 14px;
            transition: border-color 0.3s, box-shadow 0.3s;
        }
        input:focus, select:focus, textarea:focus {
            outline: none; border-color: #2196f3;
            box-shadow: 0 0 0 3px rgba(33,150,243,0.1);
        }
        input.ng-dirty.ng-valid, select.ng-dirty.ng-valid {
            border-color: #4caf50;
        }
        input.ng-dirty.ng-invalid, select.ng-dirty.ng-invalid {
            border-color: #f44336;
        }
        .error-messages { color: #f44336; font-size: 0.85em; margin-top: 4px; }
        .error-messages div { margin-top: 2px; }
        .strength-bar { height: 4px; margin-top: 6px; border-radius: 2px; transition: all 0.3s; }
        .strength-weak { background: #f44336; width: 33%; }
        .strength-medium { background: #ff9800; width: 66%; }
        .strength-strong { background: #4caf50; width: 100%; }
        .pending { color: #ff9800; font-style: italic; }
        button {
            padding: 12px 30px; border: none; border-radius: 4px;
            font-size: 16px; cursor: pointer; margin-right: 10px;
        }
        .btn-primary { background: #2196f3; color: white; }
        .btn-primary:disabled { background: #ccc; cursor: not-allowed; }
        .btn-reset { background: #eee; color: #333; }
    </style>
</head>
<body ng-controller="RegController">
    <h2>用户注册</h2>
    <form name="regForm" ng-submit="register()" novalidate>
        <!-- 用户名 -->
        <div class="form-group">
            <label>用户名</label>
            <input type="text"
                   name="username"
                   ng-model="user.username"
                   required
                   ng-minlength="3"
                   ng-maxlength="20"
                   ng-pattern="/^[a-zA-Z0-9_]+$/"
                   unique-username
                   placeholder="3-20位字母、数字或下划线">
            <div class="error-messages"
                 ng-messages="regForm.username.$error"
                 ng-if="regForm.username.$dirty">
                <div ng-message="required">用户名必填</div>
                <div ng-message="minlength">用户名至少3个字符</div>
                <div ng-message="maxlength">用户名最多20个字符</div>
                <div ng-message="pattern">只能包含字母、数字和下划线</div>
                <div ng-message="uniqueUsername">该用户名已被注册</div>
            </div>
            <div class="pending" ng-show="regForm.username.$pending">验证中...</div>
        </div>
        <!-- 邮箱 -->
        <div class="form-group">
            <label>邮箱</label>
            <input type="email"
                   name="email"
                   ng-model="user.email"
                   required
                   placeholder="example@domain.com">
            <div class="error-messages"
                 ng-messages="regForm.email.$error"
                 ng-if="regForm.email.$dirty">
                <div ng-message="required">邮箱必填</div>
                <div ng-message="email">邮箱格式不正确</div>
            </div>
        </div>
        <!-- 手机号 -->
        <div class="form-group">
            <label>手机号</label>
            <input type="text"
                   name="phone"
                   ng-model="user.phone"
                   required
                   ng-pattern="/^1[3-9]d{9}$/"
                   placeholder="11位手机号">
            <div class="error-messages"
                 ng-messages="regForm.phone.$error"
                 ng-if="regForm.phone.$dirty">
                <div ng-message="required">手机号必填</div>
                <div ng-message="pattern">请输入有效的手机号</div>
            </div>
        </div>
        <!-- 密码 -->
        <div class="form-group">
            <label>密码</label>
            <input type="password"
                   name="password"
                   ng-model="user.password"
                   required
                   ng-minlength="8"
                   password-strength
                   placeholder="至少8位">
            <div class="strength-bar"
                 ng-class="{'strength-weak': passwordStrength==='weak',
                            'strength-medium': passwordStrength==='medium',
                            'strength-strong': passwordStrength==='strong'}"
                 ng-if="user.password">
            </div>
            <div class="error-messages"
                 ng-messages="regForm.password.$error"
                 ng-if="regForm.password.$dirty">
                <div ng-message="required">密码必填</div>
                <div ng-message="minlength">密码至少8个字符</div>
                <div ng-message="passwordStrength">密码强度不足</div>
            </div>
        </div>
        <!-- 确认密码 -->
        <div class="form-group">
            <label>确认密码</label>
            <input type="password"
                   name="confirmPwd"
                   ng-model="user.confirmPwd"
                   required
                   match-field="user.password"
                   placeholder="再次输入密码">
            <div class="error-messages"
                 ng-messages="regForm.confirmPwd.$error"
                 ng-if="regForm.confirmPwd.$dirty">
                <div ng-message="required">请确认密码</div>
                <div ng-message="matchField">两次密码不一致</div>
            </div>
        </div>
        <!-- 年龄 -->
        <div class="form-group">
            <label>年龄</label>
            <input type="number"
                   name="age"
                   ng-model="user.age"
                   required
                   min="1"
                   max="150"
                   placeholder="1-150">
            <div class="error-messages"
                 ng-messages="regForm.age.$error"
                 ng-if="regForm.age.$dirty">
                <div ng-message="required">年龄必填</div>
                <div ng-message="number">请输入有效数字</div>
                <div ng-message="min">年龄不能小于1</div>
                <div ng-message="max">年龄不能大于150</div>
            </div>
        </div>
        <!-- 同意条款 -->
        <div class="form-group">
            <label>
                <input type="checkbox" name="agree" ng-model="user.agree" required>
                我已阅读并同意服务条款
            </label>
            <div class="error-messages" ng-if="regForm.agree.$dirty && regForm.agree.$error.required">
                必须同意条款才能注册
            </div>
        </div>
        <button type="submit" class="btn-primary" ng-disabled="regForm.$invalid">
            注册
        </button>
        <button type="button" class="btn-reset" ng-click="reset()">
            重置
        </button>
    </form>
</body>
</html>

十二、常见问题与陷阱

12.1 novalidate 必须加

<!-- ❌ 浏览器原生验证和 AngularJS 验证同时生效 -->
<form name="myForm">
<!-- ✅ 禁用浏览器原生验证 -->
<form name="myForm" novalidate>

12.2 ng-model 是前提

<!-- ❌ 没有 ng-model,验证不生效 -->
<input type="text" name="username" required>
<!-- ✅ 必须有 ng-model -->
<input type="text" name="username" ng-model="user.username" required>

12.3 name 属性不可省略

<!-- ❌ 没有 name,无法通过表单访问控件 -->
<input type="text" ng-model="user.name" required>
<!-- ✅ 有 name 才能访问 myForm.username -->
<input type="text" name="username" ng-model="user.name" required>

12.4 type=“number” 的特殊行为

<!-- 输入 "abc" 时:ng-model 为 undefined,$error.number = true -->
<!-- 输入 "12.5" 时:ng-model 为 12.5,验证通过 -->
<!-- 输入 "12.5.3" 时:ng-model 为 undefined,$error.number = true -->
<!-- 如果需要字符串类型的数字,用 type="text" + ng-pattern -->
<input type="text" ng-model="user.amount" ng-pattern="/^d+(.d{1,2})?$/">

12.5 验证与 ng-if 的交互

<!-- ng-if 会销毁/重建元素,导致表单控件从表单中移除/添加 -->
<div ng-if="showField">
    <input name="optional" ng-model="data.value" required>
</div>
<!-- 当 showField 为 false 时,myForm.optional 不存在 -->
<!-- 当 showField 为 true 时,myForm.optional 重新创建 -->

12.6 异步验证的防抖

// 异步验证每次输入都会发请求,需要防抖
app.directive('uniqueUsername', ['$q', '$http', '$timeout', function($q, $http, $timeout) {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModelCtrl) {
            var timeoutPromise;
            ngModelCtrl.$asyncValidators.uniqueUsername = function(modelValue) {
                if (ngModelCtrl.$isEmpty(modelValue)) return $q.resolve();
                // 取消上一次的请求
                if (timeoutPromise) $timeout.cancel(timeoutPromise);
                // 延迟 500ms 发送请求
                return $timeout(function() {}, 500)
                    .then(function() {
                        return $http.get('/api/check-username', {
                            params: { username: modelValue }
                        });
                    })
                    .then(function(response) {
                        if (response.data.exists) {
                            return $q.reject('已存在');
                        }
                        return true;
                    });
            };
        }
    };
}]);

十三、总结思维导图

AngularJS 输入验证
├── 前提条件
│   ├── ng-model(必须)
│   ├── name 属性(必须)
│   └── novalidate(建议)
├── 内置验证属性
│   ├── required / ng-required ── 必填
│   ├── ng-minlength / ng-maxlength ── 长度
│   ├── min / max ── 范围
│   ├── ng-pattern ── 正则
│   └── type="email|url|number|date" ── 格式
├── 验证状态
│   ├── $valid / $invalid ── 验证结果
│   ├── $pristine / $dirty ── 修改状态
│   ├── $touched / $untouched ── 焦点状态
│   ├── $error ── 错误详情
│   └── $pending ── 异步验证中
├── 自动 CSS 类
│   ├── ng-valid / ng-invalid
│   ├── ng-dirty / ng-pristine
│   ├── ng-touched / ng-untouched
│   └── ng-valid-xxx / ng-invalid-xxx
├── ngMessages
│   ├── ng-messages ── 消息容器
│   ├── ng-message ── 单条消息
│   ├── ng-messages-multiple ── 显示多条
│   └── ng-messages-include ── 复用模板
├── 自定义验证
│   ├── $validators ── 同步验证
│   ├── $asyncValidators ── 异步验证
│   ├── $setValidity() ── 手动设置
│   ├── $validate() ── 手动触发
│   └── $parsers ── 旧方式(1.3前)
├── 验证时机
│   ├── 实时验证(默认)
│   ├── 失焦验证($touched)
│   ├── 提交验证($submitted)
│   └── 自定义策略
└── 常见陷阱
    ├── novalidate 遗漏
    ├── ng-model 缺失
    ├── type="number" 行为
    ├── ng-if 销毁控件
    └── 异步验证需防抖

以上内容系统覆盖了 AngularJS 输入验证的内置属性、状态追踪、消息管理、自定义验证、验证时机控制及常见陷阱,可作为完整的学习和开发参考。

© 版权声明

相关文章