深入了解angular中的表单(响应式和模板驱动)

本篇文章带大家了解一下angular中的表单,聊聊响应式表单与模板驱动表单,介绍一下响应式表单怎么验证表单输入,希望对大家有所帮助!

深入了解angular中的表单(响应式和模板驱动)

一、angular表单简介

Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单模板驱动表单。 两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径。【相关教程推荐:《angular教程》】

1.1 响应式表单与模板驱动表单的差异

  • 响应式表单提供对底层表单对象模型直接显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。
  • 模板驱动表单依赖模板中的指令来创建和操作底层的对象模型。它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。

响应式模板驱动
建立表单模型显式的,在组件类中创建隐式的,由指令创建
数据模型结构化和不可变的非结构化和可变的
可预测性同步异步
表单验证函数指令

1.2 建立表单模型

响应式表单和模板驱动型表单都会跟踪用户与之交互的表单输入元素和组件模型中的表单数据之间的值变更。这两种方法共享同一套底层构建块,只在如何创建管理常用表单控件实例方面有所不同。

1.3 常用表单基础类

响应式表单和模板驱动表单都建立在下列基础类之上。

  • FormControl 实例用于追踪单个表单控件的值和验证状态。
  • FormGroup 用于追踪一个表单控件组的值和状态。
  • FormArray 用于追踪表单控件数组的值和状态。
  • ControlValueAccessor 用于在 Angular 的 FormControl 实例和原生 DOM 元素之间创建一个桥梁。

二、 响应式表单

响应式表单使用显式的、不可变的方式,管理表单在特定的时间点上的状态。对表单状态的每一次变更都会返回一个新的状态,这样可以在变化时维护模型的整体性。响应式表单是围绕 Observable 流构建的,表单的输入和值都是通过这些输入值组成的流来提供的,它可以同步访问。

2.1 添加基础表单控件

使用表单控件有三个步骤。

  • 在你的应用中注册响应式表单模块。该模块声明了一些你要用在响应式表单中的指令。

  • 生成一个新的 FormControl 实例,并把它保存在组件中。

  • 在模板中注册这个 FormControl。

要使用响应式表单控件,就要从 @angular/forms 包中导入 ReactiveFormsModule,并把它添加到你的 NgModule 的 imports 数组中。

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

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }

要注册一个表单控件,就要导入 FormControl 类并创建一个 FormControl 的新实例,将其保存为类的属性。

import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-name-editor',
  templateUrl: './name-editor.component.html',
  styleUrls: ['./name-editor.component.css']
})
export class NameEditorComponent {
  name = new FormControl('');
}

可以用 FormControl 的构造函数设置初始值,这个例子中它是空字符串。通过在你的组件类中创建这些控件,你可以直接对表单控件的状态进行监听修改校验

在组件类中创建了控件之后,你还要把它和模板中的一个表单控件关联起来。修改模板,为表单控件添加 formControl 绑定,formControl 是由 ReactiveFormsModule 中的 FormControlDirective 提供的。

<label>
  Name:
  <input type="text" [formControl]="name">
</label>

2.2 显示表单控件的值

你可以用下列方式显示它的值:

  • 通过可观察对象 valueChanges,你可以在模板中使用 AsyncPipe 或在组件类中使用 subscribe() 方法来监听表单值的变化。
  • 使用 value 属性。它能让你获得当前值的一份快照。
<label>
  Name:
  <input type="text" [formControl]="name">
</label>
<p>Value: {{ name.value }}</p>
  public name = new FormControl(&#39;test&#39;);

  public testValueChange() {
    this.name.valueChanges.subscribe({
      next: value => {
        console.log("name value is: " + value);
      }
    })
  }

2.3 替换表单控件的值

响应式表单还有一些方法可以用编程的方式``修改控件的值,它让你可以灵活的修改控件的值而不需要借助用户交互。FormControl 提供了一个 setValue() 方法,它会修改这个表单控件的值,并且验证与控件结构相对应的值的结构。比如,当从后端 API 或服务接收到了表单数据时,可以通过 setValue() 方法来把原来的值替换为新的值。

updateName() {
  this.name.setValue(&#39;Nancy&#39; + new Date().getTime());
}
<p>
  <button (click)="updateName()">Update Name</button>
</p>

2.4 把表单控件分组

表单中通常会包含几个相互关联的控件。响应式表单提供了两种把多个相关控件分组到同一个输入表单中的方法。

  • 表单组定义了一个带有一组控件的表单,你可以把它们放在一起管理。表单组的基础知识将在本节中讨论。你也可以通过嵌套表单组来创建更复杂的表单。
  • 表单数组定义了一个动态表单,你可以在运行时添加和删除控件。你也可以通过嵌套表单数组来创建更复杂的表单

要将表单组添加到此组件中,请执行以下步骤。

  • 创建一个 FormGroup 实例。

  • 把这个 FormGroup 模型关联到视图。

  • 保存表单数据。

在组件类中创建一个名叫 profileForm 的属性,并设置为 FormGroup 的一个新实例。要初始化这个 FormGroup,请为构造函数提供一个由控件组成的对象,对象中的每个名字都要和表单控件的名字一一对应。

import { FormControl, FormGroup } from &#39;@angular/forms&#39;;
  profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;),
    lastName: new FormControl(&#39;&#39;),
  });
  // 可以整个获取值
  public onSubmit() {
    // TODO: Use EventEmitter with form value
    console.warn(this.profileForm.value);// {firstName: "", lastName: ""}
  }
      // 可以借助 valueChanges 整个可观察对象整个获取值
     this.profileForm.valueChanges.subscribe( {
      next: value => {
        console.log("name value is: " + JSON.stringify(value)); // dashboard.component.ts:53 name value is: {"firstName":"dddd","lastName":"bb"}
      }
    })

    // 可以通过后期单个控件单独获取值
    this.profileForm.get(&#39;firstName&#39;).valueChanges.subscribe({
      next: value => {
        console.log("First Name is: " + value); // First Name is: aa
      }

ps: 这个 FormGroup 用对象的形式提供了它的模型值,这个值来自组中每个控件的值。 FormGroup 实例拥有和 FormControl 实例相同的属性(比如 value、untouched)和方法(比如 setValue())。

这个表单组还能跟踪其中每个控件的状态及其变化,所以如果其中的某个控件的状态或值变化了,父控件也会发出一次新的状态变更或值变更事件。该控件组的模型来自它的所有成员。在定义了这个模型之后,你必须更新模板,来把该模型反映到视图中。

<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
 
  <label>
    First Name:
    <input type="text" formControlName="firstName">
  </label>

  <label>
    Last Name:
    <input type="text" formControlName="lastName">
  </label>

  <button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>

2.5 创建嵌套的表单组

表单组可以同时接受单个表单控件实例和其它表单组实例作为其子控件。这可以让复杂的表单模型更容易维护,并在逻辑上把它们分组到一起。
要制作更复杂的表单,请遵循如下步骤。

  • 创建一个嵌套的表单组

  • 板中对这个嵌套表单分组。

要在 profileForm 中创建一个嵌套组,就要把一个嵌套的 address 元素添加到此表单组的实例中。

  public profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;),
    lastName: new FormControl(&#39;&#39;),
    address: new FormGroup({
      street: new FormControl(&#39;&#39;),
      city: new FormControl(&#39;&#39;),
      state: new FormControl(&#39;&#39;),
      zip: new FormControl(&#39;&#39;)
    })
  });
    // 可以借助 valueChanges 整个可观察对象整个获取值
    this.profileForm.valueChanges.subscribe( {
      next: value => {
        console.log("name value is: " + JSON.stringify(value));// name value is: {"firstName":"","lastName":"","address":{"street":"b","city":"","state":"","zip":""}}
      }
    });

    // 可以通过后期单个控件单独获取值
    this.profileForm.get(&#39;firstName&#39;).valueChanges.subscribe({
      next: value => {
        console.log("First Name is: " + value);
      }
    });

    // 可以获取form组件某个form组的整个值
    this.profileForm.get(&#39;address&#39;).valueChanges.subscribe(({
      next: value => {
        console.log(&#39;address value is: &#39; + JSON.stringify(value));// address value is: {"street":"b","city":"","state":"","zip":""}
      }
    }));

    // 可以获取form组件某个form组的某个formcontrol实例的值
    this.profileForm.get(&#39;address&#39;).get(&#39;street&#39;).valueChanges.subscribe(({
      next: value => {
        console.log(&#39;street value is: &#39; + value);// street value is: b
      }
    }));

在修改了组件类中的模型之后,还要修改模板,来把这个 FormGroup 实例对接到它的输入元素。

      <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

        <label>
          First Name:
          <input type="text" formControlName="firstName">
        </label>

        <label>
          Last Name:
          <input type="text" formControlName="lastName">
        </label>
        <div formGroupName="address">
          <h3>Address</h3>

          <label>
            Street:
            <input type="text" formControlName="street">
          </label>

          <label>
            City:
            <input type="text" formControlName="city">
          </label>

          <label>
            State:
            <input type="text" formControlName="state">
          </label>

          <label>
            Zip Code:
            <input type="text" formControlName="zip">
          </label>
        </div>
        <button type="submit" [disabled]="!profileForm.valid">Submit</button>
      </form>

2.6 更新部分数据模型

当修改包含多个 FormGroup 实例的值时,你可能只希望更新模型中的一部分,而不是完全替换掉。

有两种更新模型值的方式:

  • 使用 setValue() 方法来为单个控件设置新值。 setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值
  • 使用 patchValue() 方法可以用对象中所定义的任何属性为表单模型进行替换。

setValue() 方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue() 在遇到那些错误时可能会默默的失败。

  public updateProfile() {
      // profileForm 模型中只有 firstName 和 street 被修改了。注意,street 是在 address 属性的对象中被修改的。这种结构是必须的,因为 patchValue() 方法要针对模型的结构进行更新。patchValue() 只会更新表单模型中所定义的那些属性。
    this.profileForm.patchValue({
      firstName: &#39;Nancy&#39; + new Date().getTime(),
      address: {
        street: &#39;123 Drew Street&#39; + new Date().getTime()
      }
    });

    // ERROR Error: Must supply a value for form control with name: &#39;lastName&#39;.
    // setValue() 方法会严格遵循表单组的结构
    this.profileForm.setValue({
      firstName: &#39;Nancy&#39; + new Date().getTime(),
      address: {
        street: &#39;123 Drew Street&#39; + new Date().getTime()
      }
    });
  }

2.7 创建动态表单

FormArray 是 FormGroup 之外的另一个选择,用于管理任意数量的匿名控件。像 FormGroup 实例一样,你也可以往 FormArray 中动态插入和移除控件,并且 FormArray 实例的值和验证状态也是根据它的子控件计算得来的。 不过,你不需要为每个控件定义一个名字作为 key,因此,如果你事先不知道子控件的数量,这就是一个很好的选择。

要定义一个动态表单,请执行以下步骤。

  • 导入 FormArray 类。

  • 定义一个 FormArray 控件。

  • 使用 getter 方法访问 FormArray 控件。

  • 在模板中显示这个表单数组

通过把一组(从零项到多项)控件定义在一个数组中来初始化一个 FormArray。为 profileForm 添加一个 aliases 属性,把它定义为 FormArray 类型。

import { FormControl, FormGroup, FormArray } from &#39;@angular/forms&#39;;

  public profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;),
    lastName: new FormControl(&#39;&#39;),
    address: new FormGroup({
      street: new FormControl(&#39;&#39;),
      city: new FormControl(&#39;&#39;),
      state: new FormControl(&#39;&#39;),
      zip: new FormControl(&#39;&#39;)
    }),
    aliases: new FormArray([
      new FormControl(&#39;1&#39;)
    ])
  });
  public aliases = (<FormArray>this.profileForm.get(&#39;aliases&#39;));

  public addAlias() {
    (<FormArray>this.profileForm.get(&#39;aliases&#39;)).push(new FormControl(&#39;1&#39;));
  }

      // 获取整个 formArray 的数据
    this.profileForm.get(&#39;aliases&#39;).valueChanges.subscribe({
      next: value => {
        console.log(&#39;aliases values is: &#39; + JSON.stringify(value)); // aliases values is: ["1","3"]
      }
    });

    // 获取 formArray 中单个 formControl 的数据
    (<FormArray>this.profileForm.get(&#39;aliases&#39;)).controls[0].valueChanges.subscribe({
      next: value => {
        console.log(&#39;aliases[0] values is: &#39; + value); // aliases[0] values is: 0
      }
    })

要想为表单模型添加 aliases,你必须把它加入到模板中供用户输入。和 FormGroupNameDirective 提供的 formGroupName 一样,FormArrayNameDirective 也使用 formArrayName 在这个 FormArray 实例和模板之间建立绑定

      <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">

        <label>
          First Name:
          <input type="text" formControlName="firstName">
        </label>

        <label>
          Last Name:
          <input type="text" formControlName="lastName">
        </label>
        <div formGroupName="address">
          <h3>Address</h3>

          <label>
            Street:
            <input type="text" formControlName="street">
          </label>

          <label>
            City:
            <input type="text" formControlName="city">
          </label>

          <label>
            State:
            <input type="text" formControlName="state">
          </label>

          <label>
            Zip Code:
            <input type="text" formControlName="zip">
          </label>
        </div>
        <div formArrayName="aliases">
          <h3>Aliases</h3> <button (click)="addAlias()">Add Alias</button>

          <div *ngFor="let alias of aliases.controls; let i=index">
            <!-- The repeated alias template -->
            <label>
              Alias:
              <input type="text" [formControlName]="i">
            </label>
          </div>
        </div>
      </form>

2.8 响应式表单 API 汇总

说明
AbstractControl所有三种表单控件类(FormControl、FormGroup 和 FormArray)的抽象基类。它提供了一些公共的行为和属性。
FormControl管理单体表单控件的值和有效性状态。它对应于 HTML 的表单控件,比如 或 。
FormGroup管理一组 AbstractControl 实例的值和有效性状态。该组的属性中包括了它的子控件。组件中的顶层表单就是 FormGroup。
FormArray管理一些 AbstractControl 实例数组的值和有效性状态。
FormBuilder一个可注入的服务,提供一些用于提供创建控件实例的工厂方法。

三、模板驱动表单

在模板驱动表单中,表单模型是隐式的,而不是显式的。指令 NgModel 为指定的表单元素创建并管理一个 FormControl 实例。
下面的组件使用模板驱动表单为单个控件实现了同样的输入字段。

import { Component } from &#39;@angular/core&#39;;

@Component({
  selector: &#39;app-template-favorite-color&#39;,
  template: `
    Favorite Color: <input type="text" [(ngModel)]="favoriteColor">
  `
})
export class FavoriteColorComponent {
  favoriteColor = &#39;&#39;;
}

四、响应式表单验证表单输入

在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。

4.1 验证器(Validator)函数

验证器函数可以是同步函数,也可以是异步函数。

  • 同步验证器:这些同步函数接受一个控件实例,然后返回一组验证错误或 null。你可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。
  • 异步验证器 :这些异步函数接受一个控件实例并返回一个 Promise 或 Observable,它稍后会发出一组验证错误或 null。在实例化 FormControl 时,可以把它们作为第三个参数传入。

出于性能方面的考虑,只有在所有同步验证器都通过之后,Angular 才会运行异步验证器。当每一个异步验证器都执行完之后,才会设置这些验证错误。

4.2 内置验证器函数

在模板驱动表单中用作属性的那些内置验证器,比如 required 和 minlength,也都可以作为 Validators 类中的函数使用

 public profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;, [
      Validators.required
    ]),
  });

    this.profileForm.get(&#39;firstName&#39;).valueChanges.subscribe({
      next: value => {
        console.log("First Name is: " + value);
        console.log(this.profileForm.get(&#39;firstName&#39;).errors);// { required: true } | null
      }
    });
      <form [formGroup]="profileForm">

        <label>
          First Name:
          <input type="text" formControlName="firstName">
          <div *ngIf="firstName.errors?.required">
            Name is required.
          </div>
        </label>
    </form>

4.3 定义自定义验证器

内置的验证器并不是总能精确匹配应用中的用例,因此有时你需要创建一个自定义验证器。

  public profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;, [
      Validators.required,
      this.forbiddenNameValidator(/bob/i)
    ])
  });

  public forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} | null => {
      const forbidden = nameRe.test(control.value);
      return forbidden ? {forbiddenName: {value: control.value}} : null;
    };
  }

  get firstName() { return this.profileForm.get(&#39;firstName&#39;); }



      this.profileForm.get(&#39;firstName&#39;).valueChanges.subscribe({
      next: value => {
        console.log("First Name is: " + value); // First Name is: bob
        console.log(JSON.stringify(this.profileForm.get(&#39;firstName&#39;).errors));// {"forbiddenName":{"value":"bob"}} | null
      }
    });

4.4 跨字段交叉验证

跨字段交叉验证器是一种自定义验证器,可以对表单中不同字段的值进行比较,并针对它们的组合进行接受或拒绝。

下列交叉验证的例子说明了如何进行如下操作:

  • 根据两个兄弟控件的值验证响应式表单或模板驱动表单的输入,
  • 当用户与表单交互过,且验证失败后,就会显示描述性的错误信息

要想在单个自定义验证器中计算这两个控件,你就必须在它们共同的祖先控件中执行验证: FormGroup。你可以在 FormGroup 中查询它的子控件,从而让你能比较它们的值。要想给 FormGroup 添加验证器,就要在创建时把一个新的验证器传给它的第二个参数。

    this.profileForm.valueChanges.subscribe( {
      next: value => {
        console.log(JSON.stringify(this.profileForm.errors));// {"identityRevealed":true} | null
      }
    });

  public profileForm = new FormGroup({
    firstName: new FormControl(&#39;&#39;, [
      Validators.required,
    ]),
    lastName: new FormControl(&#39;&#39;),
  }, { validators: this.identityRevealedValidator});

  public identityRevealedValidator(control: FormGroup): ValidationErrors | null{
    const firstName = control.get(&#39;firstName&#39;);
    const lastName = control.get(&#39;lastName&#39;);
    return firstName && lastName && firstName.value === lastName.value ? { identityRevealed: true } : null;
  };

4.5 创建异步验证器

异步验证器实现了 AsyncValidatorFnAsyncValidator 接口。它们与其同步版本非常相似,但有以下不同之处。

  • validate() 函数必须返回一个 Promise 或可观察对象
  • 返回的可观察对象必须是有尽的,这意味着它必须在某个时刻完成(complete)。要把无尽的可观察对象转换成有尽的,可以在管道中加入过滤操作符,比如 first、last、take 或 takeUntil。

异步验证在同步验证完成后才会发生,并且只有在同步验证成功时才会执行。如果更基本的验证方法已经发现了无效输入,那么这种检查顺序就可以让表单避免使用昂贵的异步验证流程(例如 HTTP 请求)。

4.6 触发某个formControlName

    let formControl = this.profileForm.get(&#39;firstName&#39;);
    formControl.updateValueAndValidity();

更多编程相关知识,请访问:编程视频!!

以上就是深入了解angular中的表单(响应式和模板驱动)的详细内容,更多请关注其它相关文章!