angular中的表单

angular中的表单

使用angular挺长一段时间了,在关于表单的应用方面一直都在使用ng-zorro封装好的表单,至于他的基本概念以及相应的拓展都没有去详细了解,今天趁着空闲研究了一下form表单究竟是个什么东西,以及如何构建一个符合我们需求的自定义校验。
参考:《Angular权威教程》第五章 Angular中的表单,Angular官网

1.表单——既重要又复杂

在web应用中,表单是一个重要的部分。虽然我们常从点击链接或移动鼠标中得到事件的通知,但大多数“富数据“都是通过表单从用户那里获取的。

表面看起来表单只是简单的创建一个input标签,用户填入数据然后提交即可。但事实证明。表单最终可能是非常复杂的。原因如下:

  • 表单输入意味着需要在页面和服务器端同时修改这份数据;
  • 修改的内容通常要在页面的其他地方反映出来;
  • 用户的输入可能存在很多问题,所以需要验证输入的内容
  • 用户界面需要清晰的显示出可能出现的预期效果和错位信息;
  • 字段之间的依赖可能存在复杂的业务逻辑

值得庆幸的是,Angular已经给出了上述所有问题的解决方案。

  • 表单控件(Form Control)封装了表单中的输入,并提供了一些可供操纵的对象
  • 验证器(validator)让我们能以自己喜欢的任何方式验证表单输入
  • 观察者(observer)让我们能够监听表单的变化,并作出相应的回应

2.FormControl和FormGroup

FormControl和FormGroup是angular中两个最基本的表单对象

FormControl代表单一的输入字段,它是Angular表单中最小单员。FormControl封装了这些字段的值和状态,比如是否有效、是否脏(被修改过)或是否有错误等。

ngOnInit() {
    // 创建一个新的FormControl对象并将其值设置成“nate”
    const nameControl = new FormControl('nate');
    const name = nameControl.value; // nate
    console.log('nameControl', nameControl);
    console.log('value', nameControl.value);
    console.log('valid ', nameControl.valid); // true
    console.log('dirty', nameControl.dirty); // false
    console.log('errors', nameControl.errors); 
  }

我们通过打印FormControl对象会看到他还有很多属性,我们这里只讲这几个常用的属性,至于其他属性你们可以查看官网Angular-FormControl前去了解(这里可以注意一下errors的返回类型,后面自定义校验会用到)

FormGroup大多数表单都拥有不止一个字段,因此我们需要某种方式来管理多个FormControl。假设我们要检查表单的有效性。如果要遍历这个FormControl数组并检查每一个FormControl是否有效,必然相当繁琐;而FormGroup则可以为一组FormControl提供总包接口(wrapper interface),来解决这种问题。

ngOnInit() {
    const personInfo = new FormGroup({ 
    firstName: new FormControl("Nate"), 
    lastName: new FormControl("Murray"), 
    zip: new FormControl("90210") 
}) 
    console.log('personInfo', personInfo);
    console.log('value', personInfo.value);
    console.log('valid ', personInfo.valid); // true
    console.log('dirty', personInfo.dirty); // false
    console.log('errors', personInfo.errors); 
  }

FormGroup 和 FormControl 都继承自同一个祖先AbstractControl(这是FormControlFormGroupFormArray的基类)。这意味检查 personInfo 的状态或值就像检查单个FormControl那么容易。

注意,当我们试图从FormGroup中获取value时,会收到一个“键值对”结构的对象。它能让我们从表单中一次性获取全部的值而无需逐一遍历FormControl,使用起来相当顺手。

3.创建表单

假设我们创建一个商品名称的表单,这个表单非常简单只有一个带(lable)的输入框和一个提交按钮。

 <div>
      <h2>基础表单:商品名称</h2>
      <form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
        <div class="sku">
          <label for="skuInput">商品名称:</label>
          <input
            type="text"
            id="skuInput"
            placeholder="商品名称"
            name="sku"
            ngModel
          />
        </div>
        <button>提交</button>
      </form>
    </div>

加载FormsModule

为了使用这个新的表单库,先要确保我们的NgModule中导入了这个表单库。

Angular中有两种使用表单的方式,我们这次都会展开讨论:使用FormsModule以及使用ReactiveFormsModule。既然都要用到,那么这个模块就同时导入它们。因此需要在引用启动程序app.ts中这样写:

import { 
  FormsModule, 
  ReactiveFormsModule 
} from '@angular/forms'; 

// farther down... 

@NgModule({ 
  declarations: [ 
    FormsDemoApp, 
    DemoFormSku, 
    // ... our declarations here 
  ], 
  imports: [ 
    BrowserModule, 
    FormsModule,         // <-- add this 
    ReactiveFormsModule  // <-- and this 
  ], 
  bootstrap: [ FormsDemoApp ] 
}) 
class FormsDemoAppModule {}

FormsModule为我们提供了一些模板驱动的指令,例如:

ReactiveFormsModule则提供了下列指令:

我们导入了FormsModule,因此可以在视图中使用NgForm了。记住,当这些指令在视图中可用时,它就会被附加到任何能匹配其selector的节点上。

NgForm做了一件便利但隐晦的工作:它的选择器包含form 标签(而不用显式添加ngForm属性)。这意味着当我们导入FormsModule时候,NgForm就会被自动附加到视图中所有的标签上。

NgForm给我们提供了两个重要的功能:

(1) 一个名叫ngForm的FormGroup对象;
(2) 一个输出事件(ngSubmit)。

我们在视图的标签中同时用到了它们两个。

 <form #f="ngForm" 
   (ngSubmit)="onSubmit(f.value)" 

首先,我们使用了#f=“ngForm”。#v=thing 语法的意思是,我们希望在当前视图中创建一个局部变量。

这里我们为视图中的ngForm创建了一个别名,并绑定到变量#f。这个ngForm是由NgForm指令导出的。

ngForm的类型的对象是FormGroup类型的。这意味着我们可以在视图中把变量 f 当作FormGroup使用,而这也正是我们在输出事件(ngSubmit)中的使用方法。

我们在表单中绑定ngSubmit事件的语法是:(ngSubmit)=“onSubmit(f.value)”。

  • (ngSubmit):来自NgForm指令。
  • onSubmit():将会在我们的组件类中进行定义。
  • f.value:f就是我们前面提到的FormGroup,而.value会以键值对的形式返回FormGroup中所有控件的值。

总结起来,这行代码的意思是:“当我提交表单时,将会以该表单的值作为参数,调用组件实例上的onSubmit方法。

NgModel

NgModel指令指定的selector是ngModel。这意味着我们可以通过添加这个属性把它附加到
input标签上:ngModel=“whatever”。在这里我们指定了一个不带属性值的ngModel。

有两种不同的方法能在模板中指定ngModel,这里是第一种。当使用不带属性值的ngModel 时,我们是要指定:
(1) 单向数据绑定;
(2) 希望在表单中创建一个名为name的FormControl(这个name来自input标签上的name属性)。

NgModel会创建一个新的FormControl对象,把它自动添加到父FormGroup上(这里也就是form表单对象),并把这个FormControl对象绑定到一个DOM上。也就是说,它会在视图中的input标签和FormControl对象之间建立关联。这种关联是通过name属性建立的,在本例中是"name"。

注意:

  1. NgModel(Pascal命名法),指的是类和供代码中引用的对象。
  2. ngModel(首字母小写驼峰命名法),来自指令的选择器 selector,并且只会被用在DOM/模板中。

NgModel和FormControl并不是同一个,NgModel是用在视图中的指令, 而FormControl则用来表示表单中的数据和验证规则。

4.FormBuilder(响应表单)

通过上面的学习,我们知道使用ngForm和那个Control构建FormControl和FormGroup很方便,但是这却无法为我们提供更多的定制化选项。下面我们就一起来学习一下FormBuilder。

FormBuilder是一个名副其实的表单构建助手。(我们可以把他看作一个“工厂”对象)。

让我们在先前的例子中添加一个FormBuilder,看看:

  • 如何在组件定义类中使用FormGroup;
  • 如何在视图表单中使用自定义的FormGroup。

我们将使用formGroup和formControl指令来构建这个组件,这意味着我们需要导入相应的类。

import { 
  FormBuilder, 
  FormGroup 
} from '@angular/forms'; 

使用FormBuilder

通过在组件类上声明带参数的constructor,我们注入了一个FormBuilder。

export class DemoFormSkuBuilder { 
  myForm: FormGroup; 

  constructor(fb: FormBuilder) { 
    this.myForm = fb.group({ 
      'sku': ['ABC123'] 
    }); 
  } 

  onSubmit(value: string): void { 
    console.log('you submitted value: ', value); 
  } 
}

Angular将会注入一个从FormBuilder类创建的对象实例,并把它赋值给fb变量(来自构造函数)。

我们将会使用FormBuilder中的两个主要函数:

  • control,用于创建一个新的FormControl;
  • group,用于创建一个新的FormGroup。

myForm是FormGroup类型。我们通过调用fb.group()来创建FormGroup。.group方法的参数是代表组内各个FormControl的键值对。

在这里,我们设置了一个名为sku的控件,其值为[“ABC123”]——意思是控件的默认值为"ABC123"。

我们需要将它绑定到表单元素上。

在视图中使用myForm

我们希望修改标签,让它使用myForm变量。回忆一下,在上一节中我们提到过,当导入FormsModule时,ngForm就会自动起作用。还提到过ngForm会自动创建它自己的FormGroup。但在这里我们不希望使用外部的FormGroup,而是使用FormBuilder创建的这个myForm实例变量。那该怎么做呢?

Angular提供了另一个指令,能让我们使用现有的FormGroup。它叫作formGroup,可以这样使用。

<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [formGroup]="myForm" 

这里我们告诉Angular,想用myForm作为这个表单的FormGroup。

注意:
当使用FormsModule时,NgForm会自动应用于元素上。但其实有一个例外:NgForm不会应用到带formGroup属性的节点上。

这是因为NgForm的selector是:

form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]

这意味着你还可以使用 ngNoForm 属性产生一个不带NgForm的表单。

将FormControl绑定到input标签上。记住,ngModel会创建一个新的FormControl对象,并附加到父FormGroup中。但在这个例子中,我们已经用FormBuilder创建了自己的FormControl。
要将现有的FormControl绑定到input上,可以用 formControl

<label for="skuInput">SKU</label> 
<input type="text" 
   id="skuInput" 
   placeholder="SKU" 
   [formControl]="myForm.controls['sku']">

我 们 将 input标 签 上 的 formControl指 令 指 向 了 myForm.controls上 现 有 的FormControl控件sku。

需要记住以下两点。

  1. 如果想隐式创建新的FormGroup和FormControl,使用:
  • ngForm
  • ngModel
  1. 如果要绑定一个现有的FormGroup和FormControl,使用:
  • formGroup
  • formControl

5.添加验证

用户输入的数据格式并不总是正确的。如果有人输入错误的数据格式,我们希望给他反馈。并阻止他提交表单。因此,我们要用到验证器。

验证器由validators模块提供。Validators.required是最简单的验证,表明指定的字段是必填项,否则就认为FormControl是无效的。

(1) 为FormControl对象指定一个验证器;
(2) 在视图中检查验证器的状态,并据此采取行动。

要 为 FormControl对 象 分 配 一 个 验 证 器 , 我 们 可 以 直 接 把 它 作 为 第 二 个 参 数 传 给FormControl的构造函数。

const control = new FormControl('name', Validators.required);

像这个例子中一样通过如下语法使用FormBuilder:

  constructor(fb: FormBuilder) { 
    this.myForm = fb.group({ 
      'name': ['', Validators.required] 
    }); 
    this.name = this.myForm.controls['name']; 
  } 

(1)我们可以显示地把name这个FormControl赋值给类的实例变量。虽然这有点啰嗦,但是便于我们在视图中访问这个FormControl。

(2)我们也可以在myForm中查找name这个FormCOntrol。这样能简化组件类中的工作,但在视图中稍微麻烦些。

为了说明两者之间的差异,我们来看两个例子。

把商品设置成实列变量并显示

在视图中,处理单个FormControls的最灵活的方式是将每个FormControl都定义在组件类上。把商品名称和料号定义在组件类上的代码如下所示。

export class NonInWarehouseComponent implements OnInit {
  myForm: FormGroup;
  name: AbstractControl;
  constructor(fb: FormBuilder) {
    this.myForm = fb.group({
      name: ['牛奶', [Validators.required, Validators.pattern('^123')]],
      code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]],
    });
    this.name = this.myForm.controls.name;
  }

  ngOnInit() {
    const nameControl = new FormControl('nate');
    console.log('nameControl', nameControl);
  }
  onSubmit(a: any) {
    console.log('a', a);
  }
}

注意:

(1)我们在类的顶部设置name:AbstractControl;

(2)我们把用FormBuilder创建的myForm赋值给this.name变量。

这意味着我们可以在组件视图中到处引用name。不过这样做有一个缺点:我们不得不为表单中的每个字段定义一个实体变量。对大型表单而言,这会使得相当啰嗦。

现在我们的name可以得到验证了,我们要以四种不同的方式把它用在视图中:

(1)检查整个表单的有效性并显示一条错误信息;

(2)检查单个字段的有效性并显示一条错误信息;

(3)检查单个字段的有效性,当字段无效时将字段显示为红色;

(4)检查单个字段在特定规则下的有效性并显示一条错误信息。

  1. 表单信息
    我们可以通过myForm.valid来检查整个表单的有效性。

    myForm是一个FormGroup;只有当里面所有的FormGroup都有效时,这个FormGroup才有效。

  2. 字段信息
    当字段的FormControl无效时,我们也可以为该字段显示一条错误信息。

  3. 特定验证
    可能有很多原因导致一个表单字段无效。对于失败的验证,我们通常希望根据不同的原因显示不同的信息。我们可以用hassError方法来检查特定的验证失败。
    注意,FormControl和FormGroup都定义了hasError方法。这意味着我们可以给他传入第二个参数path来在FormGroup中查询特定的字段。比如:

完整代码

template:`<div>
      <h2>商品表单:商品名称</h2>
      <form [formGroup]="myForm" (ngSubmit)="onSubmit(myForm)">
        <div>
          <label for="nameInput">商品名称:</label>
          <input
            type="text"
            id="nameInput"
            placeholder="请输入名称"
            [formControl]="myForm.controls['name']"
          />
          <div style="color:red" *ngIf="!name.valid">
            名称无效
          </div>
          <div style="color:red" *ngIf="name.hasError('textinvalid')">
            名称不是以“123”开头
          </div>
          <div *ngIf="name.dirty">
            数据已变动
          </div>
        </div>
        <div>
          <label for="codeInput">商品料号:</label>
          <input
            type="text"
            id="codeInput"
            placeholder="请输入料号"
            [formControl]="myForm.controls['code']"
          />
          <div
            style="color:red"
            *ngIf="myForm.controls.code.hasError('required')"
          >
            该项必填
          </div>
          <div
            style="color:red"
            *ngIf="myForm.controls.code.hasError('pattern')"
          >
            只可输入数字和英文
          </div>
        </div>
        <div style="color:green" *ngIf="myForm.isvalid">
          表单无效
        </div>
        <div style="color:green" *ngIf="myForm.valid">
          表单有效
        </div>
        <button type="submit">提交</button>
      </form>
    </div>`
export class NonInWarehouseComponent implements OnInit {
  myForm: FormGroup;
  name: AbstractControl;
  constructor(fb: FormBuilder) {
    this.myForm = fb.group({
      name: ['牛奶', Validators.compose([Validators.required, textValidator])],
      code: ['', [Validators.required, Validators.pattern('^[A-Za-z0-9]*$')]],
    });
    this.name = this.myForm.controls.name;
  }

  ngOnInit() {
    const nameControl = new FormControl('nate');
    console.log('nameControl', nameControl);
  }
  onSubmit(a: any) {
    console.log('a', a);
  }
}

大多数情况下,我们并不希望为每一个AbstractControl控件都创建一个实例变量。在没有实例变量的情况下,我们可以通过myForm.controls属性

6.自定义验证器

我们经常需要根据需求写一些自定义的验证器,下面我们来看看如何实现。

要明白如何实现自己的验证器,不妨看看angular源代码中是如何实现Validators.required的:

export class Validators {
static required(control: AbstractControl): ValidationErrors | null;
}
export declare type ValidationErrors = {
    [key: string]: any;
};

一个验证器:

  • 接收一个AbstractControl对象作为输入;
  • 当验证器失败时,会返回一个String Map<string,any>对象,他的键是”错误代码“,它的值是true。

编写验证器

假设我们的name有特殊的验证需求,比如name必须以123作为开始。我们写的验证器是这样的:

function textValidator(
  controls: FormControl // 因为FormControl继承于AbstractControl所以也可以写成FormControl对象
): {
  [s: string]: boolean;
} {
  if (!controls.value.match(/^123/)) {
    return { textinvalid: true };
  }
}

当输入值(控件的值control.value)不是以123作为开始时,验证器会返回错误代码invalidSku。

给FormControl分配验证器

现在要为Form Control添加验证,但是一个小问题:name已经有一个验证器了,怎样才能在同一个字段上添加多个验证器。

我们可以用Validators.compose来实现。

Validators.compose([Validators.required, textValidator])
// 不用compose时
[Validators.required, textValidator]

注意:保留compose是为了向以前历史版本进行兼容,现在不用compose也可实现。

Validators.compose把两个验证器包装在一起,我们可以将其赋值给FormControl。只有当两个验证器都合法时,FormControl才是合法的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值