angular - 表单
(1)模板驱动表单
参考:
简单表单
ngForm 固定的,需要提交的值 ngModel
<form #sub="ngForm" (submit)="onSubmit(sub)">
<input type="text" name="username" ngModel>
<input type="id" name="userId" ngModel>
<button>提交</button>
</form>
onSubmit(form:NgForm){
console.log(form.value);
console.log(form.valid);
}
分组
ngModelGroup : 会创建 FormGroup
的实例并把它绑定到 DOM 元素中。
<form #sub="ngForm" (submit)="onSubmit(sub)">
<ng-container ngModelGroup="user">
<input type="text" name="username" ngModel>
</ng-container>
<ng-container ngModelGroup="Guid">
<input type="id" name="Id" ngModel>
</ng-container>
<button>提交</button>
</form>
(2)模板驱动表单验证
Angular 内置表单验证器:
- required:检查输入字段是否为空。
- minlength:检查输入字段的长度是否符合最小长度要求。
- maxlength:检查输入字段的长度是否符合最大长度要求。
- min:检查输入字段的值是否符合最小值要求。
- max:检查输入字段的值是否符合最大值要求。
- pattern:使用正则表达式来验证输入字段的值是否符合特定的模式。
- email:检查输入字段的值是否符合电子邮件格式。
- url:检查输入字段的值是否符合 URL 格式。
- date:检查输入字段的值是否符合日期格式。
- checkbox:检查是否选择了复选框。
- number:检查输入字段的值是否为数字。
- requiredTrue(): 这个验证器是 Angular 中的一个内置验证器,它用于检查输入字段是否被选中和/或填写。如果输入字段的值是 true 或者用户已经填写了该字段,那么这个验证就会通过。如果输入字段为空或者未被选中,那么这个验证就会失败。
- compose(): 这个函数是用来组合多个验证器的。你可以将多个验证器传递给 compose() 函数,然后它将返回一个新的验证器,这个新的验证器将按照你指定的顺序执行这些验证器。你可以使用 pipe() 函数来将多个验证器串联起来,但是 compose() 更加灵活,因为它允许你在不同的上下文中重复使用相同的验证器。s
显示未通过验证的信息
要启用验证功能:必须有一个模板引用变量 #username="ngModel",这里的 ngModel指令提供的功能,用于验证状态。
验证对象属性
名称 | 描述 |
---|---|
path | 这个属性返回元素的名称 |
valid | 通过验证规则定义的条件 (样式 ng-valid) |
invalid | 不能通过验证规则定义的条件 (样式 ng-invalid) |
pristine | 内容没修改,未被用户编辑(样式 ng-pristine) |
dirty | 内容被修改,被用户编辑 (样式 ng-drity) |
touched | 元素被用户访问 (样式 ng-touched)(一般通过制表符 TAB选择表单域) |
untouched | 元素未被用户访问, (样式 ng-untouched)(一般通过制表符 TAB选择表单域) |
errors | 返回一个 ValidationErrors 键值对对象 export declare type ValidationErrors = { [key: string]: any; }; |
value | 返回 value 值,用于 自定义表单验证规则 |
<form #sub="ngForm" (submit)="onSubmit(sub)">
<input type="text" name="username" required pattern="\d" #username="ngModel" as ngModel>
<div *ngIf="username.touched && username.invalid && username.errors">
<div *ngIf="username.errors['required']">必须填写</div>
<div *ngIf="username.errors['pattern']">必须为数字</div>
</div>
<input type="id" name="userId" ngModel>
<button [disabled]="sub.invalid">提交</button>
</form>
这里 angular 15 使用的 errors["XXXX"] 的方式(ValidationErrors),angular 12 使用的 errors.requered 的方式。
表单验证错误描述属性
名称 | 描述 |
---|---|
minlength.requiredLength | 所需的字符数 |
minlength.actualLength | 输入的字符数 |
pattern.requiredPattern | 返回指定正则 |
pattern.actualValue | 元素的内容 |
使用组件显示验证消息,验证整个表单
formSubmitted 指定表单是否已经提交,并在提交前阻止验证。
component.ts
import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app',
templateUrl: 'template.html',
})
export class ProductComponent {
model: Model = new Model();
//是否已经提交
formSubmitted: boolean = false;
submitForm(form: NgForm) {
this.formSubmitted = true;
if (form.valid) {
this.addProject(this.newProduct);
this.newProduct = new Product();
form.reset();
this.formSubmitted = false;
}
}
getValidationMessages(state: any, thingName?: string) {
let thing: string = state.path || thingName;
let messages: string[] = [];
if (state.errors) {
for (const errorName in state.errors) {
switch (errorName) {
case 'required':
messages.push(`you must enter a ${thing}`);
break;
case 'minlength':
messages.push(
`a ${thing} must be at least ${state.errors['minlength'].requiredLength}`
);
break;
case 'pattern':
messages.push(`The ${thing} contains illegal chracters`);
break;
}
}
}
return messages;
}
//......other method
}
html
form定义一个引用变量 form,ngForm赋值给它:#form="ngForm",ngSubmit 绑定表达式调用控制器 submitForm
name="name"这里的字符串name就是 #username的 path
<!-- 14.4.3 验证整个表单 -->
<style>
input.ng-dirty.ng-invalid {
border: 2px solid #ff0000
}
input.ng-dirty.ng-valid {
border: 2px solid #6bc502
}
</style>
<div class="m-2">
<div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>
<form #form="ngForm" (ngSubmit)="submitForm(form)">
<div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
There are problems with the form
</div>
<div class="form-group">
<label>Name</label>
<input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
pattern="^[A-Za-z ]+$" />
<ul class="text-danger list-unstlyed"
*ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
<li *ngFor="let error of getValidationMessages(username)">
<span>{{error}}</span>
</li>
</ul>
</div>
<button class="btn btn-primary" type="submit">Create</button>
</form>
</div>
1 显示摘要信息
component.ts
通过 NgForm 对象的 controls 属性访问各个元素
getFormValidationMessages(form: NgForm): string[] {
let messages: string[] = [];
Object.keys(form.controls).forEach((k) => {
this.getValidationMessages(form.controls[k], k).forEach((m) =>
messages.push(m)
);
});
return messages;
}
html
<!-- 14.4.3-1 显示验证摘要信息 -->
<style>
input.ng-dirty.ng-invalid {
border: 2px solid #ff0000
}
input.ng-dirty.ng-valid {
border: 2px solid #6bc502
}
</style>
<div class="m-2">
<div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>
<form #form="ngForm" (ngSubmit)="submitForm(form)">
<div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && form.invalid">
There are problems with the form
<ul>
<li *ngFor="let error of getFormValidationMessages(form)">
{{error}}
</li>
</ul>
</div>
<div class="form-group">
<label>Name</label>
<input class="form-control" name="name" [(ngModel)]="newProduct.name" #username="ngModel" required minlength="5"
pattern="^[A-Za-z ]+$" />
<ul class="text-danger list-unstlyed"
*ngIf="(formSubmitted || username.dirty) && username.invalid && username.errors">
<li *ngFor="let error of getValidationMessages(username)">
<span>{{error}}</span>
</li>
</ul>
</div>
<button class="btn btn-primary" type="submit">Create</button>
</form>
</div>
2 禁用提交按钮
<button class="btn btn-primary" type="submit" [disabled]="formSubmitted && form.invalid"
[class.btn-secondary]="formSubmitted && form.invalid">
Create
</button>
(3)模型驱动表单
3.1) 响应式表单
3.1.1) 创建基础表单
1 在你的应用中注册响应式表单模块。该模块声明了一些你要用在响应式表单中的指令。
要使用响应式表单控件,就要从
@angular/forms
包中导入 ReactiveFormsModule,并把它添加到你的 NgModule 的imports
数组中。
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
// other imports ...
ReactiveFormsModule
],
})
export class AppModule { }
2 生成一个新的 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('');
}
3 在模板中注册这个 FormControl。
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">
使用这种模板绑定语法,把该表单控件注册给了模板中名为 name
的输入元素。这样,表单控件和 DOM 元素就可以互相通讯了:视图会反映模型的变化,模型也会反映视图中的变化。
a) 显示表单值
-
通过可观察对象
valueChanges
,你可以在模板中使用 AsyncPipe或在组件类中使用subscribe()
方法来监听表单值的变化。 -
使用
value
属性。它能让你获得当前值的一份快照。
<p>Value: {{ name1.value }}</p>
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">
一旦你修改了表单控件所关联的元素,这里显示的值也跟着变化了。
b) 替换表单控件的值
html
<p>Value: {{ name1.value }}</p>
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name1">
<button type="button" (click)="updateName()">Update Name</button>
FormControl
实例提供了一个 setValue()
方法,它会修改这个表单控件的值
updateName() {
this.name1.setValue('Nancy');
}
3.1.2) 把表单分组 FormGroup 和 FromArray
FormGroup
定义了一个带有一组控件的表单,你可以把它们放在一起管理。表单组的基础知识将在本节中讨论。你也可以通过嵌套表单组来创建更复杂的表单。
FormArray
定义了一个动态表单,你可以在运行时添加和删除控件。你也可以通过嵌套表单数组来创建更复杂的表单。欲知详情,参阅下面的创建动态表单。
a) FormGroup 创建确定子控件数量的动态表单
1.创建一个 FormGroup
实例。
要初始化这个 FormGroup,请为构造函数提供一个由控件组成的对象,对象中的每个名字都要和表单控件的名字一一对应
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-ProfileEditor',
templateUrl: './ProfileEditor.component.html',
styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
});
constructor() { }
ngOnInit() {
}
}
2 把这个 FormGroup 模型关联到视图
由 FormControlName
指令提供的 formControlName
属性把每个输入框和 FormGroup
中定义的表单控件绑定起来。这些表单控件会和相应的元素通讯,它们还把更改传给 FormGroup
,这个 FormGroup
是模型值的事实之源。
<form [formGroup]="profileForm">
<label for="first-name">First Name: </label>
<input id="first-name" type="text" formControlName="firstName">
<label for="last-name">Last Name: </label>
<input id="last-name" type="text" formControlName="lastName">
</form>
3 保存表单数据。
ProfileEditor
组件从用户那里获得输入,但在真实的场景中,你可能想要先捕获表单的值,等将来在组件外部进行处理。FormGroup
指令会监听 form
元素发出的 submit
事件,并发出一个 ngSubmit
事件,让你可以绑定一个回调函数。把 onSubmit()
回调方法添加为 form
标签上的 ngSubmit
事件监听器。
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
ProfileEditor
组件上的 onSubmit()
方法会捕获 profileForm
的当前值。要保持该表单的封装性,就要使用 EventEmitter
向组件外部提供该表单的值。下面的例子会使用 console.warn
把这个值记录到浏览器的控制台中。
onSubmit() {
// TODO: Use EventEmitter with form value
console.warn(this.profileForm.value);
}
form
标签所发出的 submit
事件是内置 DOM 事件,通过点击类型为 submit
的按钮可以触发本事件。这还让用户可以用回车键来提交填完的表单。往表单的底部添加一个 button
,用于触发表单提交。
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
b) 嵌套表单组
1 创建一个嵌套的表单组
在这个例子中,address group
把现有的 firstName
、lastName
控件和新的 street
、city
、state
和 zip
控件组合在一起。虽然 address
这个 FormGroup
是 profileForm
这个整体 FormGroup
的一个子控件,但是仍然适用同样的值和状态的变更规则。来自内嵌控件组的状态和值的变更将会冒泡到它的父控件组,以维护整体模型的一致性
import { Component } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';
@Component({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl(''),
state: new FormControl(''),
zip: new FormControl('')
})
});
}
2 在模板中对这个嵌套表单分组
内嵌的使用formGroupName
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label for="first-name">First Name: </label>
<input id="first-name" type="text" formControlName="firstName">
<label for="last-name">Last Name: </label>
<input id="last-name" type="text" formControlName="lastName">
<div formGroupName="address">
<h2>Address</h2>
<label for="street">Street: </label>
<input id="street" type="text" formControlName="street">
<label for="city">City: </label>
<input id="city" type="text" formControlName="city">
<label for="state">State: </label>
<input id="state" type="text" formControlName="state">
<label for="zip">Zip Code: </label>
<input id="zip" type="text" formControlName="zip">
</div>
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
c) 更新部分数据模型
当修改包含多个 FormGroup
实例的值时,你可能只希望更新模型中的一部分,而不是完全替换掉
方法 | 详情 |
---|---|
setValue() |
使用 setValue() 方法来为单个控件设置新值。setValue() 方法会严格遵循表单组的结构,并整体性替换控件的值。 |
patchValue() |
用此对象中定义的任意属性对表单模型进行替换。 |
setValue()
方法的严格检查可以帮助你捕获复杂表单嵌套中的错误,而 patchValue()
在遇到那些错误时可能会默默的失败。
在 ProfileEditorComponent
中,使用 updateProfile
方法传入下列数据可以更新用户的名字与街道住址。
updateProfile() {
const newFirstName = 'Alice'; // 新的first name值
// setValue 更新单个
this.profileForm.get('firstName')?.setValue(newFirstName);
// patchValue 更新多个一部分
// this.profileForm.patchValue({
// firstName: 'Nancy',
// address: {
// street: '123 Drew Street'
// }
// });
}
加个按钮来更新
<button type="button" (click)="updateProfile()">Update Profile</button>
3.1.3) FormBuilder 服务生成控件
a 导入 FormBuilder 类。
b 注入这个 FormBuilder
服务。
c 生成表单内容。
FormBuilder
服务有三个方法:control()
、group()
和 array()
。这些方法都是工厂方法,用于在组件类中分别生成 FormControl
、FormGroup
和 FormArray
。
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
@Component({
selector: 'app-ProfileEditor',
templateUrl: './ProfileEditor.component.html',
styleUrls: ['./ProfileEditor.component.css']
})
export class ProfileEditorComponent implements OnInit {
profileForm = this.fb.group({
firstName: [''],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
});
constructor(private fb: FormBuilder) { }
ngOnInit() {
}
}
// ts{10-19,21}
在上面的例子中,你可以使用 group()
方法,用和前面一样的名字来定义这些属性。这里,每个控件名对应的值都是一个数组,这个数组中的第一项是其初始值。
3.1.4) 响应式表单的验证
3.1.4.1) 在表单组件中导入一个验证器函数。
从 @angular/forms
包中导入 Validators 类。
import { Validators } from '@angular/forms';
3.1.4.2) 把这个验证器添加到表单中的相应字段。
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
});
3.1.4.3) 添加逻辑来处理验证状态。
<p>Form Status: {{ profileForm.status }}</p>
参考:
3.1.5) FormArray 创建不确定数量子控件的动态表单
FormArray
如果你事先不知道子控件的数量,这就是一个很好的选择。
FormArray 是
FormGroup
之外的另一个选择,用于管理任意数量的匿名控件。像FormGroup
实例一样,你也可以往FormArray
中动态插入和移除控件,并且FormArray
实例的值和验证状态也是根据它的子控件计算得来的。不过,你不需要为每个控件定义一个名字作为 key.
a) 导入 FormArray
类。
b) 定义一个 FormArray
控件。
使用 FormBuilder.array() 方法来定义该数组,并用 FormBuilder.control() 方法来往该数组中添加一个初始控件。
FormGroup
中的这个 aliases
控件现在管理着一个控件,将来还可以动态添加多个。
profileForm = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
aliases: this.fb.array([
this.fb.control('')
])
});
c) 使用 getter 方法访问 FormArray
控件。
相对于重复使用 profileForm.get()
方法获取每个实例的方式,getter 可以让你轻松访问表单数组各个实例中的别名。表单数组实例用一个数组来代表未定数量的控件。通过 getter 来访问控件很方便,这种方法还能很容易地重复处理更多控件。
使用 getter 语法创建类属性 aliases
,以从父表单组中接收表示绰号的表单数组控件。
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
注意:
因为返回的控件的类型是 AbstractControl,所以你要为该方法提供一个显式的类型声明来访问 FormArray 特有的语法。
定义一个方法来把一个绰号控件动态插入到绰号 FormArray
中。用 FormArray.push()
方法把该控件添加为数组中的新条目。
addAlias() {
this.aliases.push(this.fb.control(''));
}
在这个模板中,这些控件会被迭代,把每个控件都显示为一个独立的输入框。
d) 在模板中显示这个表单数组。
要想为表单模型添加 aliases
,你必须把它加入到模板中供用户输入。和 FormGroupNameDirective
提供的 formGroupName 一样,FormArrayNameDirective
也使用 formArrayName 在这个 FormArray
实例和模板之间建立绑定。
在 formGroupName
<div>
元素的结束标签下方,添加一段模板 HTML。
<div formArrayName="aliases">
<h2>Aliases</h2>
<button type="button" (click)="addAlias()">+ Add another alias</button>
<div *ngFor="let alias of aliases.controls; let i=index">
<!-- The repeated alias template -->
<label for="alias-{{ i }}">Alias:</label>
<input id="alias-{{ i }}" type="text" [formControlName]="i">
</div>
</div>
3.2) 严格类型化表单
从 Angular 14 开始,响应式表单默认是严格类型的。
3.2.1) FormControl 入门
const email = new FormControl('angularrox@gmail.com');
此控件将被自动推断为 FormControl<string|null>
类型。TypeScript 会在整个FormControl
API中自动强制执行此类型,例如 email.value
、 email.valueChanges
、 email.setValue(...)
等。
a) 可空性 nonNullable
你可能想知道:为什么此控件的类型包含 null
?这是因为控件可以随时通过调用 reset 变为 null
:
const email = new FormControl('angularrox@gmail.com');
email.reset();
console.log(email.value); // null
TypeScript 将强制你始终处理控件已变为 null
的可能性。如果要使此控件不可为空,可以用 nonNullable
选项。这将导致控件重置为其初始值,而不是 null
:
const email = new FormControl('angularrox@gmail.com', {nonNullable: true});
email.reset();
console.log(email.value); // angularrox@gmail.com
3.2.2) FormArray:动态的、同质的集合
FormArray
包含一个开放式控件列表。type 参数对应于每个内部控件的类型:
const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));
此 FormArray 将具有内部控件类型 FormControl<string|null>
。
如果你想在数组中有多个不同的元素类型,则必须使用 UntypedFormArray
,因为 TypeScript 无法推断哪种元素类型将出现在哪个位置。
3.2.3) FormGroup 和 FormRecord
Angular 为具有枚举键集的表单提供了 FormGroup
类型,并为开放式或动态组提供了一种名为 FormRecord
的类型。
a) 部分值
const login = new FormGroup({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
在任何 FormGroup
上,都可以禁用控件。任何禁用的控件都不会出现在组的值中。
因此,login.value
的类型是 Partial<{email: string, password: string}>
。这种类型的 Partial
意味着每个成员可能是未定义的。
更具体地说,login.value.email
的类型是 string|undefined
,TypeScript 将强制你处理可能 undefined
的值(如果你启用了 strictNullChecks
)。
如果你想访问包括禁用控件的值,从而绕过可能的 undefined
字段,可以用 login.getRawValue()
。
b) 可选控件和动态组
某些表单的控件可能存在也可能不存在,可以在运行时添加和删除。你可以用可选字段来表示这些控件:
interface LoginForm {
email: FormControl<string>;
password?: FormControl<string>;
}
const login = new FormGroup<LoginForm>({
email: new FormControl('', {nonNullable: true}),
password: new FormControl('', {nonNullable: true}),
});
login.removeControl('password');
在这个表单中,我们明确地指定了类型,这使我们可以将 password
控件设为可选的。TypeScript 会强制只有可选控件才能被添加或删除。
c) FormRecord
某些 FormGroup
的用法不符合上述模式,因为键是无法提前知道的。FormRecord
类就是为这种情况设计的:
const addresses = new FormRecord<FormControl<string|null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));
任何 string|null
类型的控件都可以添加到此 FormRecord
。
如果你需要一个动态(开放式)和异构(控件是不同类型)的 FormGroup
,则无法提升为类型安全的,这时你应该使用 UntypedFormGroup
。
FormRecord
也可以用 FormBuilder
构建:
const addresses = fb.record({'Andrew': '2340 Folsom St'});
如果你需要一个动态(开放式)和异构(控件是不同类型)的 FormGroup
,则无法提高类型安全,你应该使用 UntypedFormGroup
。
3.2.4) FormBuilder 和 NonNullableFormBuilder
FormBuilder
类已升级为支持新增的类型的版本,方式与上面的示例相同。
此外,还有一个额外的构建器:NonNullableFormBuilder
。它是在所有控件都上指定 {nonNullable: true}
的简写,用来在大型非空表单中消除主要的样板代码。你可以用 FormBuilder
上的 nonNullable
属性访问它:
const fb = new FormBuilder();
const login = fb.nonNullable.group({
email: '',
password: '',
});
在上面的示例中,两个内部控件都将不可为空(即将设置 nonNullable
)。
你还可以用名称 NonNullableFormBuilder
注入它。
3.3) 构建动态表单
3.4) 例子
ReactiveFormsModule 的使用
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
// import { AppComponent } from './app.component';
import { ProductComponent } from './component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [ProductComponent], //列出应用的类,告诉Angular哪些类构成了这个模块。
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
providers: [], //提供服务
bootstrap: [ProductComponent], //根组件
})
export class AppModule {}
基于模型的功能在 ReactiveFormsModule 模块定义中。
FormControl 和 FormGroup 的使用
FormControl: 表单中的表单项
FormGroup: 表单组,表单至少是一个 FormGroup
// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';
export class ProductFormControl extends FormControl { //FormControl表单中的单个元素
lable: string;
modelProperty: string;
/**
* 构造函数用于初始化一个带有label、模型属性、值和验证器的 FormControl。
* @param {string} lable - 表示控件的标签或名称的字符串。它用于在用户界面中显示控件的label。
* @param {string} modelProperty - 表示绑定到此表单控件的模型对象的属性名称的字符串。它用于将表单控件的值映射到模型对象的相应属性上。
* @param {any} value - 代表控件当前值的参数。对于 FormControl,它是当前值。对于已启用的 FormGroup,它是组内所有已启用控件的值,以键值对的形式呈现为对象。对于已禁用的 FormGroup,它是组内所有控件的值。
* @param {any} validator - 用于确定控件有效性的函数。它可以是一个单独的验证器函数,也可以是使用 Validators.compose() 函数组合多个验证器函数的结果。验证器用于对控件的值执行验证检查,并在值无效时返回错误对象。
*/
constructor(
lable: string,
modelProperty: string,
value: any,
validator: any
) {
/**
* 例子:const fc = new FormControl('foo');
*
* value: 控件的当前值。
对于 FormControl,是当前值。
对于已启用的 FormGroup,是组内所有已启用控件的值,以对象形式呈现,每个组成员都有一个键值对。
对于已禁用的 FormGroup,是组内所有控件的值,以对象形式呈现,每个组成员都有一个键值对。
对于 FormArray,是组内所有已启用控件的值,以数组形式呈现。
validator: 返回用于同步确定此控件有效性的函数。如果已添加多个验证器,则这将是一个组合函数。有关更多信息,请参阅 Validators.compose()。
*/
super(value, validator); //实现 FormControl 的构造函数
this.lable = lable;
this.modelProperty = modelProperty;
}
}
export class ProductFormGroup extends FormGroup { //FormGroup 管理form 元素
constructor() {
super({
name: new ProductFormControl('Name', 'name', '', Validators.required),
category: new ProductFormControl(
'Category',
'category',
'',
Validators.compose([
Validators.required,
Validators.pattern('^[A-Za-z ]+$'),
Validators.minLength(3),
Validators.maxLength(10),
])
),
price: new ProductFormControl(
'Price',
'price',
'',
Validators.compose([
Validators.required,
Validators.pattern('^[0-9\.]+$'),
])
),
});
}
}
FormGroup的构造函数接收一个对象,该对象各个属性与各个属性的名称与模板各个 input 元素的名称一一对应,每个属性都赋予一个用来表示该 input 元素的 ProductFormControl 对象,该对象同时指定 input 的验证要求。
传给超类构造函数对象第一个属性最简单:
name: new ProductFormControl('Name', 'name', '', Validators.required),
属性名为 name,告诉angular 属性对应模板中名为 name 的input元素。ProductFormControl 构造函数实参指定:
与 input 元素相关的 label 元素内容(Name),
input 元素绑定的 Product 类的某个属性名称(name),
数据绑定的初始值(空字符串)
所需的验证(Validators.required)
可以使用 Validators.compose 将多个验证器组合起来。
FormArray: 复杂表单, 动态添加表单项和表单组,表单验证时,FormArray 有一项没通过,整体不通过。
(4)模型驱动的表单验证
错误消息生成从组件移动到表单模型类,让组件尽可能简单。ProductFormControl 类的 getValidationMessages 方法中尉 maxLength 添加的验证消息。
// form.module.ts
import { FormControl, FormGroup, Validators } from '@angular/forms';
export class ProductFormControl extends FormControl {
//FormControl表单中的单个元素
lable: string;
modelProperty: string;
/**
* 构造函数用于初始化一个带有lable、模型属性、值和验证器的 FormControl。
* @param {string} lable - 表示控件的标签或名称的字符串。它用于在用户界面中显示控件的 lable。
* @param {string} modelProperty - 表示绑定到此表单控件的模型对象的属性名称的字符串。它用于将表单控件的值映射到模型对象的相应属性上。
* @param {any} value - 代表控件当前值的参数。对于 FormControl,它是当前值。对于已启用的 FormGroup,它是组内所有已启用控件的值,以键值对的形式呈现为对象。对于已禁用的 FormGroup,它是组内所有控件的值。
* @param {any} validator - 用于确定控件有效性的函数。它可以是一个单独的验证器函数,也可以是使用 Validators.compose() 函数组合多个验证器函数的结果。验证器用于对控件的值执行验证检查,并在值无效时返回错误对象。
*/
constructor(
lable: string,
modelProperty: string,
value: any,
validator: any
) {
/**
* 例子:const fc = new FormControl('foo');
*
* value: 控件的当前值。
对于 FormControl,是当前值。
对于已启用的 FormGroup,是组内所有已启用控件的值,以对象形式呈现,每个组成员都有一个键值对。
对于已禁用的 FormGroup,是组内所有控件的值,以对象形式呈现,每个组成员都有一个键值对。
对于 FormArray,是组内所有已启用控件的值,以数组形式呈现。
validator: 返回用于同步确定此控件有效性的函数。如果已添加多个验证器,则这将是一个组合函数。有关更多信息,请参阅 Validators.compose()。
*/
super(value, validator); //实现 FormControl 的构造函数
this.lable = lable;
this.modelProperty = modelProperty;
}
//14.5.2 定义表单模型
getValidationMessages() {
let messages: string[] = [];
if (this.errors) {
for (const errorName in this.errors) {
switch (errorName) {
case 'required':
messages.push(`you must enter a ${this.lable}`);
break;
case 'minlength':
messages.push(
`a ${this.lable} must be at least ${this.errors['minlength'].requiredLength}`
);
break;
case 'maxlength':
messages.push(
`a ${this.lable} must be no more than ${this.errors['minlength'].requiredLength}`
);
break;
case 'pattern':
messages.push(`The ${this.lable} contains illegal chracters`);
break;
}
}
}
return messages;
}
}
export class ProductFormGroup extends FormGroup {
//FormGroup 管理form 元素
constructor() {
super({
name: new ProductFormControl('Name', 'username', '', Validators.required),
category: new ProductFormControl(
'Category',
'category',
'',
Validators.compose([
Validators.required,
Validators.pattern('^[A-Za-z ]+$'),
Validators.minLength(3),
Validators.maxLength(10),
])
),
price: new ProductFormControl(
'Price',
'price',
'',
Validators.compose([
Validators.required,
Validators.pattern('^[0-9.]+$'),
])
),
});
}
get productControls(): ProductFormControl[] {
return Object.keys(this.controls).map(
(k) => this.controls[k] as ProductFormControl
);
}
getValidationMessages(name: string): string[] {
return (
this.controls[name] as ProductFormControl
).getValidationMessages();
}
getFormValidationMessages(): string[] {
let messages: string[] = [];
Object.values(this.controls).forEach((c) =>
messages.push(...(c as ProductFormControl).getValidationMessages())
);
return messages;
}
}
删除了用于生成错误消息的方法,它们被移动到 form.model
导入 ProductFormGroup 类,并且定义一个 fromGroup 属性,这样就可以使用自定义表单模型类。
//14.5.3 使用模型验证
//component.ts
import { ApplicationRef, Component } from '@angular/core';
import { Model } from './repository.model';
import { Product } from './product.model';
import { NgForm } from '@angular/forms';
import { ProductFormGroup } from './form.module';
@Component({
selector: 'app',
templateUrl: 'template.html',
})
export class ProductComponent {
model: Model = new Model();
formGroup: ProductFormGroup = new ProductFormGroup();
selectedProduct: string = '';
//是否已经提交
formSubmitted: boolean = false;
submitForm() {
Object.keys(this.formGroup.controls)
.forEach(c=> this.newProduct[c] = this.formGroup.controls[c].value);//如果错误:No index signature with a parameter of type 'string' was found on type 'Product'. => Product 类定义 [key: string]: any;
this.formSubmitted = true;
if (this.formGroup.valid) {
this.addProject(this.newProduct);
this.newProduct = new Product();
this.formGroup.reset();
this.formSubmitted = false;
}
}
newProduct: Product = new Product();
get jsonProduct() {
return JSON.stringify(this.newProduct);
}
addProject(p: Product) {
console.log('new project: ' + this.newProduct);
}
}
下面是HTML,
<!-- 14.5.3 使用模型验证 -->
<style>
input.ng-dirty.ng-invalid {
border: 2px solid #ff0000
}
input.ng-dirty.ng-valid {
border: 2px solid #6bc502
}
</style>
<div class="m-2">
<div class="bg-info text-white mb-2 p-2">Model Data:{{jsonProduct}}</div>
<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
<div class="bg-danger text-white p-2 mb-2" *ngIf="formSubmitted && formGroup.invalid">
There are problems with the form
<ul>
<li *ngFor="let error of formGroup.getFormValidationMessages()">
{{error}}
</li>
</ul>
</div>
<div class="form-group">
<label>Name</label>
<input class="form-control" name="name" formControlName="name" />
<ul class="text-danger list-unstlyed"
*ngIf="(formSubmitted || formGroup.controls['name'].dirty) && formGroup.controls['name'].invalid">
<li *ngFor="let error of formGroup.getValidationMessages('name')">
<span>{{error}}</span>
</li>
</ul>
</div>
<div class="form-group">
<label>Category</label>
<input class="form-control" name="price" formControlName="category" />
<ul class="text-danger list-unstlyed"
*ngIf="(formSubmitted || formGroup.controls['category'].dirty) && formGroup.controls['category'].invalid">
<li *ngFor="let error of formGroup.getValidationMessages('category')">
<span>{{error}}</span>
</li>
</ul>
</div>
<div class="form-group">
<label>Price</label>
<input class="form-control" name="price" formControlName="price" />
<ul class="text-danger list-unstlyed"
*ngIf="(formSubmitted || formGroup.controls['price'].dirty) && formGroup.controls['price'].invalid">
<li *ngFor="let error of formGroup.getValidationMessages('price')">
<span>{{error}}</span>
</li>
</ul>
</div>
<button class="btn btn-primary" type="submit"
[disabled]="formSubmitted && formGroup.invalid"
[class.btn-secondary]="formSubmitted && formGroup.invalid">
Create
</button>
</form>
</div>
第1个修改:form
<form class="m-2" novalidate [formGroup]="formGroup" (ngSubmit)="submitForm()">
[formGroup]="formGroup" 赋给 formGroup 指令的值是 form 属性,这里是 formGroup,返回一个 ProductFormGroup 对象。
第2个修改:input
删除了单独的验证属性和被赋予特殊值 ngForm 的模板变量。但是添加了 foprmControlName 属性,模型表单这里使用 ProductFormGroup 使用名称来识别。
foprmControlName 让 angular 添加和移除 input元素的验证类。告诉 angular,用特定的验证器验证。
···
name: new ProductFormControl('Name', 'username', '', Validators.required),
···
第3个修改: ul
FormGroup 提供一个 controls 属性,返回自己管理的 FormControl 对象集合,按名称进行索引。
export class Product { constructor( public id?: number, public name?: string, public category?: string, public price?: number ) {} [key: string]: any; }