angular 表单
首先,只要你创建表单,Angular就会创建对应FormControl,无论是模板驱动表单还是响应式表单。模板驱动表单的FormControl是由NgModel指令隐性创建,而响应式表单是由你自己创建,通过FormControlName指令将Angular表单元素和原生表单元素进行绑定。
也就是说在Angular中的表单,不是原生表单,而是封装后的Angular表单。不仅仅是原生的表单控件可以处理封装成Angular表单,其他自定义的Angular组件也可以,只要定义了ControlValueAccessor。
ControlValueAccessor
那ControlValueAccessor是什么呢?它是原生元素和Angular表单之间的桥梁,将组件或者指令继承ControlValueAccessor的接口就能变成Angular表单使用了。
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState?(isDisabled: boolean): void
}
- writeValue(obj: any):该方法用于将模型中的新值写入视图或 DOM 属性中。
- registerOnChange(fn: any):设置当控件接收到 change 事件后,调用的函数
- registerOnTouched(fn: any):设置当控件接收到 touched 事件后,调用的函数
- setDisabledState?(isDisabled: boolean):当控件状态变成
DISABLED或从DISABLED状态变化成ENABLE状态时,会调用该函数。该函数会根据参数值,启用或禁用指定的 DOM 元素。
原生表单控件和Angular表单控件保持一致的原理
我们看下formControl指令的实现:
// packages/forms/src/directives/reactive_directives/form_control_directive.ts
export class FormControlDirective ... {
...
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
setUpControl(this.form, this);
formControl指令调用了setUpControl函数来实现formControl和ControlValueAccessor之间的交互。
// packages/forms/src/directives/shared.ts
export function setUpControl(control: FormControl, dir: NgControl) {
// 初始化原生表单控件
dir.valueAccessor.writeValue(control.value);
// 监听原生表单控件,将值同步给form control
dir.valueAccessor.registerOnChange((newValue: any) => {
...
control.setValue(newValue, {emitModelToViewChange: false});
});
// 反之,监听form control,将值同步给原生表单控件
control.registerOnChange((newValue: any, ...) => {
dir.valueAccessor.writeValue(newValue);
});
Demo
import {
Component,
Input,
OnChanges,
SimpleChanges,
forwardRef,
} from "@angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
Validator,
AbstractControl,
ValidatorFn,
ValidationErrors,
FormControl,
} from "@angular/forms";
export const EXE_COUNTER_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent),
multi: true,
};
export const EXE_COUNTER_VALIDATOR = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => CounterComponent),
multi: true,
};
export function createCounterRangeValidator(
maxValue: number,
minValue: number
) {
return (control: AbstractControl): ValidationErrors => {
return control.value > +maxValue || control.value < +minValue
? { rangeError: { current: control.value, max: maxValue, min: minValue } }
: null;
};
}
@Component({
selector: "exe-counter",
template: `
<div>
<p>当前值: {{ count }}</p>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
`,
providers: [EXE_COUNTER_VALUE_ACCESSOR, EXE_COUNTER_VALIDATOR],
})
export class CounterComponent
implements ControlValueAccessor, Validator, OnChanges
{
@Input() _count: number = 0;
get count() {
return this._count;
}
set count(value: number) {
this._count = value;
this.propagateChange(this._count);
}
propagateChange = (_: any) => {};
writeValue(value: any) {
if (value !== undefined) {
this.count = value;
}
}
registerOnChange(fn: any) {
this.propagateChange = fn;
}
registerOnTouched(fn: any) {}
increment() {
this.count++;
}
decrement() {
this.count--;
}
private _validator: ValidatorFn;
private _onChange: () => void;
@Input() counterRangeMin: number; // 设置数据有效范围的最大值
@Input() counterRangeMax: number; // 设置数据有效范围的最小值
// 监听输入属性变化,调用内部的_createValidator()方法,创建RangeValidator
ngOnChanges(changes: SimpleChanges): void {
if ("counterRangeMin" in changes || "counterRangeMax" in changes) {
this._createValidator();
}
}
// 动态创建RangeValidator
private _createValidator(): void {
this._validator = createCounterRangeValidator(
this.counterRangeMax,
this.counterRangeMin
);
}
// 执行控件验证
validate(c: AbstractControl): ValidationErrors | null {
return this.counterRangeMin == null || this.counterRangeMax == null
? null
: this._validator(c);
}
}
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
@Component({
selector: 'exe-app',
template: `
<form [formGroup]="form">
<exe-counter formControlName="counter"
counterRangeMin="5"
counterRangeMax="8">
</exe-counter>
</form>
<p *ngIf="!form.valid">Counter is invalid!</p>
<pre>{{ form.get('counter').errors | json }}</pre>
`,
})
export class AppComponent {
form: FormGroup;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.form = this.fb.group({
counter: 5
});
}
}