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输入字段的值发生变化时,数据流是如何从视图开始经过下列步骤进行流动的。
-
最终用户在输入框元素中敲 "Blue"。
-
该输入框元素会发出一个 "input" 事件,带着值 "Blue"。
-
附着在该输入框上的控件值访问器会触发
FormControl
实例上的setValue()
方法。 -
FormControl
实例通过valueChanges
这个可观察对象发出新值。 -
valueChanges
的任何订阅者都会收到新值。 -
控件值访问器
ControlValueAccessory
还会调用NgModel.viewToModelUpdate()
方法,它会发出一个ngModelChange
事件。 -
由于该组件模板双向数据绑定到了
favoriteColor
,组件中的favoriteColor
属性就会修改为ngModelChange
事件所发出的值("Blue")。
这个模型到视图的示意图展示了当 favoriteColor
从蓝变到红时,数据是如何经过如下步骤从模型流动到视图的。
-
组件中修改了
favoriteColor
的值。 -
变更检测开始。
-
在变更检测期间,由于这些输入框之一的值发生了变化,Angular 就会调用
NgModel
指令上的ngOnChanges
生命周期钩子。 -
ngOnChanges()
方法会把一个异步任务排入队列,以设置内部FormControl
实例的值。 -
变更检测完成。
-
在下一个检测周期,用来为
FormControl
实例赋值的任务就会执行。 -
FormControl
实例通过可观察对象valueChanges
发出最新值。 -
valueChanges
的任何订阅者都会收到这个新值。 -
控件值访问器
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]包装对象:
1 2 3 | < form [formGroup]="signupForm" (ngSubmit)="onSubmit()"> .. </ form > |
在ts中,signupForm作为AppComponent的一个对象,类型是FormGroup
数据流与双向绑定
在响应式表单中,视图中的每个表单元素都直接链接到一个表单模型(FormControl
实例)。 从视图到模型的修改以及从模型到视图的修改都是同步的,而且不依赖于 UI 的渲染方式。
这个视图到模型的示意图展示了当输入字段的值发生变化时数据是如何从视图开始,经过下列步骤进行流动的。
-
最终用户在输入框元素中键入了一个值,这里是 "Blue"。
-
这个输入框元素会发出一个带有最新值的 "input" 事件。
-
这个控件值访问器
ControlValueAccessor
会监听表单输入框元素上的事件,并立即把新值传给FormControl
实例。 -
FormControl
实例会通过valueChanges
这个可观察对象发出这个新值。 -
valueChanges
的任何一个订阅者都会收到这个新值。
这个模型到视图的示意图体现了程序中对模型的修改是如何通过下列步骤传播到视图中的。
-
favoriteColorControl.setValue()
方法被调用,它会更新这个FormControl
的值。 -
FormControl
实例会通过valueChanges
这个可观察对象发出新值。 -
valueChanges
的任何订阅者都会收到这个新值。 -
该表单输入框元素上的控件值访问器会把控件更新为这个新值。
输入验证
在响应式表单中,事实之源是其组件类。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(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 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2020-04-14 Mac npm安装报错 rollbackFailedOptional verb npm-session 解决办法