热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

详谈Angular2+的表单(一)之模板驱动型表单

这篇文章主要介绍了Angular2+的表单(一)之模板驱动型表单,非常不错,具有参考借鉴价值,需要的朋友可以参考下

摘要

在企业应用开发时,表单是一个躲不过去的事情,和面向消费者的应用不同,企业领域的开发中,表单的使用量是惊人的。这些表单的处理其实是一个挺复杂的事情,比如有的是涉及到多个 Tab 的表单,有的是向导形式多个步骤的,各种复杂的验证逻辑和时不时需要弹出的对话框等等。笔者试图在这一系列文章中对 Angular 中的表单处理做一个相对完整的梳理。

Angular 中提供两种类型的表单处理机制,一种叫模版驱动型(Template Driven)的表单,另一种叫模型驱动型表单( Model Driven ),这后一种也叫响应式表单 ( Reactive Forms ),由于模版驱动中有一个 ngModel 的指令,容易和这里说的模型驱动混淆,所以在我们的文章中叫后一种说法:响应式表单。

第一篇主要介绍模版驱动型的表单。

号外

本文评论区会抽出5位童鞋,赠送笔者的 《Angular 从零到一》纸书,机不可失,大家踊跃发言哦。

模版驱动的表单

模版驱动的表单和 AngularJS 对于表单的处理类似,把一些指令(比如 ngModel )、数据值和行为约束(比如 require 、 minlength 等等)绑定到模版中(模版就是组件元数据 @Component 中定义的那个 template ),这也是模版驱动这个叫法的来源。总体来说,这种类型的表单通过绑定把很多工作交给了模版。

模版驱动的例子

还是用例子来说话,比如我们有一个用户注册的表单,用户名就是 email ,还需要填的信息有:住址、密码和重复密码。这个应该是比较常见的一个注册时需要的信息了。那么我们第一步来建立领域模型:

// src/app/domain/index.ts
export interface User {
 // 新的用户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 ,这一点千万不要忘记了。

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from "@angular/forms";
import { TemplateDrivenComponent } from './template-driven/template-driven.component';
@NgModule({
 imports: [
 CommonModule,
 FormsModule
 ],
 exports: [TemplateDrivenComponent],
 declarations: [TemplateDrivenComponent]
})
export class FormDemoModule { }
进行模版驱动类型的表单处理的一个必要步骤就是建立数据的双向绑定,那么我们需要在组件中建立一个类型为 User 的成员变量并赋初始值。
// template-driven.component.ts
// 省略元数据和导入的类库信息
export class TemplateDrivenComponent implements OnInit {
 user: User = {
 email: '',
 password: '',
 repeat: '',
 address: {
  province: '',
  city: '',
  area: '',
  addr: ''
 }
 };
 // 省略其他部分
}

有了这样一个成员变量之后,我们在组件模版中就可以使用 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 的子控件。下面的例子中我们在 中加上了 ngForm 指令,声明这是一个 Angular 可识别的表单,而 ngModel 会将 注册成表单的子控件,这个子控件的名字就是 email ,而且 ngModel 会基于这个子控件的值去绑定表单的的值,这也是为什么需要显式声明 name 的原因。

其实在我们导入 FormsModule 的时候,所有的 标签都会默认的被认为是一个 NgForm ,因此我们并不需要显式的在标签中写 ngForm 这个指令。



 

这一切现在都是不可见的,所以大家可能还是有些困惑,那么下面我们将其“可视化”,这需要我们引用一下表单对象,所以我们使用 #f="ngForm" 以便我们可以在模版中输出表单的一些特性。



 ...


{{f.value | json}}

这时如果我们在 email 中输入 sss ,可以看到下图的以 JSON 形式出现的表单值:

 

控件的输入值同步到了表单的值中

单向数据绑定

那么接下来,我们看看 [ngModel] 有什么用?如果我们想给控件设置一个初始值怎么办呢,这时就需要进行一个单向绑定,方向是从组件到视图。我们可以做的是在初始化 User 的时候,将 email 属性设置成 wang@163.com

user: User = {
 email: 'wang@163.com',
 ...
 };

而且在模版中使用 [ngModel]="user.email" 进行单向绑定,这个语法其实和普通的属性绑定是一样的,用中括号标示这是一个要进行数据绑定的属性,等号右边是需要绑定的值(这里是 user.email )。那么我们就可以得到下面这样的输出了, email 的初始值被绑定成功!

单向数据绑定

 

双向数据绑定

但上面的例子存在一个问题,数据的绑定是单向的,也就是说,在输入框进行输入的时候,我们的 user 的值不会随之改变的。为了更好的说明,我们将 user 和 表单的值同时输出

user: {{user | json}}
表单: {{f.value | json}}

此时我们将默认的电子邮件改成 wang@gmail.com 的话,表单的值是改变了,但 user 并未改变。

 

输入的值影响了表单,但不会影响领域对象

如果我们希望的是在输入时,这个输入的值也反向的影响我们的 user 对象的值的话,那就需要用到双向绑定了,也就是 [(ngModel)] 需要上场了。

 

表单和领域对象的值保持了同步

无论如何,这个 [()] 表达真是很奇怪的样子,其实这个表达是一个语法糖。只要我们知道下面的两种写法是等价的,我们就会很清楚的理解了:用这个语法糖你就不用既写数据绑定又写事件绑定了。


ngModelGroup 是什么鬼?

如果我们仔细观察上面的输出的话,会发现一个问题: user 中是有一个嵌套对象 address 的,而表单中没有嵌套对象的。如果要实现表单中的结构和领域对象的结构一致的话,我们就得请出 ngModelGroup 了。 ngModelGroup 会创建并绑定一个 FormGroup 到该 DOM 元素。 FormGroup 又是什么呢?简单来说,是一组 FormControl。


 

这样的话,我们再来看一下输出,现在就完全一致了:

 

表单和领域对象的结构也完全一致了

数据验证

模版驱动型的表单的验证也是主要由模版来处理的,在看怎么使用之前,需要界定一下验证规则:

  • 三个必填项: email , password 和 repeat
  • email 的形式需要符合电子邮件的标准
  • password 和 repeat 必须一致

当然除了这几个规则,我们还希望在表单未验证通过时提交按钮是不可用的。


 
 

Angular 中有几种内建支持的验证器( Validators )

  • required - 需要 FormControl 有非空值
  • minlength - 需要 FormControl 有最小长度的值
  • maxlength - 需要 FormControl 有最大长度的值
  • pattern - 需要 FormControl 的值可以匹配正则表达式

如果我们想看到结果的话,我们可以在模版中加上下面的代码,将错误以 JSON 形式输出即可。

email 验证: {{f.controls.email?.errors | json}}

我们看到,如果不填电子邮件的话,错误的 JSON 是 {"required": true} ,这告诉我们目前有一个 required 的规则没有被满足。

 

验证结果

当我们输入一个字母 w 之后,就会发现错误变成了下面的样子。这是因为我们对于 email 应用了多个规则,当必填项满足后,系统会继续检查其他验证结果。

{ 
"pattern": 
 { 
  "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 等)


email 是必填项
email 格式不正确

自定义验证

内建的验证器对于两个密码比较的这种验证是不够的,那么这就需要我们自己定义一个验证器。对于响应式表单来说,会比较简单一些,但对于模版驱动的表单,这需要我们实现一个指令来使这个验证器更通用和更一致。因为我们希望实现的样子应该是和 required 、 minlength 等差不多的形式,比如下面这个样子 validateEqual="repeat"

那么要实现这种形式的验证的话,我们需要建立一个指令,而且这个指令应该实现 Validator 接口。一个基础的框架如下:

import { Directive, forwardRef } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl } from '@angular/forms';
@Directive({
 selector: '[validateEqual][ngModel]',
 providers: [
 { 
  provide: NG_VALIDATORS, 
  useExisting: forwardRef(()=>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 。

export class RepeatValidatorDirective implements Validator{
 constructor(
 @Attribute('validateEqual') public validateEqual: string,
 @Attribute('reverse') public reverse: string) { }
 private get isReverse() {
 if (!this.reverse) return false;
 return this.reverse === 'true' ? true: false;
 }
 validate(c: AbstractControl): { [key: string]: any } {
 // 控件自身值
 let self = c.value;
 // 要对比的值,也就是在 validateEqual=“ctrlname” 的那个控件的值
 let target = c.root.get(this.validateEqual);
 // 不反向查询且值不相等
 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;
 }
}

这样改造后,我们的模版文件中对于密码和确认密码的验证器如下:



完成后的验证错误提示

 

表单的提交

表单的提交比较简单,绑定表单的 ngSubmit 事件即可


但需要注意的一点是,button如果不指定类型的话,会被当做 type="submit" ,所以当按钮不是进行提交表单的话,需要显式指定 type="button" 。而且如果遇到点击提交按钮页面刷新的情况的话,意味着默认的表单提交事件引起了浏览器的刷新,这种时候需要阻止事件冒泡。

onSubmit({value, valid}, event: Event){ 
 if(valid){
 console.log(value);
 }
 event.preventDefault();
}

对于模板驱动的表单,我们就先总结到这里,下一篇文章我们会一起讨论响应式表单。

本文代码: https://github.com/wpcfan/ng-features.git

以上所述是小编给大家介绍的Angular 2+ 的表单(一)之模板驱动型表单,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!


推荐阅读
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 目录实现效果:实现环境实现方法一:基本思路主要代码JavaScript代码总结方法二主要代码总结方法三基本思路主要代码JavaScriptHTML总结实 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 禁止程序接收鼠标事件的工具_VNC Viewer for Mac(远程桌面工具)免费版
    VNCViewerforMac是一款运行在Mac平台上的远程桌面工具,vncviewermac版可以帮助您使用Mac的键盘和鼠标来控制远程计算机,操作简 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 关于我们EMQ是一家全球领先的开源物联网基础设施软件供应商,服务新产业周期的IoT&5G、边缘计算与云计算市场,交付全球领先的开源物联网消息服务器和流处理数据 ... [详细]
  • 推荐系统遇上深度学习(十七)详解推荐系统中的常用评测指标
    原创:石晓文小小挖掘机2018-06-18笔者是一个痴迷于挖掘数据中的价值的学习人,希望在平日的工作学习中,挖掘数据的价值, ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • YOLOv7基于自己的数据集从零构建模型完整训练、推理计算超详细教程
    本文介绍了关于人工智能、神经网络和深度学习的知识点,并提供了YOLOv7基于自己的数据集从零构建模型完整训练、推理计算的详细教程。文章还提到了郑州最低生活保障的话题。对于从事目标检测任务的人来说,YOLO是一个熟悉的模型。文章还提到了yolov4和yolov6的相关内容,以及选择模型的优化思路。 ... [详细]
author-avatar
尹框343437851
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有