文章目录
案例来源于Angular官网(略作拓展)
表单
1.1 响应式表单 reactive forms
通过指令[formControl]
实现,直接访问FormControl
实例
通过指令内部的值访问器ControlValueAccessor
,将FormControl
实例和视图中的表单元素联系起来。
1.2 模板驱动表单 template-driven forms
通过NgModel
指令为表单元素创建并管理FormControl
实例,间接访问FormControl
实例
1.3 比较
响应式 | 模板驱动 | |
---|---|---|
建立表单模型 | 显式创建,在组件类里创建 | 隐式创建 |
数据模型 | 结构化、不可变 | 非结构化、可变 |
数据流 | 同步 | 异步 |
表单验证 | 函数 | 指令 |
适用场景 | 可伸缩性强、较少的测试设置(不需要深入理解变更检测) | 可复用性弱、测试以来手动触发变更检测 |
1.4 使用
declare abstract class AbstractControl<TValue = any, TRawValue extends TValue = TValue> {
readonly value: TValue;
constructor(validators: ValidatorFn | ValidatorFn[] | null, asyncValidators: AsyncValidatorFn | AsyncValidatorFn[] | null);
get valid(): boolean;
readonly errors: ValidationErrors | null;
readonly valueChanges: Observable<TValue>;
addValidators(validators: ValidatorFn | ValidatorFn[]): void;
removeValidators(validators: ValidatorFn | ValidatorFn[]): void;
abstract setValue(value: TRawValue, options?: Object): void;
abstract patchValue(value: TValue, options?: Object): void;
abstract reset(value?: TValue, options?: Object): void;
setErrors(errors: ValidationErrors | null, opts?: {
emitEvent?: boolean;
}): void;
getError(errorCode: string, path?: Array<string | number> | string): any;
hasError(errorCode: string, path?: Array<string | number> | string): boolean;
}
export declare interface FormControl<TValue = any> extends AbstractControl<TValue> {
setValue(value: TValue, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
emitModelToViewChange?: boolean;
emitViewToModelChange?: boolean;
}): void;
patchValue(value: TValue, options?: {
onlySelf?: boolean;
emitEvent?: boolean;
emitModelToViewChange?: boolean;
emitViewToModelChange?: boolean;
}): void;
}
export export declare class FormGroup<TControl extends {
[K in keyof TControl]: AbstractControl<any>;
} = any> extends AbstractControl<ɵTypedOrUntyped<TControl, ɵFormGroupValue<TControl>, any>, ɵTypedOrUntyped<TControl, ɵFormGroupRawValue<TControl>, any>> {
controls: ɵTypedOrUntyped<TControl, TControl, {
[key: string]: AbstractControl<any>;
}>;
addControl(this: FormGroup<{
[key: string]: AbstractControl<any>;
}>, name: string, control: AbstractControl, options?: {
emitEvent?: boolean;
}): void;
removeControl(this: FormGroup<{
[key: string]: AbstractControl<any>;
}>, name: string, options?: {
emitEvent?: boolean;
}): void;
}
1.4.1 FormControl
创建表单/模板中注册表单
export class NameEditorComponent {
name = new FormControl('', [Validators.required, this.lessThan8Words]);
}
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">
获取/更新表单的值
<p>Value: {{ name.value }}</p>
<button type="button" (click)="updateName()">Update Name</button>
<button type="submit" (click)="getName()">Update Name</button>
updateName() {
this.name.setValue('Nancy');
}
getName() {
console.log(this.name.value);
}
为表单添加事件
ngOnInit(): void {
this.name.valueChanges.subscribe((value) => {
console.log(value); // 获取到最新的表单值
});
}
校验表单
我们在创建表单时,为表单添加自定义校验规则(名字长度不能超过8个字母)
lessThan8Words: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
if (control.value.length >= 8) {
return { nameError: 'name length must less than 8.' };
}
return null;
};
name = new FormControl('', [Validators.required, this.lessThan8Words]);
<p *ngIf="name.hasError('nameError')" style="color: red;">{{ name.getError('nameError') }}</p>
在name
这个表单对象中,包含了以下几个和校验相关的常用属性
valid: boolean
:表示是否符合校规则
errors:ValidationErrors
:汇总了所有校验错误的属性和值
hasError('nameError')
:error
对象是否包含该错误getError('nameError')
:获取该错误的错误信息
{
"nameError": "name length must less than 8."
"required": true // 说明此时,表单里为空
}
代码
import { NgIf } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import {
FormControl,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators,
AbstractControl,
} from '@angular/forms';
@Component({
standalone: true,
selector: 'app-name-editor',
templateUrl: './name-editor.component.html',
styleUrls: ['./name-editor.component.css'],
imports: [ReactiveFormsModule, NgIf],
})
export class NameEditorComponent implements OnInit {
ngOnInit(): void {
this.name.valueChanges.subscribe((value) => {
console.log(value);
});
}
lessThan8Words: ValidatorFn = (
control: AbstractControl
): ValidationErrors | null => {
if (control.value.length >= 8) {
return { nameError: 'name length must less than 8.' };
}
return null;
};
name = new FormControl('', [Validators.required, this.lessThan8Words]);
updateName() {
this.name.setValue('Nancy');
}
getName() {
console.log(this.name.value);
}
}
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">
<p>Value: {{ name.value }}</p>
<p *ngIf="name.hasError('nameError')" style="color: red;">{{ name.getError('nameError') }}</p>
<button type="button" (click)="updateName()">Update Name</button>
<button type="submit" (click)="getName()">Update Name</button>
表单中通常包含多个相关的控件,响应式表单提供了
FormGroup
和FormArray
两种组合相关控件的方式
1.4.2 FormGroup
FormGroup
可以理解为FormControl
的组合,在使用上略有延伸。此处我们来看几个不同的地方
创建表单/模板中注册表单
export class FullNameEditorComponent implements OnInit {
fullNameForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
});
}
<form [formGroup]="fullNameForm" (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">
</form>
获取/更新表单的值
public onSubmit(): void {
console.log(this.fullNameForm?.value);
}
- setValue:严格遵循表单的组织结构,替换控件的整个值。若传入对象的结构和定义的表单结构不同,则更新失败。
- patchValue:替换表单中的任何属性。若传入对象的结构和定义的表单结构不同,不报错,仅更新表单模型中定义的属性。
public updateFullName(): void {
this.fullNameForm.setValue({ firstName: 'amy', lastName: 'niu' });
this.fullNameForm.patchValue({ firstName: 'amy' }); // 这种也能通过
}
新增/移除表单
this.fullNameForm.addControl('newControl', new FormControl(''));
this.fullNameForm.removeControl('firstName');
获取到某个FormControl
对象
-
controls
{ firstName: FormControl, lastName: FormControl }
-
get('firstName')
获取到firstName
这个FormControl
对象
1.4.3 FormArray
FormArray可以理解为
FormControl的数组,在使用上略有延伸。
- 构造函数的第一个参数即是FormControl的数组
- 使用方式类似Array,at/push/insert等
1.4.3 FormBuilder
当我们要设计一个控件众多的表单时,重复的new FormControl
非常麻烦,所以FormBuilder
提供了更便利的方式生成控件。
this.formBuilder.group
的返回值是FormGroup
类型,故它的具体使用方式和`FromGroup基本一致,此处不重复阐述。
仅展示如何创建表单以及在模版中注册表单
import { JsonPipe, NgFor } from '@angular/common';
import { Component } from '@angular/core';
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Validators } from '@angular/forms';
import { FormArray } from '@angular/forms';
@Component({
standalone: true,
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css'],
imports: [NgFor, ReactiveFormsModule, JsonPipe],
})
export class ProfileEditorComponent {
profileForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.formBuilder.group({
street: [''],
city: [''],
state: [''],
zip: [''],
}),
aliases: this.formBuilder.array([this.formBuilder.control('')]),
});
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
constructor(private formBuilder: FormBuilder) {}
addAlias() {
this.aliases.push(this.formBuilder.control(''));
}
onSubmit() {
// TODO: Use EventEmitter with form value
console.warn(this.profileForm.value);
}
}
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
<label for="first-name">First Name: </label>
<input id="first-name" type="text" formControlName="firstName" required>
<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>
<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>
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
</form>
<hr>
<p>Form Value: {{ profileForm.value | json }}</p>
<p>Form Status: {{ profileForm.status }}</p>
<button type="button" (click)="updateProfile()">Update Profile</button>