Angular 学习笔记 (八) - 表单Forms

用表单处理用户输入是许多常见应用的基础功能。 应用通过表单来让用户登录、修改个人档案、输入敏感信息以及执行各种数据输入任务。

Angular 提供了两种不同的方法来通过表单处理用户输入:响应式表单(Reactive Form)模板驱动表单(Template-driven Form)。 两者都从视图中捕获用户输入事件、验证用户输入、创建表单模型、修改数据模型,并提供跟踪这些更改的途径。

本指南提供的信息可以帮你确定哪种方式最适合你的情况。它介绍了这两种方法所用的公共构造块,还总结了两种方式之间的关键区别,并在建立、数据流和测试等不同的情境下展示了这些差异。

参考:https://angular.io/guide/forms-overview

  • 响应式表单提供对底层表单对象模型直接、显式的访问。它们与模板驱动表单相比,更加健壮:它们的可扩展性、可复用性和可测试性都更高。如果表单是你的应用程序的关键部分,或者你已经在使用响应式表单来构建应用,那就使用响应式表单。

  • 模板驱动表单依赖模板中的指令来创建和操作底层的对象模型。它们对于向应用添加一个简单的表单非常有用,比如电子邮件列表注册表单。它们很容易添加到应用中,但在扩展性方面不如响应式表单。如果你有可以只在模板中管理的非常基本的表单需求和逻辑,那么模板驱动表单就很合适。

 

模板驱动表单

 

使用TD form,需要首先在app.module.ts中引入FromsModule

 1 import { BrowserModule } from '@angular/platform-browser';
 2 import { NgModule } from '@angular/core';
 3 import { FormsModule } from '@angular/forms';
 4 
 5 import { AppComponent } from './app.component';
 6 
 7 @NgModule({
 8   declarations: [
 9     AppComponent
10   ],
11   imports: [
12     BrowserModule,
13     FormsModule
14   ],
15   providers: [],
16   bootstrap: [AppComponent]
17 })
18 export class AppModule { }

对于Form,在html template中的使用:

<form (ngSubmit)="onSubmit()" #f="ngForm">
  <div class="form-group">
    <div class="form-group">
       <label for="username">Username</label>
       <input 
          type="text" 
          id="username" 
          class="form-control" 
          ngModel 
          name="username">
     </div>
  </div>
</form>

在ts代码中用ViewChild引入表单对象:

@ViewChild('f') signupForm:NgForm;

数据流与双向绑定

在模板驱动表单中,每一个表单元素都是和一个负责管理内部表单模型的指令关联起来的。

这个视图到模型的图表展示了当input输入字段的值发生变化时,数据流是如何从视图开始经过下列步骤进行流动的。

  1. 最终用户在输入框元素中敲 "Blue"。

  2. 该输入框元素会发出一个 "input" 事件,带着值 "Blue"。

  3. 附着在该输入框上的控件值访问器会触发 FormControl 实例上的 setValue() 方法。

  4. FormControl 实例通过 valueChanges 这个可观察对象发出新值。

  5. valueChanges 的任何订阅者都会收到新值。

  6. 控件值访问器 ControlValueAccessory 还会调用 NgModel.viewToModelUpdate() 方法,它会发出一个 ngModelChange 事件。

  7. 由于该组件模板双向数据绑定到了 favoriteColor,组件中的 favoriteColor 属性就会修改为 ngModelChange 事件所发出的值("Blue")。

这个模型到视图的示意图展示了当 favoriteColor 从蓝变到红时,数据是如何经过如下步骤从模型流动到视图的。

  1. 组件中修改了 favoriteColor 的值。

  2. 变更检测开始。

  3. 在变更检测期间,由于这些输入框之一的值发生了变化,Angular 就会调用 NgModel 指令上的 ngOnChanges 生命周期钩子。

  4. ngOnChanges() 方法会把一个异步任务排入队列,以设置内部 FormControl 实例的值。

  5. 变更检测完成。

  6. 在下一个检测周期,用来为 FormControl 实例赋值的任务就会执行。

  7. FormControl 实例通过可观察对象 valueChanges 发出最新值。

  8. valueChanges 的任何订阅者都会收到这个新值。

  9. 控件值访问器 ControlValueAccessor 会使用 favoriteColor 的最新值来修改表单的输入框元素。

 

 

输入验证

为了往模板驱动表单中添加验证机制,你要添加一些验证属性,就像原生的 HTML 表单验证器。 Angular 会用指令来匹配这些具有验证功能的指令。

每当表单控件中的值发生变化时,Angular 就会进行验证,并生成一个验证错误的列表(对应着 INVALID 状态)或者 null(对应着 VALID 状态)。

你可以通过把 ngModel 导出成局部模板变量来查看该控件的状态。 比如下面这个例子就把 NgModel 导出成了一个名叫 name 的变量:

<input id="name" name="name" class="form-control"
      required minlength="4" appForbiddenName="bob"
      [(ngModel)]="hero.name" #name="ngModel" >

<div *ngIf="name.invalid && (name.dirty || name.touched)"
    class="alert alert-danger">

  <div *ngIf="name.errors.required">
    Name is required.
  </div>
  <div *ngIf="name.errors.minlength">
    Name must be at least 4 characters long.
  </div>
  <div *ngIf="name.errors.forbiddenName">
    Name cannot be Bob.
  </div>

</div>

例子

通过TD Form实现一个用户注册的页面,需要用户输入一系列信息并添加了一些规则进行验证,页面如下:

 

 

 HTML template:

 1 <div class="container">
 2   <div class="row">
 3     <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
 4       <form (ngSubmit)="onSubmit()" #f="ngForm">
 5         <div id="user-data" 
 6           ngModelGroup="userData"
 7           #userData="ngModelGroup">
 8           <div class="form-group">
 9             <label for="username">Username</label>
10             <input 
11               type="text" 
12               id="username" 
13               class="form-control" 
14               ngModel 
15               name="username"
16               required>
17           </div>
18           <button 
19             class="btn btn-default"
20             type="button" 
21             (click)="suggestUserName()">Suggest an Username</button>
22           <div class="form-group">
23             <label for="email">Mail</label>
24             <input 
25               type="email"
26               id="email" 
27               class="form-control"
28               ngModel
29               name="email"
30               required
31               email
32               #email="ngModel">
33               <span class="help-block" *ngIf="!email.valid && email.touched">Please enter a valid email!</span>
34           </div>
35         </div>
36         <p *ngIf="!userData.valid && userData.touched">User Data is invalid!</p>
37         <div class="form-group">
38           <label for="secret">Secret Questions</label>
39           <select 
40             id="secret"
41             class="form-control"
42             [ngModel]="defaultQuestion"
43             name="secret">
44             <option value="pet">Your first Pet?</option>
45             <option value="teacher">Your first teacher?</option>
46           </select>
47         </div>
48         <div class="form-group">
49           <textarea 
50             name="questionAnswer"
51             class="form-control"
52             rows="4"
53             [(ngModel)]="answer">
54           </textarea>
55         </div>
56         <div class="radio" *ngFor="let gender of genders">
57           <label>
58             <input 
59               type="radio"
60               name="gender"
61               ngModel
62               [value]="gender"
63               required>
64             {{ gender }}
65           </label>
66         </div>
67         <button 
68           class="btn btn-primary"
69           type="submit"
70           [disabled]="!f.valid">Submit</button>
71       </form>
72     </div>
73   </div>
74   <hr>
75   <div class="row">
76     <div class="col-xs-12">
77       <h3>Your Data</h3>
78       <p>Username:{{user.username}}</p>
79       <p>Mail:{{user.email}}</p>
80       <p>Sercret Question:{{user.secretQuestion}}</p>
81       <p>Answer:{{user.answer}}</p>
82       <p>Gender:{{user.gender}}</p>
83     </div>
84   </div>
85 </div>

app.component.ts

 1 import { Component, ViewChild } from '@angular/core';
 2 import { NgForm } from '@angular/forms';
 3 
 4 @Component({
 5   selector: 'app-root',
 6   templateUrl: './app.component.html',
 7   styleUrls: ['./app.component.css']
 8 })
 9 export class AppComponent {
10   @ViewChild('f') signupForm:NgForm;
11   defaultQuestion = 'pet';
12   answer = '';
13   genders = ['male', 'female'];
14 
15   user = {
16     username: '',
17     email: '',
18     secretQuestion: '',
19     answer: '',
20     gender: ''
21   };
22 
23   suggestUserName() {
24     const suggestedName = 'Superuser';
25     this.signupForm.form.patchValue({
26       userData: {
27         username: suggestedName
28       }
29     });
30   }
31 
32   onSubmit() {
33     this.user.username = this.signupForm.value.userData.username;
34     this.user.email = this.signupForm.value.userData.email;
35     this.user.secretQuestion = this.signupForm.value.secret;
36     this.user.answer = this.signupForm.value.questionAnswer;
37     this.user.gender = this.signupForm.value.gender;
38     this.signupForm.reset();
39   }
40 }

 

响应式表单

使用ReactiveForm,需要在app.module.ts中引入:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

在模板中,用 [formGroup]包装对象:

<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
  ..
</form>

在ts中,signupForm作为AppComponent的一个对象,类型是FormGroup

数据流与双向绑定

在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl 实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。

这个视图到模型的示意图展示了当输入字段的值发生变化时数据是如何从视图开始,经过下列步骤进行流动的。

  1. 最终用户在输入框元素中键入了一个值,这里是 "Blue"。

  2. 这个输入框元素会发出一个带有最新值的 "input" 事件。

  3. 这个控件值访问器 ControlValueAccessor 会监听表单输入框元素上的事件,并立即把新值传给 FormControl 实例。

  4. FormControl 实例会通过 valueChanges 这个可观察对象发出这个新值。

  5. valueChanges 的任何一个订阅者都会收到这个新值。

 

 

这个模型到视图的示意图体现了程序中对模型的修改是如何通过下列步骤传播到视图中的。

  1. favoriteColorControl.setValue() 方法被调用,它会更新这个 FormControl 的值。

  2. FormControl 实例会通过 valueChanges 这个可观察对象发出新值。

  3. valueChanges 的任何订阅者都会收到这个新值。

  4. 该表单输入框元素上的控件值访问器会把控件更新为这个新值。

 

 

输入验证

在响应式表单中,事实之源是其组件类。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。

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

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

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

例子

实现和上例相似的页面,这里用Validators为用户名和邮箱的输入多设置了一些验证规则。

HTML template:

 1 <div class="container">
 2   <div class="row">
 3     <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
 4       <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
 5         <div formGroupName="userData">
 6           <div class="form-group">
 7             <label for="username">Username</label>
 8             <input
 9               type="text"
10               id="username"
11               formControlName="username"
12               class="form-control">
13             <span 
14               *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched"
15               class="help-block">Please enter a valid username!
16               <span
17                 *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
18                 This name has been registered.
19               </span>
20             </span>
21           </div>
22           <div class="form-group">
23             <label for="email">email</label>
24             <input
25               type="text"
26               id="email"
27               formControlName="email"
28               class="form-control">
29             <span 
30               *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched"
31               class="help-block">Please enter a valid email!</span>
32           </div>
33         </div>
34         <div class="radio" *ngFor="let gender of genders">
35           <label>
36             <input
37               type="radio"
38               formControlName="gender"
39               [value]="gender">{{ gender }}
40           </label>
41         </div>
42         <div formArrayName="hobbies">
43           <h4>Your Hobbies</h4>
44           <button class="btn btn-default" type="button" (click)="onAddHobby()">Add Hobby</button>
45           <div 
46             class="form-group"
47             *ngFor="let hobbyControl of getHobbiesControl(); let i=index">
48             <input type="text" class="form-control" [formControlName]="i">
49           </div>
50         </div>
51         <button class="btn btn-primary" type="submit">Submit</button>
52       </form>
53     </div>
54   </div>
55 </div>

app.component.ts:

 1 import { Component, OnInit } from '@angular/core';
 2 import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms';
 3 import { Observable } from 'rxjs';
 4 
 5 @Component({
 6   selector: 'app-root',
 7   templateUrl: './app.component.html',
 8   styleUrls: ['./app.component.css']
 9 })
10 export class AppComponent implements OnInit{
11   genders = ['male', 'female'];
12   signupForm: FormGroup;
13   forbiddenUsernames = ['Aspirant'];
14 
15   constructor(private formBuilder: FormBuilder) {}
16 
17   ngOnInit() {
18     this.signupForm = new FormGroup({
19       'userData': new FormGroup({
20         'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
21         'email': new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails),
22       }),
23       'gender': new FormControl('male'),
24       'hobbies': new FormArray([])
25     });
26     this.signupForm.statusChanges.subscribe(
27       value => {
28         //console.log(value);
29       }
30     );
31     this.signupForm.patchValue({
32       'userData': {
33         'username': 'Name'
34       }
35     })
36   }
37 
38   onSubmit() {
39     console.log(this.signupForm);
40   }
41 
42   onAddHobby() {
43     const control = new FormControl(null, [Validators.required]);
44     (<FormArray>this.signupForm.get('hobbies')).push(control);
45   }
46 
47   getHobbiesControl() {
48     return (this.signupForm.get('hobbies') as FormArray).controls;
49   }
50 
51   forbiddenNames(control: FormControl): {[s: string]: boolean} {
52     if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
53       return {'nameIsForbidden': true};
54     }
55     return null;
56   }
57 
58   forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
59     const promise = new Promise<any>((resolve, reject) => {
60       setTimeout(() => {
61         if (control.value === 'test@test.com'){
62           resolve({'emailIsForbidden': true});
63         } else {
64           resolve(null);
65         }
66       }, 1500);
67     });
68     return promise;
69   }
70 }

 

posted @ 2021-04-14 15:06  Asp1rant  阅读(364)  评论(0编辑  收藏  举报