对 Angular Forms 的深入探索
深入研究了这个@angular/forms软件包之后,我对它的底层工作原理有了更清晰的了解。在本文中,我想与大家分享我的见解。
注意:本文基于Angular 8.2.x版本。
内容
- 基础实体
- 模板驱动表单和响应式表单
- 验证者
- 探索内置
ControlValueAccessor功能 - 更好地了解这
AbstractControl棵树_pendingDirty,,_pendingValue_pendingChange_pendingChange_pendingDirty_pendingValueAbstractControl.setValue()和AbstractControl.patchValue()patchValue例子setValue例子AbstractControl提交后树状图会发生什么变化?AbstractControl从树中检索s- AbstractControl.updateValueAndValidity()
- 禁用/
AbstractControl启用 - 如何根据 AbstractControl 的状态添加 CSS 类?
- 结论
基础实体
为了充分利用Forms API,我们必须确保仔细查看它的一些基本部分。
抽象控制
这个(抽象)类包含多个类共享的逻辑FormControl,FormGroup以及FormArray:
- 运行验证器
- 更改和计算 UI 状态-
markAsDirty(),,,,等等...markAsTouched()dirtytouchedpristine - 重置状态
- 跟踪验证状态(
invalid,valid)
这个类及其子类可以称为模型层——它存储与特定实体相关的数据。
多个AbstractControls 可以看作一棵树,其中叶子始终是FormControl实例,而另外 2 个(FormArray,FormGroup)可以被视为AbstractControl容器,这意味着它们不能用作叶子,因为它们必须至少包含一个AbstractControl实例。
// FG - FormGroup
// FA - FormArray
// FC - FormControl
FG
/ \
FC FG
/ \
FC FA
/ | \
FC FC FC
上面的树可能是以下原因造成的:
<form>
<input type="text" formControlName="companyName">
<ng-container formGroupName="personal">
<input type="text" formControlName="name">
<ng-container formArrayName="hobbies">
<input type="checkbox" formControlName="0">
<input type="checkbox" formControlName="1">
<input type="checkbox" formControlName="2">
</ng-container>
</ng-container>
</form>
您可以在接下来的章节中找到更多相关信息formArrayName。formGroupName
表单控件
它AbstractControl继承了上述所有特性。这里需要特别指出的是,它仅与一个表单控件(DOM 元素: `<form> ` 或 ` <custom>`)或自定义组件(借助 `<custom> `——稍后会详细介绍!)FormControl组合在一起。<input><textarea>ControlValueAccessor
如果一个元素不属于任何树状结构,FormControl则可以将其视为独立元素。因此,它将完全独立,这意味着它的有效性、值和用户交互不会影响其任何表单容器祖先(ng-run 示例)。AbstractControl
表单数组
它具有扩展性AbstractControl,其作用是将多个AbstractControls 组合在一起。
从树状结构的角度来看,节点必须至少包含一个子节点。它的验证状态、脏值、已修改状态和值通常取决于其子节点。但是,也可能存在容器具有特定验证器的情况,因此错误可能会出现在该节点级别。
它的主要特点是将子节点存储在数组中。
表单组
与 相同FormArray,只是它将后代存储在一个对象中。
抽象控制指令
它是基于表单控件的指令(NgModel,,)的基类FormControlName,并包含反映绑定控件当前状态的布尔值获取器(,,等等…… )。 前面提到的控件借助(,)的具体实现和一个。FormControlDirectivevalidtoucheddirtyAbstractControlDirectiveNgModelFormControlNameControlValueAccessor
因此,可以将此类视为middleman连接ControlValueAccessor(视图层)和AbstractControl(模型层)的桥梁——更多内容将在后续章节中介绍。
值得一提的是,多个AbstractControlDirectives 可以将同一个 s 绑定 AbstractControl到多个DOM 元素或自定义组件,或者绑定到多个ControlValueAccessors。
请看这个例子:
<form>
<input ngModel name="option" value="value1" type="radio">
<input ngModel="value3" name="option" value="value2" type="radio">
<input ngModel="value1" name="option" value="value3" type="radio">
</form>
另外,要直接在模板中设置默认值,可以将最后一个ngModel指令的值设置为您希望默认选中的单选按钮的值。在上面的代码片段中,第一个按钮将被选中。
这是因为最后一个指令将是对函数 的最后一次调用。setUpControl()
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
/* ... */
dir.valueAccessor !.writeValue(control.value);
/* ... */
}
抽象表单组指令
它是一个容器,用于AbstractFormGroupDirective存储AbstractControlDirective实例,当你想要创建子组AbstractControl(例如address: { city, street, zipcode }:)或为某些特定对象运行验证器时非常有用AbstractControls(例如:最小-最大验证器,确保min控件的值不能大于max控件的值)。
其具体实现方式有:formGroupName,,formArrayName。ngModelGroup
<form [formGroup]="filterForm">
<ng-container formGroupName="price">
<input formControlName="min" type="text">
<input formControlName="max" type="text">
</ng-container>
</form>
FormGroupName作为它的子类,AbstractFormGroupDirective它拥有本节开头列出的所有属性。它AbstractControl也可以作为实例的容器。
但是,它FormGroup只能是顶级容器。这意味着,您不能将FormGroupName其用作顶级容器,否则会导致错误。
AbstractFormGroupDirective提供了一种访问顶级FormGroup实例的方法:
get formDirective(): Form|null { return this._parent ? this._parent.formDirective : null; }
其中this._parent可以是另一个AbstractFormGroupDirective实例FormGroupDirective。它FormGroupDirective没有_parent属性。
ControlValueAccessor
ControlValueAccessor是Forms API的重要组成部分,可以将其视为视图层。
它的作用是将DOM 元素(例如 `<div>` <input>、`<div> <textarea>`)或自定义组件(例如`<div> ` <app-custom-input>)与` <div> ` 连接起来。最终,它将成为视图层和模型层之间的桥梁。这样,这两个层就可以相互交互。AbstractControlDirectiveNgModelFormControlNameAbstractControlDirectiveControlValueAccessorAbstractControl
例如:
- 当用户在输入框中输入内容时:
View->Model - 当值通过编程方式设置时
FormControl.setValue('newValue'):Model->View
只有FormControl实例才能“直接”与视图交互ControlValueAccessor,因为在视图树中AbstractControl,视图FormControl只能是叶节点,它不应该包含其他节点。由此我们可以推断,来自视图的更新将从叶节点开始。
// FG - FormGroup
// FA - FormArray
// FC - FormControl
FG
/ \
user typing into an input <- FC FA
/ | \
FC FC FC <- user selecting checkbox
界面ControlValueAccessor如下所示:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
writeValue()- 向元素写入新值;新值来自模型(FormControl.setValue->ControlValueAccessor.writeValue-> 更新元素 -> 更改在用户界面中可见)registerOnChange()- 注册一个回调函数,每当UI中的值发生变化 时,该函数都会被调用,并将新值传播到模型。registerOnTouched()- 注册一个回调函数,当失去焦点时,该函数将被调用;FormControl当失去焦点时,程序可能会收到此事件的通知,因为它可能需要执行一些更新。setDisabledState-根据提供的值禁用/启用DOM元素;此方法通常是由于 MODEL 发生变化而调用的。
您可以在以下部分看到这些方法的用途:连接FormControl到ControlValueAccessor。
有 3 种类型的ControlValueAccessors:
- 默认
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
})
export class DefaultValueAccessor implements ControlValueAccessor { }
- 内置
const BUILTIN_ACCESSORS = [
CheckboxControlValueAccessor,
RangeValueAccessor,
NumberValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,
RadioControlValueAccessor,
];
您可以在“探索内置访问器”中阅读更多有关内置访问器的信息。ControlValueAccessor
- 自定义 - 当您希望自定义组件成为组件
AbstractControl树的一部分时。
@Component({
selector: 'app-custom-component',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: CustomInputComponent,
multi: true,
}
]
/* ... */
})
export class CustomInputComponent implements ControlValueAccessor { }
<form>
<app-custom-component ngModel name="name"></app-custom-component>
</form>
请记住,这ngModel是一个基于表单控件的ControlValueAccessor指令,因此它将成为(视图)和FormControl(模型)之间的桥梁。
联系FormControlControlValueAccessor
如前文所述,视图层( )AbstractControlDirective需要与模型层(,具体而言)进行有效通信,反之亦然。ControlValueAccessorAbstractControlFormControl
这种联系可以形象地表示如下:
--------------------------
| |
| ControlValueAccessor | <--- View Layer
| |
--------------------------
| ▲
| |
| |
▼ |
------------------------------
| |
| AbstractControlDirective |
| |
------------------------------
| ▲
| |
| |
▼ |
-----------------
| |
| FormControl | <--- Model Layer
| |
-----------------
这↓表示ViewToModelPipeline,而↑表示ModelToViewPipeline。
AbstractControlDirective它在这里起着至关重要的作用。让我们来看看实际的实现情况!
上图是以下代码片段生成的:
注意:实际上,它NgControl扩展了,并且主要充当基于表单控件的AbstractControlDirective指令的提供者:,等等……,但没有任何默认实现。NgModelFormControlName
每次初始化基于表单控件的指令时,setUpControl都会调用该函数。
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
control.validator = Validators.compose([control.validator !, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
dir.valueAccessor !.writeValue(control.value);
setUpViewChangePipeline(control, dir);
setUpModelChangePipeline(control, dir);
setUpBlurPipeline(control, dir);
/* ... Skipped for brevity ... */
}
// VIEW -> MODEL
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
// Update the MODEL based on the VIEW's value
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
// `{emitModelToViewChange: false}` will make sure that `ControlValueAccessor.writeValue` won't be called
// again since the value is already updated, because this change comes from the view
control.setValue(control._pendingValue, {emitModelToViewChange: false});
// If you have something like `<input [(ngModel)]="myValue">`
// this will allow `myValue` to be the new value that comes from the view
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
// MODEL -> VIEW
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
// control -> view
dir.valueAccessor !.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
});
}
以下是ControlValueAccessor界面:
export interface ControlValueAccessor {
writeValue(obj: any): void;
registerOnChange(fn: any): void;
registerOnTouched(fn: any): void;
setDisabledState?(isDisabled: boolean): void;
}
如您所见,该setUpViewChangePipeline方法通过为视图分配一个回调函数,将视图与模型(单向连接)AbstractControlDirective连接起来。这将dir允许在视图中发生的操作传播到模型中。ControlValueAccessor.onChange
以下是一个具体的实现示例ControlValueAccessor.registerOnChange:
@Directive({
selector: 'input[custom-value-accessor][type=text][ngModel]',
host: {
'(input)': 'onChange($event.target.value)',
}
})
export class CustomValueAccessor {
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
}
这setUpModelChangePipeline将允许AbstractControlDirective将模型与视图连接起来。这意味着每次调用时,其中注册的所有回调函数也会被调用,以便根据新模型的值更新视图。FormControl.setValue()FormControl
请注意,我说的是所有回调函数。这是因为多个回调函数AbstractControlDirective可以使用同一个FormControl实例。
// Inside `FormControl`
_onChange: Function[] = [];
registerOnChange(fn: Function): void { this._onChange.push(fn); }
// FormControl.setValue
setValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
(this as{value: any}).value = this._pendingValue = value;
if (this._onChange.length && options.emitModelToViewChange !== false) {
this._onChange.forEach(
(changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
}
this.updateValueAndValidity(options); // Update ancestors
}
举个例子:
<form>
<input type="radio" ngModel name="genre" value="horror">
<input type="radio" ngModel name="genre" value="comedy">
</form>
该函数setUpControl(control, dir)将被调用两次,每次对应一个实例ngModel。但是,每次调用时,参数control(一个FormControl实例)都将相同。这意味着该函数control.onChanges将包含两个回调函数,每个实例对应一个回调函数ControlValueAccessor(<input type="radio">该实例绑定RadioControlValueAccessor到该回调函数)。
顺便提一下,它ControlValueAccessor.registerOnTouched遵循与以下相同的原则ControlValueAccessor.registerOnChange:
// Called inside `setUpControl`
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnTouched(() => {
control._pendingTouched = true;
if (control.updateOn === 'blur' && control._pendingChange) updateControl(control, dir);
if (control.updateOn !== 'submit') control.markAsTouched();
});
}
这样,每当视图中发生模糊事件时,模型都会得到更新。
模板驱动表单和响应式表单
这两种策略都非常有效,但依我之见,Reactive Forms它们在处理复杂、动态的逻辑时尤其方便。
模板驱动表单
使用这种策略时,表单构建的大部分逻辑都在视图内部执行。这意味着AbstractControl树状结构是在构建视图的同时创建的 。
以下是我们在采用这种模板驱动方法时可以使用的工具:
export const TEMPLATE_DRIVEN_DIRECTIVES: Type<any>[] =
[NgModel, NgModelGroup, NgForm];
吴恩模型
这是一个基于表单控件的指令,它将视图层与模型层连接起来,反之亦然。它还会将视图层注册到模型树中。FormControlFormControlAbstractControl
使用此指令时,您还可以指定一些选项:
@Input('ngModelOptions')
options !: {name?: string, standalone?: boolean, updateOn?: 'change' | 'blur' | 'submit'};
如果您想使用独立 FormControl实例,可以按照以下方法操作:
<form #f="ngForm">
<input [ngModelOptions]="{ standalone: true }" #myNgModel="ngModel" name="name" ngModel type="text">
</form>
{{ myNgModel.value }}
<br>
{{ f.value | json }}
Ng模型组
NgModel提供了一种对多个指令进行分组的方法。在模型层,这由一个非顶层NgModelGroup实例表示。 它还会将指令注册到树中。 FormGroupFormGroupAbstractControl
<form> <!-- `NgForm` - automatically bound to `<form>` -->
<input type="text" ngModel name="companyName"/>
<div ngModelGroup="personal">
<input type="text" ngModel name="name"/>
<div ngModelGroup="address">
<input type="text" ngModel name="city"/>
<input type="text" ngModel name="street" />
</div>
</div>
</form>
第一个出现的NgModelGroup必须是以下对象的子对象NgForm:
<!-- Valid -->
<form>
<ng-container #myGrp="ngModelGroup" ngModelGroup="address">
<input type="text"ngModel name="city" />
<input type="text" ngModel name="street">
</ng-container>
</form>
<!-- Invalid: `No provider for ControlContainer ...` -->
<div #myGrp="ngModelGroup" ngModelGroup="address">
<input type="text"ngModel name="city" />
<input type="text" ngModel name="street">
</div>
NgForm
它将多个指令分组在一起NgModel。在模型层,它由一个顶级NgModelGroup实例表示,因此它可以监听表单特定的事件,例如 ` add` 和 `add` 。此外,它还会自动绑定到标签。resetsubmit<form>
在模型中,这是树FormGroup的根实例AbstractControl。
<form> <!-- NgForm -->
<input ngModel name="companyName" type="text"> <!-- NgModel -->
<div ngModelGroup="address"> <!-- NgModelGroup -->
<input ngModel name="city" type="text"> <!-- NgModel -->
<input ngModel name="street" type="text"> <!-- NgModel -->
</div>
</form>
反应形式
与此相反Template Driven Forms,当使用表单时,视图构建过程中已经创建了Reactive Forms表单。
以下是我们采用这种被动式方法时可以使用的工具:
export const REACTIVE_DRIVEN_DIRECTIVES: Type<any>[] =
[FormControlDirective, FormGroupDirective, FormControlName, FormGroupName, FormArrayName];
表单控件指令
它是一种基于表单控件的指令,是视图和模型这两个主要层之间的桥梁。
它接收一个FormControl实例,[formControl]="formControlInstance"该实例已经与现有树同步,因为formControlInstance它已经是现有AbstractControl树的一部分。因此,这里重要的是使用值访问器formControlInstance将绑定到当前DOM 元素。
如果您想使用独立 FormControl实例,可以按照以下方法操作:
<input #f="ngForm" [formControl]="formControlInstance" type="text">
{{ f.value }}
表单组指令
在模型层,它是一个顶级FormGroup实例(<form [formGroup]="formGroupInstance">)。这也意味着它会监听表单特定的事件,例如reset和submit。它是已构建formGroupInstance树的根。 AbstractControl
窗体控件名称
它接收一个字符串作为参数,其任务是根据提供的控件名称和在视图中的位置[formControlName]="nameOfFormControlInstance"来确定实例。如果根据路径找不到实例,则会抛出错误。FormControlnameOfFormControlInstanceFormControl
因此,它nameOfFormControlInstance必须是一个有效的名称,因为它依赖于表单容器才能将其正确添加FormControl到AbstractControl树中。
如前所述,路径是根据DOM 元素(或自定义组件)的位置推断出来的nameOfFormControlInstance:
// control - is, in this case, the top level `FormGroup` instance
function _find(control: AbstractControl, path: Array<string|number>| string, delimiter: string) {
if (path == null) return null;
if (!(path instanceof Array)) {
path = (<string>path).split(delimiter);
}
if (path instanceof Array && (path.length === 0)) return null;
return (<Array<string|number>>path).reduce((v: AbstractControl | null, name) => {
if (v instanceof FormGroup) {
return v.controls.hasOwnProperty(name as string) ? v.controls[name] : null;
}
if (v instanceof FormArray) {
return v.at(<number>name) || null;
}
return null;
}, control);
}
<form [formGroup]="myFormGroup">
<!-- path: 'name' -->
<input formControlName="name" type="text">
<!-- path: 'address' -->
<ng-container formGroupName="address">
<!-- path: ['address', 'city'] -->
<input formControlName="city" type="text">
<!-- path: ['address', 'street'] -->
<input formControlName="street" type="text">
</ng-container>
</form>
每条指令的路径就是这样确定的:
export function controlPath(name: string, parent: ControlContainer): string[] {
return [...parent.path !, name];
}
值得一提的是,该属性nameOfFormControlInstance不能动态更改。一旦FormControl添加,就无法自动更改。(示例)
原因如下:
@Directive({selector: '[formControlName]', providers: [controlNameBinding]})
export class FormControlName extends NgControl implements OnChanges, OnDestroy {
/* ... */
ngOnChanges(changes: SimpleChanges) {
if (!this._added) this._setUpControl();
}
private _setUpControl() {
this._checkParentType();
// formDirective - points to the top-level `FormGroup` instance
(this as{control: FormControl}).control = this.formDirective.addControl(this);
if (this.control.disabled && this.valueAccessor !.setDisabledState) {
this.valueAccessor !.setDisabledState !(true);
}
this._added = true;
}
/* ... */
}
但是,如果您仍然希望在更改FormControl时更改实例nameOfFormControlInstance,可以使用以下方法:
{FormArray|FormGroup}.setControl(ctrlName, formControlInstance)
表单组名称
它接收一个字符串作为参数[formGroupName]="nameOfFormGroupInstance",并根据该参数找到正确的FormGroup实例。
它不能用作顶级表单控件容器,必须在现有控件中注册FormGroupDirective。
假设你有一个如下所示的表单:
const address = this.fb.group({
street: this.fb.control(''),
});
this.form = this.fb.group({
name: this.fb.control(''),
address,
});
在视图中写入此内容将导致错误Cannot find control with name: 'street':
<form #f="ngForm" [formGroup]="form">
<input formControlName="name" type="text">
<input formControlName="street" type="text">
</form>
解决此问题的办法是使用FormGroupName指令来创建子组,以便视图与模型相关联。
<form #f="ngForm" [formGroup]="form">
<input formControlName="name" type="text">
<ng-container formGroupName="address">
<input formControlName="street" type="text">
</ng-container>
</form>
{{ f.value | json }}
注意:当使用FormControlDirective( [formControl]="formControlInstance") 时,这并非必要,因为FormControlDirective无需查找FormControl实例,因为它已经通过 接收一个实例formControlInstance。
表单数组名称
与 相同FormGroupName,只是它必须FormArray在树中找到一个已存在的实例AbstractControl。
this.fooForm = this.fb.group({
movies: this.fb.array([
this.fb.control('action'),
this.fb.control('horror'),
this.fb.control('mistery'),
]),
});
<form #f="ngForm" [formGroup]="fooForm">
<ng-container formArrayName="movies">
<input
*ngFor="let _ of fooForm.controls['movies'].controls; let idx = index;"
[formControlName]="idx"
type="text"
>
</ng-container>
</form>
{{ f.value | json }}
验证者
AbstractControl验证器允许开发人员对实例施加约束( FormControl,,FormArray)FormGroup。
验证器会在树初始化时设置并运行AbstractControl。如果想在初始化之后设置验证器,可以使用 `setValidator()`AbstractFormControl.setValidators和AbstractFormControl.setAsyncValidators`runValidator()` 来设置它们,并使用 `runValidator() AbstractFormControl.updateValueAndValidity` 来运行它们。
setValidators(newValidator: ValidatorFn|ValidatorFn[]|null): void {
this.validator = coerceToValidator(newValidator);
}
updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
/* ... */
if (this.enabled) {
this._cancelExistingSubscription();
// Run sync validators
// and will invoke `this.validator`
(this as{errors: ValidationErrors | null}).errors = this._runValidator();
// If `errors` property is not null -> status = 'INVALID'
(this as{status: string}).status = this._calculateStatus();
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator(opts.emitEvent);
}
}
/* ... */
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
}
从上面的代码片段中我们还可以看出,如果同步验证器返回错误,异步验证器将不会运行。
内置验证器的使用
内置验证器可以作为指令使用,也可以作为类的静态成员Validator使用。
例如,可以直接在视图中使用电子邮件验证器,如下所示:
<form>
<input email ngModel name="email" type="text">
</form>
@Directive({
selector: '[email][formControlName],[email][formControl],[email][ngModel]',
providers: [EMAIL_VALIDATOR]
})
export class EmailValidator implements Validator {
/* ... */
validate(control: AbstractControl): ValidationErrors|null {
return this._enabled ? Validators.email(control) : null;
}
/* ... */
}
而如果Reactive Forms你这样使用:
this.form = new FormGroup({
name: new FormControl(defaultValue, [Validators.Email])
})
虽然Reactive Forms通常在组件类中设置验证器,但您仍然可以在视图中提供验证器;AbstractControl创建实例时,验证器最终会被合并到视图中。setUpControl
// dir.validator - sync validators provided via directives(eg: `<input email type="text">`)
// control.validator - sync validators provided through `Reactive Forms`(eg: new FormControl('', [syncValidators]))
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
control.validator = Validators.compose([control.validator !, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
/* ... */
}
验证者的组成
验证器可以从多个来源提供:可以是视图中的验证器,也可以是类中的验证器,或者两者都有。
所有验证器最终都将合并到一个函数中,该函数在被调用时将按顺序执行所有验证器并累加其结果(返回的错误)。
首先会对实现了该Validator接口的对象进行规范化,这意味着它们将被转换为一个函数,该函数在被调用时将执行该Validator.validate方法:
export function normalizeValidator(validator: ValidatorFn | Validator): ValidatorFn {
if ((<Validator>validator).validate) {
return (c: AbstractControl) => (<Validator>validator).validate(c);
} else {
return <ValidatorFn>validator;
}
}
验证器在函数内部设置并合并(如果需要)setUpControl:
export function setUpControl(control: FormControl, dir: NgControl): void {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwError(dir, 'No value accessor for form control with');
control.validator = Validators.compose([control.validator !, dir.validator]);
control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
/* ... */
}
让我们一起探索背后的奥秘Validators.compose:
export class Validators {
static compose(validators: (ValidatorFn|null|undefined)[]|null): ValidatorFn|null {
if (!validators) return null;
const presentValidators: ValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
return _mergeErrors(_executeValidators(control, presentValidators));
};
}
}
function _executeValidators(control: AbstractControl, validators: ValidatorFn[]): any[] {
return validators.map(v => v(control));
}
// Accumulate errors
function _mergeErrors(arrayOfErrors: ValidationErrors[]): ValidationErrors|null {
const res: {[key: string]: any} =
arrayOfErrors.reduce((res: ValidationErrors | null, errors: ValidationErrors | null) => {
return errors != null ? {...res !, ...errors} : res !;
}, {});
return Object.keys(res).length === 0 ? null : res;
}
同样的逻辑也适用于此Validator.composeAsync,区别在于验证器的执行方式。首先,它会将所有异步验证器转换为可观察对象,然后借助forkJoin运算符执行它们。
export class Validators {
static composeAsync(validators: (AsyncValidatorFn|null)[]): AsyncValidatorFn|null {
if (!validators) return null;
const presentValidators: AsyncValidatorFn[] = validators.filter(isPresent) as any;
if (presentValidators.length == 0) return null;
return function(control: AbstractControl) {
const observables = _executeAsyncValidators(control, presentValidators).map(toObservable);
return forkJoin(observables).pipe(map(_mergeErrors));
};
}
}
自定义验证器
创建自定义验证器的推荐方法是将其用作实现该接口的指令Validator:
// min-max-validator.directive.ts
@Directive({
selector: '[min-max-validator]',
providers: [
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MinMaxValidator),
multi: true,
}
]
})
export class MinMaxValidator implements Validator {
constructor() { }
validate (f: FormGroup): ValidationErrors | null {
if (f.pristine) {
return null;
}
const { min, max } = f.controls;
// `min` or `max` is not a number or is empty
if (min.invalid || max.invalid) {
return null;
}
if (+min.value >= +max.value) {
return { minGreaterMax: 'min cannot be greater than max!' };
}
return null;
}
}
<form #f="ngForm">
<ng-container min-max-validator ngModelGroup="price" #priceGrp="ngModelGroup">
<input type="text" ngModel name="min" pattern="^\d+$" required />
<input type="text" ngModel name="max" pattern="^\d+$" required >
</ng-container>
</form>
动态验证器
界面Validator如下所示:
export interface Validator {
validate(control: AbstractControl): ValidationErrors|null;
registerOnValidatorChange?(fn: () => void): void;
}
我们可以使用该方法registerOnValidatorChange注册一个回调函数,当验证器的输入发生变化时,该函数就会被调用。调用该回调函数可以确保您的AbstractControl实例与更新后的验证器保持一致。
例如:<input [required]="true">--><input [required]="false">
@Directive({
selector:
':not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]',
providers: [REQUIRED_VALIDATOR],
host: {'[attr.required]': 'required ? "" : null'}
})
export class RequiredValidator implements Validator {
set required(value: boolean|string) {
this._required = value != null && value !== false && `${value}` !== 'false';
if (this._onChange) this._onChange();
}
registerOnValidatorChange(fn: () => void): void { this._onChange = fn; }
}
export function setUpControl(control: FormControl, dir: NgControl): void {
/* ... */
// re-run validation when validator binding changes, e.g. minlength=3 -> minlength=4
dir._rawValidators.forEach((validator: Validator | ValidatorFn) => {
if ((<Validator>validator).registerOnValidatorChange)
(<Validator>validator).registerOnValidatorChange !(() => control.updateValueAndValidity());
});
dir._rawAsyncValidators.forEach((validator: AsyncValidator | AsyncValidatorFn) => {
if ((<Validator>validator).registerOnValidatorChange)
(<Validator>validator).registerOnValidatorChange !(() => control.updateValueAndValidity());
});
/* ... */
}
探索内置ControlValueAccessor功能
以下是Angular提供的内置值访问器:
const BUILTIN_ACCESSORS = [
CheckboxControlValueAccessor,
RangeValueAccessor,
NumberValueAccessor,
SelectControlValueAccessor,
SelectMultipleControlValueAccessor,
RadioControlValueAccessor,
];
在接下来的章节中,我们将探讨一些内置值访问器的内部结构。
SelectValueAccessor
我们可以通过两种方式使用此值访问器:使用[value]或[ngValue]。
使用<option [value]="primitiveValue">
顾名思义,参数primitiveValue只能是原始值。如果您想绑定一个对象,[ngValue]则应该选择使用 `__init__` 参数。
每个<option>都会将其值设置为primitiveValue。
@Input('value')
set value(value: any) {
this._setElementValue(value);
}
_setElementValue(value: string): void {
this._renderer.setProperty(this._element.nativeElement, 'value', value);
}
使用<option [ngValue]="primitiveOrNonPrimitiveValue">
与不同[value],[ngValue]可以接受原始类型和非原始类型作为参数。
<option>它将根据提供给 . 的值设置标签的值[ngValue]。
@Input('ngValue')
set ngValue(value: any) {
if (this._select == null) return;
this._select._optionMap.set(this.id, value);
this._setElementValue(_buildValueString(this.id, value));
this._select.writeValue(this._select.value);
}
/* ... */
function _buildValueString(id: string | null, value: any): string {
if (id == null) return `${value}`;
if (value && typeof value === 'object') value = 'Object';
return `${id}: ${value}`.slice(0, 50);
}
我们可以看到,如果传递一个对象,其值将类似于 `[] '1: Object'`。如果传递一个原始值,例如城市名称,其值将为:0: 'NY'
需要注意的是,当您<select>使用`setValue` 更改 `value` 的值时FormControl.setValue(arg),如果 ` argvalue` 是一个对象,则必须确保它与您传递给 `setValue` 的对象相同<option [ngValue]="arg"></option>。这是因为默认情况下,` SelectControlValueAccessor.writeValue(obj)setValue` 会使用 `value`===来标识选定的 `value` option。
writeValue(value: any): void {
this.value = value;
const id: string|null = this._getOptionId(value); // <---- Here!
if (id == null) {
this._renderer.setProperty(this._elementRef.nativeElement, 'selectedIndex', -1);
}
const valueString = _buildValueString(id, value);
this._renderer.setProperty(this._elementRef.nativeElement, 'value', valueString);
}
_getOptionId(value: any): string|null {
for (const id of Array.from(this._optionMap.keys())) {
if (this._compareWith(this._optionMap.get(id), value)) return id;
}
return null;
}
默认情况下,它_compareWith看起来像这样:
return a === b || typeof a === 'number' && typeof b === 'number' && isNaN(a) && isNaN(b);
以下是一个使用自定义_compareWith函数的 StackBlitz 示例:
compareWith(existing, toCheckAgainst) {
if (!toCheckAgainst) {
return false;
}
return existing.id === toCheckAgainst.id;
}
<!--
1) Try without '[compareWith]="compareWith"'
2) select another option(`B`, or `C`)
3) click `change`
You should not see the value updated inside the `<select>`
and that is because the default impl. of `compareWith` will compare the values with `===`
-->
<select
#s="ngModel"
[ngModel]="selectedItem"
[compareWith]="compareWith"
>
<option
*ngFor="let item of items"
[ngValue]="item"
>
{{item.name}}
</option>
</select>
<br><br>
<button (click)="s.control.setValue({ id: '1', name: 'A' })">change</button>
以下是此类行为的测试用例。
SelectMultipleValueAccessor
每个选项都会被跟踪(添加到内部_optionMap属性中),因为
- 当发生更改事件时,值访问器需要向模型提供正确的值(提供给或的
<select>值);这可以通过遍历选定的选项并从中检索它们的值来实现。[value][ngValue]<option>event.target.selectedOptions_optionMap
// _ - the select element
this.onChange = (_: any) => {
const selected: Array<any> = [];
if (_.hasOwnProperty('selectedOptions')) {
const options: HTMLCollection = _.selectedOptions;
for (let i = 0; i < options.length; i++) {
const opt: any = options.item(i);
const val: any = this._getOptionValue(opt.value);
selected.push(val);
}
}
this.value = selected;
fn(selected);
};
FormControl当元素绑定的值<select>以编程方式更改时FormControl.setValue(),需要以某种方式确定哪些现有选项与新提供的值匹配。
writeValue(value: any): void {
this.value = value;
let optionSelectedStateSetter: (opt: ɵNgSelectMultipleOption, o: any) => void;
if (Array.isArray(value)) {
// convert values to ids
const ids = value.map((v) => this._getOptionId(v));
optionSelectedStateSetter = (opt, o) => { opt._setSelected(ids.indexOf(o.toString()) > -1); };
} else {
optionSelectedStateSetter = (opt, o) => { opt._setSelected(false); };
}
this._optionMap.forEach(optionSelectedStateSetter);
}
RadioValueAccessor
此值访问器借助内部服务跟踪单选按钮:RadioControlRegistry,该服务保存一个键值对数组[NgControl, RadioValueAccessor],其中是提供程序令牌,映射到NgControl基于表单控件的指令之一:NgModel,,FormControl。FormControlName
让我们看看它实际是如何运作的:
@Injectable()
export class RadioControlRegistry {
private _accessors: any[] = [];
add(control: NgControl, accessor: RadioControlValueAccessor) {
this._accessors.push([control, accessor]);
}
remove(accessor: RadioControlValueAccessor) {
for (let i = this._accessors.length - 1; i >= 0; --i) {
if (this._accessors[i][1] === accessor) {
this._accessors.splice(i, 1);
return;
}
}
}
select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
c[1].fireUncheck(accessor.value);
}
});
}
private _isSameGroup(
controlPair: [NgControl, RadioControlValueAccessor],
accessor: RadioControlValueAccessor): boolean {
if (!controlPair[0].control) return false;
return controlPair[0]._parent === accessor._control._parent &&
controlPair[1].name === accessor.name;
}
}
密切关注RadioControlRegistry._isSameGroup方法。
让我们用一个更简单的例子来缩小范围:
<form>
<input ngModel name="option" value="value1" type="radio"> <!-- #1 NgModel._parent = the top-level `FormGroup` which results from `<form>` -->
<ng-container ngModelGroup="foo">
<input ngModel name="option" value="value1" type="radio"> <!-- #2 NgModel._parent = the sub-group `FormGroup` which results from `ngModelGroup` -->
</ng-container>
</form>
请注意,两个单选按钮的值相同!
数组RadioControlRegistry._accessors结构如下:
[
NgControl(-> NgModel) /* #1 */, RadioControlValueAccessor,
NgControl(-> NgModel) /* #2 */, RadioControlValueAccessor,
]
当用户点击第一个RadioControlRegistry单选按钮时,将执行以下方法:
select(accessor: RadioControlValueAccessor) {
this._accessors.forEach((c) => {
if (this._isSameGroup(c, accessor) && c[1] !== accessor) {
c[1].fireUncheck(accessor.value);
}
});
}
第一个单选按钮对应的元素accessor在哪里?RadioControlValueAccessor
方法如下_isSameGroup:
private _isSameGroup(
controlPair: [NgControl, RadioControlValueAccessor],
accessor: RadioControlValueAccessor): boolean {
if (!controlPair[0].control) return false;
return controlPair[0]._parent === accessor._control._parent &&
controlPair[1].name === accessor.name;
}
controlPair[0]._parent === accessor._control._parent正是这个原因防止了第一个单选按钮影响第二个单选按钮。
以下列示例为例,如果我们点击第二个按钮,第一个按钮将被标记为已选中。
<form>
<input ngModel name="option" value="value1" type="radio">
<input ngModel name="option" value="value1" type="radio">
</form>
这是因为在N具有相同属性的单选按钮name中value,只能选中一个。在本例中,是最后一个满足这些条件的按钮:
this._isSameGroup(c, accessor) && c[1] !== accessor
选中的单选按钮accessor在哪里?RadioControlValueAccessor
更好地了解这AbstractControl棵树
在整篇文章中,你可能已经注意到“AbstractControl树”这个词。记住,这AbstractControl是一个抽象类,它的具体实现是 ` Tree` FormControl、` Tree`FormGroup和 `Tree` FormArray。
为了使事物更直观,我们可以将它们之间的联系可视化为树状结构。
例如,这个
new FormGroup({
name: new FormControl(''),
address: new FormGroup({
city: new FormControl(''),
street: new FormControl(''),
}),
});
可以形象地描述如下:
FG
/ \
FC FG
/ \
FC FC
利用上面的图表,我们将了解AbstractControl诸如reset()、submit()、等常见操作如何改变树状结构markAsDirty()。
我建议您在继续阅读之前先阅读基础实体部分。
_pendingDirty,,_pendingValue_pendingChange
这些类的私有属性AbstractControl是一些你可能无需关注的细节。然而,它们对AbstractControl树的有效性起着至关重要的作用。
这些属性是在上下文中遇到的,FormControl因为它们的值取决于从视图(从视图)发送的值ControlValueAccessor。
_pendingChange
此属性指示用户是否已更改FormControl's 值。
假设你有一个<input ngModel name="name" type="text">输入框,用户在其中输入内容。一旦发生这种情况,ControlValueAccessor输入框的onChange函数就会被调用。分配给输入框的函数onChange如下所示:
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
control._pendingChange = true标记用户已与该对象进行过明显的交互<input>。
这到底有什么用呢?因为你可以设置AbstractControl更新自身的事件(默认值为change)。
你可以通过属性查看更新策略:_updateOn_updateOn: 'change'|'blur'|'submit';
考虑到这一点,如果FormControl更新策略设置为blur,并且blur事件发生在视图中,而用户没有在视图中输入任何内容,会发生什么情况<input>?在这种情况下,_pendingChange可以防止对树进行冗余遍历。
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnTouched(() => {
/* ... */
if (control.updateOn === 'blur' && control._pendingChange) updateControl(control, dir);
/* ... */
});
}
如果用户在文本框中输入了任何内容<input>,则文本框的值control._pendingChange将被设置为 true true。因此,当失去焦点事件发生时,文本框FormControl及其父元素都会被更新。
_pendingDirty
如果用户在用户界面中更改了其值,FormControl则考虑A。dirty
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
/* ... */
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
/* ... */
}
当 UI 的值发生变化时,(位于视图层dir.valueAccessor !.registerOnChange(cb)中的)将调用在 with 中注册的回调函数。ControlValueAccessor
实现AbstractControl.markedAsDirty方式如下:
markAsDirty(opts: {onlySelf?: boolean} = {}): void {
(this as{pristine: boolean}).pristine = false;
if (this._parent && !opts.onlySelf) {
this._parent.markAsDirty(opts);
}
}
因此,如果 aFormControl被标记为脏(由于 UI 更改),则其祖先也会相应更新(在这种情况下,它们将被标记为脏)。
FG (3)
/ \
FC FG (2)
/ \
FC FC (1)
(1).parent = (2)
(2).parent = (3)
(3).parent = null(root)
假设(1)绑定FormControl到某个元素,<input>并且用户刚刚输入了该元素,则上述方法将从updateControl函数中调用:control.markAsDirty(),其中control是(1)。这将向上传播到根节点,顺序如下:(1) -> (2) -> (3)。因此,整棵树将被标记为脏的!
还可以选择仅标记(1)为脏污:(1).markedAsDirty({ onlySelf: true })。
_pendingDirty现在你可能会想,如果控件的脏状态会在用户输入内容后立即改变,那么还有什么必要呢?这是因为默认策略是change,但它可以更改为其他策略,例如blur或submit。
例如,当视图中发生模糊事件时,会发生以下情况:
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnTouched(() => {
/* ... */
if (control.updateOn === 'blur' && control._pendingChange) updateControl(control, dir);
/* ... */
});
}
_pendingValue
你可以把这处房产看作是某某事物的最新FormControl价值。
ControlValueAccessor.onChange它的值在调用时设置,ControlValueAccessor.onChange调用方式如下:
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
/* ... */
if (control.updateOn === 'change') updateControl(control, dir);
});
}
function updateControl(control: FormControl, dir: NgControl): void {
if (control._pendingDirty) control.markAsDirty();
control.setValue(control._pendingValue, {emitModelToViewChange: false});
dir.viewToModelUpdate(control._pendingValue);
control._pendingChange = false;
}
然而,_pendingValue` $ v_var value..._pendingValuevalueAbstractControlvalue_pendingValueFormControlchange
例如,如果FormControl's 更新策略设置为submit,则在提交事件发生之前,模型的值(FormControl.value)不会等于_pendingValue(这是反映视图的值)。
AbstractControl.setValue()和AbstractControl.patchValue()
// {FormGroup|FormArray}.setValue
setValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
this._checkAllValuesPresent(value);
Object.keys(value).forEach(name => {
this._throwIfControlMissing(name);
this.controls[name].setValue(value[name], {onlySelf: true, emitEvent: options.emitEvent});
});
this.updateValueAndValidity(options);
}
// {FormGroup|FormArray}.patchValue
patchValue(value: {[key: string]: any}, options: {onlySelf?: boolean, emitEvent?: boolean} = {}):
void {
Object.keys(value).forEach(name => {
if (this.controls[name]) {
this.controls[name].patchValue(value[name], {onlySelf: true, emitEvent: options.emitEvent});
}
});
this.updateValueAndValidity(options);
}
AbstractControl.setValue将要求您为所有现有控件提供值,而将允许您为任何现有控件提供值。AbstractControl.patchValue
{FormGroup|FormArray}.setValue首先会检查你是否提供了一个包含所有现有控件的对象,然后会检查你是否提供了任何冗余控件(不在现有控件范围内的控件)。
调用setValue`/`时patchValue,如果` AbstractControlis` 为真FormControl,则会先更新FormControl实例,然后更新其祖先实例;否则,会先更新其后代实例,然后更新其祖先实例。
{ onlySelf: true }可以通过将参数作为第二个参数传递来避免更新祖先。
以下是第一个例子:
const fg = new FormGroup({
name: new FormControl(''),
address: new FormGroup({
city: new FormControl(''),
street: new FormControl(''),
}),
});
FG (4)
/ \
FC FG (3) - address
/ \
FC FC
(1) (2)
表演后
fg.get('address').setValue({ city: 'city', street: 'street' })
它将首先更新(1)和(2),然后更新其容器的值和有效性(3),最后更新其祖先。
patchValue例子
const c = new FormControl('');
const c2 = new FormControl('');
const a = new FormArray([c, c2]);
a.patchValue(['andrei']);
console.log(a.value) // ['andrei', '']
setValue例子
const c1 = new FormControl('c1');
const c2 = new FormControl('c2');
const a = new FormArray([c1, c2]);
a.setValue(['c1-updated', 'c2-updated', 'c3']); // Error: Cannot find form control at index 2
a.setValue(['c1-updated']); // Error: Must supply a value for form control at index: 1
a.setValue(['c1-updated', 'c2-updated']);
console.log(a.value); // ["c1-updated", "c2-updated"]
AbstractControl提交后树状图会发生什么变化?
注意:只有FormGroupDirective可以NgForm打电话onSubmit。
onSubmit($event) {
(this as{submitted: boolean}).submitted = true;
syncPendingControls(this.form, this.directives);
this.ngSubmit.emit($event);
return false;
}
某些AbstractControl实例可能对该选项进行了updateOn不同的设置。因此,如果某个实例FormControl将该updateOn选项设置为“否submit”,则表示其值和UI 状态(例如,状态dirty、untouched状态等)仅在事件发生时才会更新submit。这就是该syncPendingControls()选项的作用。
// FormControl
_syncPendingControls(): boolean {
if (this.updateOn === 'submit') {
if (this._pendingDirty) this.markAsDirty();
if (this._pendingTouched) this.markAsTouched();
if (this._pendingChange) {
this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false});
return true;
}
}
return false;
}
// FormArray - FormGroup works in a very similar fashion
_syncPendingControls(): boolean {
let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => {
return child._syncPendingControls() ? true : updated;
}, false);
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
return subtreeUpdated;
}
请看这个例子:
this.form = this.fb.group({ name: this.fb.control('', { updateOn: 'submit' }) });
this.form.valueChanges.subscribe(console.warn);
拥有这样的视野
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input [formControl]="form.get('name')" type="text">
<button type="submit">Submit</button>
</form>
you get the same values every time the submit event occurs, whereas with this view
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="name" type="text">
<br><br>
<button type="submit">Submit</button>
</form>
you get the values only once, when the submit event occurs
That's because of the way FormControlName directives work inside a FormGroupDirective. A FormGroupDirective will keep track of FormControlName directives with the help of directives property. When the submit event occurs, each FormControlName will set the _pendingChange property of their bound FormControl to false.
directives.forEach(dir => {
const control = dir.control as FormControl;
if (control.updateOn === 'submit' && control._pendingChange) {
/* ... */
control._pendingChange = false;
}
});
FormControl._pendingChange is set to true every time the change event occurs in the UI.
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}
You can find more about _pendingChange here.
Retrieving AbstractControls from the tree
const fg = new FormGroup({
name: new FormControl(''),
address: new FormGroup({
city: new FormControl(''),
street: new FormControl(''),
}),
});
There are a couple of ways to retrieve an AbstractControl.
If the AbstractControl you want to retrieve is a direct descendant of a form control container(fg in this case), you can do this:
fg.controls[nameOfCtrl];
// In our example
fg.controls['name']
fg.controls['address']
However, if the AbstractControl is a few levels deep, you might find it annoying to write such things:
fg.controls['address'].controls['city']
You can use the AbstractControl.get() method instead
fg.get('address.city')
// Or
fg.get(['address', 'street'])
AbstractControl.get() will internally call a function _find which will traverse the tree downwards based on the path provided.
function _find(control: AbstractControl, path: Array<string|number>| string, delimiter: string) {
if (path == null) return null;
if (!(path instanceof Array)) {
path = (<string>path).split(delimiter);
}
if (path instanceof Array && (path.length === 0)) return null;
return (<Array<string|number>>path).reduce((v: AbstractControl | null, name) => {
if (v instanceof FormGroup) {
return v.controls.hasOwnProperty(name as string) ? v.controls[name] : null;
}
if (v instanceof FormArray) {
return v.at(<number>name) || null;
}
return null;
}, control);
}
As you might have noticed, if fg had been a FormArray instance, you could've retrieved its descendants by specifying an index, as opposed to a property name(like you'd do with FormGroup)
fg.get('1.city');
// Or
fg.get(['1', 'city']);
AbstractControl.updateValueAndValidity()
updateValueAndValidity(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
this._setInitialStatus();
this._updateValue();
if (this.enabled) {
this._cancelExistingSubscription();
(this as{errors: ValidationErrors | null}).errors = this._runValidator(); // Sync validators
(this as{status: string}).status = this._calculateStatus(); // VALID | INVALID | PENDING | DISABLED
if (this.status === VALID || this.status === PENDING) {
this._runAsyncValidator(opts.emitEvent);
}
}
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
}
}
As shown above, this method is responsible for multiple things:
1) updating the current AbstractControl's value
2) running validators(sync & async)
3) calculating status based on what validators return
4) emitting the new value and the new status to the subscribers(unless emitEvent = false)
5) repeating 1-4 for the parent(unless onlySelf = true)
const fg = new FormGroup({
name: new FormControl(''),
address: new FormGroup({
city: new FormControl(''),
street: new FormControl(''),
}),
});
FG (3)
/ \
FC FG (2)
/ \
FC FC (1)
(1) - fg.get('address.street')
(2) - fg.get('address')
(3) - fg
As soon as you do (1).setValue('new value'), (1).updateValueAndValidity() will be invoked.
setValue(value: any, options: {
onlySelf?: boolean,
emitEvent?: boolean,
emitModelToViewChange?: boolean,
emitViewToModelChange?: boolean
} = {}): void {
(this as{value: any}).value = this._pendingValue = value;
if (this._onChange.length && options.emitModelToViewChange !== false) {
this._onChange.forEach(
(changeFn) => changeFn(this.value, options.emitViewToModelChange !== false));
}
this.updateValueAndValidity(options);
}
After (1) has been updated, (2) will be updated and so on.. until the root is reached.
Disabling/enabling AbstractControls
An AbstractControl can be disabled/enabled from the model. The change can be seen in the view with the help of ControlValueAccessor.setDisabledState:
export function setUpControl(control: FormControl, dir: NgControl): void {
/* ... */
if (dir.valueAccessor !.setDisabledState) {
control.registerOnDisabledChange(
(isDisabled: boolean) => { dir.valueAccessor !.setDisabledState !(isDisabled); });
}
/* ... */
}
When disabling an AbstractControl instance you can choose not to update its ancestors by using this.control.disable({ onlySelf: true }). This might be the case when a FormControl might be part of the a FormGroup and because of this control being invalid, the entire FormGroup is marked as invalid.
const fg = this.fb.group({
name: this.fb.control('', Validators.required),
age: '',
city: this.fb.control('', Validators.required)
});
fg.controls['name'].disable();
fg.controls['city'].disable({ onlySelf: true });
console.log(fg.valid) // false
Had we omitted { onlySelf: true }, the entire form group(fg) would've been valid(fg.valid === true).
disable(opts: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
// If parent has been marked artificially dirty we don't want to re-calculate the
// parent's dirtiness based on the children.
const skipPristineCheck = this._parentMarkedDirty(opts.onlySelf);
(this as{status: string}).status = DISABLED;
(this as{errors: ValidationErrors | null}).errors = null;
this._forEachChild(
(control: AbstractControl) => { control.disable({...opts, onlySelf: true}); });
this._updateValue();
if (opts.emitEvent !== false) {
(this.valueChanges as EventEmitter<any>).emit(this.value);
(this.statusChanges as EventEmitter<string>).emit(this.status);
}
// Will update the value, validity, dirtiness, and touch status
this._updateAncestors({...opts, skipPristineCheck});
this._onDisabledChange.forEach((changeFn) => changeFn(true));
}
private _updateAncestors(
opts: {onlySelf?: boolean, emitEvent?: boolean, skipPristineCheck?: boolean}) {
if (this._parent && !opts.onlySelf) {
this._parent.updateValueAndValidity(opts);
if (!opts.skipPristineCheck) {
this._parent._updatePristine();
}
this._parent._updateTouched();
}
}
When an AbstractControl is disabled, its validators won't run and its errors will be marked as null and its children are also going to be disabled.
If a parent has been marked artificially dirty(dirtiness is not determined by its children: manually doing {FormGroup|FormArray}.markAsDirty), there is no need to recalculate the parent's dirtiness based on the children because they don't have any effect on the parent:
this.form = this.fb.group({
name: this.fb.control({ value: 'andrei', disabled: false }),
age: this.fb.control(''),
});
const nameCtrl = this.form.controls['name'];
// Now, its ancestors will be marked as dirty as well
// In this case, there is only one `FormGroup`(this.form)
nameCtrl.markAsDirty();
nameCtrl.disable();
// Now, `this.form` will be marked as `pristine`, because
// the child that influenced the parent's dirtiness is disabled
Also, if a form-control-container(FormGroup or FormArray) is disabled, its value will the value collected from all its descendants, regardless of their disabled value:
const g = new FormGroup({
name: new FormControl('name'),
address: new FormGroup({
city: new FormControl('city'),
street: new FormControl('street'),
}),
});
g.get('address.city').disable();
g.controls['name'].disable();
console.log(g.value);
/*
{
"address": {
"street": "street"
}
}
*/
g.disable();
console.log(g.value)
/*
{
"name": "name",
"address": {
"city": "city",
"address": "address"
}
}
The reason behind this is the way AbstractControl.disable() works. Starting from the current AbstractControl it will first disable all its descendants, then collect their value. For example, here is how a FormArray would accumulate the values from its descendants:
_updateValue(): void {
(this as{value: any}).value =
this.controls.filter((control) => control.enabled || this.disabled)
.map((control) => control.value);
}
The control.enabled || this.disabled expression allows us to get the value, even though the child control might be disabled.
However, if the container is not disabled and the child control is, its value won't be taken into account.
If you still want to get the form value, including the disabled controls you can use {FormGroup|FormArray}.getRawValue():
// FormArray.getRawValue()
getRawValue(): any[] {
return this.controls.map((control: AbstractControl) => {
return control instanceof FormControl ? control.value : (<any>control).getRawValue();
});
}
How are CSS classes added depending on AbstractControl's status ?
CSS classes(ng-valid, ng-pristine, ng-touched etc) are added with the help of NgControlStatus directive, which is automatically bound to a form control element when using ngModel, formControl, formControlName.
Additionally, NgControlStatusGroup is added to the form group(<form>, formGroupName, formGroup, ngModelGroup, formArrayName).
Both NgControlStatus and NgControlStatusGroup will be updated when change detection occurs.
export class AbstractControlStatus {
private _cd: AbstractControlDirective;
constructor(cd: AbstractControlDirective) { this._cd = cd; }
get ngClassUntouched(): boolean { return this._cd.control ? this._cd.control.untouched : false; }
get ngClassTouched(): boolean { return this._cd.control ? this._cd.control.touched : false; }
get ngClassPristine(): boolean { return this._cd.control ? this._cd.control.pristine : false; }
get ngClassDirty(): boolean { return this._cd.control ? this._cd.control.dirty : false; }
get ngClassValid(): boolean { return this._cd.control ? this._cd.control.valid : false; }
get ngClassInvalid(): boolean { return this._cd.control ? this._cd.control.invalid : false; }
get ngClassPending(): boolean { return this._cd.control ? this._cd.control.pending : false; }
}
export const ngControlStatusHost = {
'[class.ng-untouched]': 'ngClassUntouched',
'[class.ng-touched]': 'ngClassTouched',
'[class.ng-pristine]': 'ngClassPristine',
'[class.ng-dirty]': 'ngClassDirty',
'[class.ng-valid]': 'ngClassValid',
'[class.ng-invalid]': 'ngClassInvalid',
'[class.ng-pending]': 'ngClassPending',
};
@Directive({selector: '[formControlName],[ngModel],[formControl]', host: ngControlStatusHost})
export class NgControlStatus extends AbstractControlStatus {
constructor(@Self() cd: NgControl) { super(cd); }
}
With that in mind you can add your custom css class depending on form control's(or form-control-container's) validity or user interaction status by using a custom directive
constructor (private ngControlStatus: NgControlStatus) { }
@HostBinding('[class.card__price--incorrect]') this.ngControlStatus.ngClassInvalid();
Note: in order for this to work, your element(or component), besides the above directive, must include one of these form-control-based directives: [formControlName],[ngModel],[formControl]
Conclusion
I hope this article has clarified some concepts and emphasized how powerful this package can be.
Thanks for reading!
Photo by O12 on Unsplash.
文章来源:https://dev.to/angular/a-thorough-exploration-of-angular-forms-21pi