详谈Angular 2+ 的表单(一)之模板驱动型表单
在企业应用开发时,表单是一个躲不过去的事情,和面向消费者的应用不同,企业领域的开发中,表单的使用量是惊人的。这些表单的处理其实是一个挺复杂的事情,比如有的是涉及到多个 Tab 的表单,有的是向导形式多个步骤的,各种复杂的验证逻辑和时不时需要弹出的对话框等等。笔者试图在这一系列文章 中对 Angular 中的表单处理做一个相对完整的梳理。
Angular 中提供两种类型的表单处理机制,一种叫模版驱动型(Template Driven)的表单,另一种叫模型驱动型表单( Model Driven ),这后一种也叫响应式表单 ( Reactive Forms ),由于模版驱动中有一个 ngModel 的指令,容易和这里说的模型驱动混淆,所以在我们的文章 中叫后一种说法:响应式表单。
第一篇主要介绍模版驱动型的表单。
号外
本文评论 区会抽出5位童鞋,赠送笔者的 《Angular 从零到一》纸书,机不可失,大家踊跃发言哦。
模版驱动的表单
模版驱动的表单和 AngularJS 对于表单的处理类似,把一些指令(比如 ngModel )、数据值和行为约束(比如 require 、 minlength 等等)绑定到模版中(模版就是组件元数据 @Component 中定义的那个 template ),这也是模版驱动这个叫法的来源。总体来说,这种类型的表单通过绑定把很多工作交给了模版。
模版驱动的例子
还是用例子来说话,比如我们有一个用户 注册 的表单,用户名 就是 email ,还需要填的信息有:住址、密码和重复密码。这个应该是比较常见的一个注册 时需要的信息了。那么我们第一步来建立领域模型:
用户id一般由服务器
自动 生成 ,所以可以为空,用 ? 标示
id?: string;
email: string;
password: string;
repeat: string;
address: Address;
}
export interface Address {
province: string; // 省份
city: string; // 城市
area: string; // 区县
addr: string; // 详细地址
}
接下来我们建立模版文件 ,一个最简单的 HTML 模版,先不增加 任何的绑定或事件处理:
渲染之后的效果 就像下面这样:
简单的Form
数据绑定
对于模版驱动型的表单处理,我们首先需要在对应的模块中引入 FormsModule ,这一点千万不要忘记了。
有了这样一个成员变量之后,我们在组件模版中就可以使用 ngModel 进行绑定了。
令人困惑的 ngModel
我们在 Angular 中可以使用三种形式的 ngModel 表达式: ngModel,[ngModel] 和 [(ngModel)] 。但无论那种形式,如果你要使用 ngModel 就必须为该控件(比如下面的 input )指定一个 name 属性 ,如果你忘记添加 name 的话,多半你会看到下面这样的错误 :
ERROR Error: Uncaught (in promise): Error: If ngModel is used within a form tag,either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.
ngModel 和 FormControl
假如我们使用的是 ngModel ,没有任何中括号小括号的话,这代表着我们创建了一个 FormControl 的实例,这个实例将会跟踪值的变化、用户 的交互、验证状态以及保持视图和领域对象的同步等工作。
如果我们将这个控件放在一个 Form 表单中, ngModel 会自动 将这个 FormControl 注册 为 Form 的子控件。下面的例子中我们在
此时我们将默认的电子邮件改成 的话,表单的值是改变了,但 user 并未改变。
输入的值影响了表单,但不会影响领域对象
如果我们希望的是在输入时,这个输入的值也反向的影响我们的 user 对象的值的话,那就需要用到双向绑定了,也就是 [(ngModel)] 需要上场了。
表单和领域对象的值保持了同步
无论如何,这个 [()] 表达真是很奇怪的样子,其实这个表达是一个语法糖。只要我们知道下面的两种写法是等价的,我们就会很清楚的理解了:用这个语法糖你就不用既写数据绑定又写事件绑定了。
ngModelGroup 是什么鬼?
如果我们仔细观察上面的输出 的话,会发现一个问题: user 中是有一个嵌套对象 address 的,而表单中没有嵌套对象的。如果要实现表单中的结构和领域对象的结构一致的话,我们就得请出 ngModelGroup 了。 ngModelGroup 会创建并绑定一个 FormGroup 到该 DOM 元素。 FormGroup 又是什么呢?简单来说,是一组 FormControl。
这样的话,我们再来看一下输出 ,现在就完全一致了:
表单和领域对象的结构也完全一致了
数据验证
模版驱动型的表单的验证也是主要由模版来处理的,在看怎么使用之前,需要界定一下验证规则:
三个必填项: email,password 和 repeat
email 的形式需要符合电子邮件 的标准
password 和 repeat 必须一致
当然除了这几个规则,我们还希望在表单未验证通过时提交按钮是不可用的。
我们看到,如果不填电子邮件 的话,错误 的 JSON 是 {"required ": true} ,这告诉我们目前有一个 required 的规则没有被满足。
验证结果
当我们输入一个字母 w 之后,就会发现错误 变成了下面的样子。这是因为我们对于 email 应用了多个规则,当必填项满足后,系统会继续检查其他验证结果。
requiredPattern": "^([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+@([a-zA-Z0-9]+[_|_|.]?)*[a-zA-Z0-9]+.[a-zA-Z]{2,4}$","actualValue": "w"
}
}
通过几次实验,我们应该可以得出结论,当验证未通过时,验证器返回的是一个对象, key 为验证的规则(比如 required ,minlength 等),value 为验证结果。如果验证通过,返回的是一个 null 。
知道这一点后,我们其实就可以做出验证出错的提示 了,为了方便引用,我们还是导出 ngModel 到一个 email 引用,然后就可以访问这个 FormControl 的各个属性 了:验证的状态( valid/invalid )、控件的状态(是否获得过焦点 -- touched/untouched,是否更改过内容 -- pristine/dirty 等)
那么要实现这种形式的验证的话,我们需要建立一个指令,而且这个指令应该实现 Validator 接口。一个基础的框架如下:
RepeatValidatorDirective),multi: true
}
]
})
export class RepeatValidatorDirective implements Validator{
constructor() { }
validate(c: AbstractControl): { [key: string]: any } {
return null;
}
}
我们还没有开始正式的写验证逻辑,但上面的框架已经出现了几个有意思的点:
1.Validator 接口要求必须实现的一个方法 是 validate(c: AbstractControl): ValidationErrors | null; 。这个也就是我们前面提到的验证正确返回 null 否则返回一个对象,虽然没有严格的约束,但其 key 一般用于表示这个验证器的名字或者验证的规则名字,value 一般是失败的原因或验证结果。
2.和组件类似,指令也有 selector 这个元数据,用于选择那个元素应用该指令,那么我们这里除了要求 DOM 元素应用 validateEqual 之外,还需要它是一个 ngModel 元素,这样它才是一个 FormControl,我们在 validate 的时候才是合法的。
3.那么那个 providers 里面那些面目可憎的家伙又是干什么的呢? Angular 对于在一个 FormControl 上执行验证器有一个内部机制: Angular 维护一个令牌为 NG_VALIDATORS 的 multi provider (简单来说,Angular 为一个单一令牌注入多个值的这种形式叫 multi provider )。所有的内建验证器都是加到这个 NG_VALIDATORS 的令牌上的,因此在做验证时,Angular 是注入了 NG_VALIDATORS 的依赖,也就是所有的验证器,然后一个个的按顺序执行。因此我们这里也把自己加到这个 NG_VALIDATORS 中去。
4.但如果我们直接写成 useExisting: RepeatValidatorDirective 会出现一个问题, RepeatValidatorDirective 还没有生成 ,你怎么能在元数据中使用呢?这就需要使用 forwardRef 来解决 这个问题,它接受一个返回一个类的函数 作为参数,但这个函数 不会立即被调用 ,而是在该类声明后被调用 ,也就避免了 undefined 的状况。
下面我们就来实现这个验证逻辑,由于密码和确认密码有主从关系,并非完全的平行关系。也就是说,密码是一个基准对比对象,当密码改变时,我们不应该提示 密码和确认密码不符,而是应该将错误 放在确认密码中。所以我们给出另一个属性 reverse 。
查询且值不相等
if (target && self !== target.value && !this.isReverse) {
return {
validateEqual: true
}
}
// 反向
查询 且值相等
if (target && self === target.value && this.isReverse) {
delete target.errors['validateEqual'];
if (!Object.keys(target.errors).length) target.setErrors(null);
}
// 反向
查询 且值不相等
if (target && self !== target.value && this.isReverse) {
target.setErrors({
validateEqual: true
})
}
return null;
}
}
这样改造后,我们的模版文件 中对于密码和确认密码的验证器如下:
required
minlength="8"
validateEqual="repeat"
reverse="true">
required
minlength="8"
validateEqual="password"
reverse="false">
完成后的验证错误 提示
表单的提交
表单的提交比较简单,绑定表单的 ngSubmit 事件即可
但需要注意的一点是,button如果不指定类型的话,会被当做 type="submit" ,所以当按钮不是进行提交表单的话,需要显式指定 type="button" 。而且如果遇到点击提交按钮页面 刷新的情况的话,意味着默认的表单提交事件引起了浏览器的刷新,这种时候需要阻止事件冒泡。
对于模板驱动的表单,我们就先总结到这里,下一篇 文章 我们会一起讨论响应式表单。
本文代码 : nofollow " href="https://github.com/wpcfan/ng-features.git">https://github.com/wpcfan/ng-features.git
以上所述是小编给大家介绍的Angular 2+ 的表单(一)之模板驱动型表单。编程之家 jb51.cc 收集整理的教程希望能对你有所帮助,如果觉得编程之家不错,可分享 给好友!感谢支持 。