热门标签 | 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+ 的表单(一)之模板驱动型表单,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对网站的支持!


推荐阅读
  • 实践指南:使用Express、Create React App与MongoDB搭建React开发环境
    本文详细介绍了如何利用Express、Create React App和MongoDB构建一个高效的React应用开发环境,旨在为开发者提供一套完整的解决方案,包括环境搭建、数据模拟及前后端交互。 ... [详细]
  • Python3爬虫入门:pyspider的基本使用[python爬虫入门]
    Python学习网有大量免费的Python入门教程,欢迎大家来学习。本文主要通过爬取去哪儿网的旅游攻略来给大家介绍pyspid ... [详细]
  • JavaScript 跨域解决方案详解
    本文详细介绍了JavaScript在不同域之间进行数据传输或通信的技术,包括使用JSONP、修改document.domain、利用window.name以及HTML5的postMessage方法等跨域解决方案。 ... [详细]
  • 利用Node.js实现PSD文件的高效切图
    本文介绍了如何通过Node.js及其psd2json模块,快速实现PSD文件的自动化切图过程,以适应项目中频繁的界面更新需求。此方法不仅提高了工作效率,还简化了从设计稿到实际应用的转换流程。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • Requests库的基本使用方法
    本文介绍了Python中Requests库的基础用法,包括如何安装、GET和POST请求的实现、如何处理Cookies和Headers,以及如何解析JSON响应。相比urllib库,Requests库提供了更为简洁高效的接口来处理HTTP请求。 ... [详细]
  • 在现代前端开发中,组件化已成为不可或缺的技术,尤其在 React 和 Vue 生态中。然而,组件的管理和测试一直是开发者面临的挑战。本文将介绍如何使用 Storybook 来简化这一过程,提高开发效率。 ... [详细]
  • JavaScript 实现图片文件转Base64编码的方法
    本文详细介绍了如何使用JavaScript将用户通过文件输入控件选择的图片文件转换为Base64编码字符串,适用于Web前端开发中图片上传前的预处理。 ... [详细]
  • 本文详细介绍了如何在 Ubuntu 14.04 系统上搭建仅使用 CPU 的 Caffe 深度学习框架,包括环境准备、依赖安装及编译过程。 ... [详细]
  • 搭建个人博客:WordPress安装详解
    计划建立个人博客来分享生活与工作的见解和经验,选择WordPress是因为它专为博客设计,功能强大且易于使用。 ... [详细]
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • 本文探讨了如何通过优化 DOM 操作来提升 JavaScript 的性能,包括使用 `createElement` 函数、动画元素、理解重绘事件及处理鼠标滚动事件等关键主题。 ... [详细]
  • 调试利器SSH隧道
    在开发微信公众号或小程序的时候,由于微信平台规则的限制,部分接口需要通过线上域名才能正常访问。但我们一般都会在本地开发,因为这能快速的看到 ... [详细]
  • Web动态服务器Python基本实现
    Web动态服务器Python基本实现 ... [详细]
  • 精选10款Python框架助力并行与分布式机器学习
    随着神经网络模型的不断深化和复杂化,训练这些模型变得愈发具有挑战性,不仅需要处理大量的权重,还必须克服内存限制等问题。本文将介绍10款优秀的Python框架,帮助开发者高效地实现分布式和并行化的深度学习模型训练。 ... [详细]
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社区 版权所有