Angular中的FormControl与ControlValueAccessor
什么是FormControl
通常我们会在Angular中使用FormControl
来管理表单控件,比如:
<input [formControl]="formControl">
formControl = new FormControl();
这其中[formControl]
是Angular提供的指令,用于将FormControl
与表单控件进行绑定,而FormControl
则用于管理表单控件的状态。
这也是我们最常见的formControl使用方法。接下来我们会分别介绍formControl
指令与FormControl
类。
FormControl类
FormControl类是对单一表单控件进行值追踪和状态校验的类。
通常我们会通过new FormControl()
来创建一个FormControl实例,并将其赋值给某个变量,然后通过[formControl]
指令进行绑定。
采用FormControl实例进行父子组件通信的方式通常通过 FormControl 自带的方法。
父组件更改子组件数据,通过setValue()
方法。
子组件更改父组件数据,可以通过valueChanges
这个Observable属性,但这个valueChanges在 emitEvent=false
将不会触发。
也可以通过registerOnChange()
方法来注册callback,但在emitModelToViewChange=false
时将不会触发。
formControl指令
formControl指令用于将FormControl实例与表单控件进行绑定,其作用类似于[(ngModel)]
指令。
@Directive({selector: '[formControl]', providers: [formControlBinding], exportAs: 'ngForm'})
export class FormControlDirective extends NgControl implements OnChanges, OnDestroy {}
从以上代码可以看出,formControl
指令继承自NgControl
,并实现了OnChanges
与OnDestroy
接口。
其selector是[formControl]
,这意味着其实formControl指令可以应用在任何带有[formControl]
指令的组件上,这也为自定义组件采用formControl提供了条件。
在FormControl类的介绍中我们知道FormControl类其实是单纯的值管理器,那他究竟是如何达到控制表单控件状态的效果呢?这就要引入另一个概念——ControlValueAccessor
。
export class FormControlDirective extends NgControl implements OnChanges, OnDestroy {
@Input('formControl') form!: FormControl;
constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) validators: (Validator|ValidatorFn)[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators:
(AsyncValidator|AsyncValidatorFn)[],
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[],
@Optional() @Inject(NG_MODEL_WITH_FORM_CONTROL_WARNING) private _ngModelWarningConfig: string|
null,
@Optional() @Inject(CALL_SET_DISABLED_STATE) private callSetDisabledState?:
SetDisabledStateOption) {
super();
this._setValidators(validators);
this._setAsyncValidators(asyncValidators);
this.valueAccessor = selectValueAccessor(this, valueAccessors);
}
}
在这段代码中我们看到,formControl指令通过注入器在本节点上查找NG_VALUE_ACCESSOR,如果找到,则通过某种策略选择其一将其赋值给valueAccessor属性。
此时我们已经通过指令同时获得了FormControl实例和valueAccessor实例,接下来就是关联两者了。
export class FormControlDirective extends NgControl implements OnChanges, OnDestroy {
ngOnChanges(changes: SimpleChanges): void {
if (this._isControlChanged(changes)) {
const previousForm = changes['form'].previousValue;
if (previousForm) {
cleanUpControl(previousForm, this, /* validateControlPresenceOnChange */ false);
}
setUpControl(this.form, this, this.callSetDisabledState);
this.form.updateValueAndValidity({emitEvent: false});
}
if (isPropertyUpdated(changes, this.viewModel)) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
_ngModelWarning('formControl', FormControlDirective, this, this._ngModelWarningConfig);
}
this.form.setValue(this.model);
this.viewModel = this.model;
}
}
}
这段代码中有一个重要的实现setUpControl
,对二者进行了关联。以下为setUpControl
实现。
export function setUpControl(
control: FormControl, dir: NgControl,
callSetDisabledState: SetDisabledStateOption = setDisabledStateDefault): void {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
if (!control) _throwError(dir, 'Cannot find control with');
if (!dir.valueAccessor) _throwMissingValueAccessorError(dir);
}
setUpValidators(control, dir);
// 此处立即将FormControl实例的值赋值给了valueAccessor的writeValue方法
dir.valueAccessor!.writeValue(control.value);
// The legacy behavior only calls the CVA's `setDisabledState` if the control is disabled.
// If the `callSetDisabledState` option is set to `always`, then this bug is fixed and
// the method is always called.
if (control.disabled || callSetDisabledState === 'always') {
dir.valueAccessor!.setDisabledState?.(control.disabled);
}
// 这里定义了当视图出现变化,也就是表单元素的值发生改变如何修改FormControl实例的值
setUpViewChangePipeline(control, dir);
// 这里定义了当FormControl实例的值发生了变化,如何修改扁担元素的值
setUpModelChangePipeline(control, dir);
setUpBlurPipeline(control, dir);
setUpDisabledChangeHandler(control, dir);
}
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);
});
}
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
const onChange = (newValue?: any, emitModelEvent?: boolean) => {
// control -> view
dir.valueAccessor!.writeValue(newValue);
// control -> ngModel
if (emitModelEvent) dir.viewToModelUpdate(newValue);
};
control.registerOnChange(onChange);
// Register a callback function to cleanup onChange handler
// from a control instance when a directive is destroyed.
dir._registerOnDestroy(() => {
control._unregisterOnChange(onChange);
});
}
什么是ControlValueAccessor
定义了一个接口,可以在Form API与原生控件之间建立桥接,比如<input>
、<select>
、<textarea>
、<checkbox>
。
interface ControlValueAccessor {
writeValue(obj: any): void
registerOnChange(fn: any): void
registerOnTouched(fn: any): void
setDisabledState(isDisabled: boolean)?: void
}
其中writeValue
和registerOnChange
是必须实现的。
writeValue
是用来把Form API中的值写入到原生控件中,registerOnChange
是用来把原生控件的值更新到Form API。
从上文中可以得知FormControl指令之所以可以和原生控件交互,是因为FormControl注入了ControlValueAccessor实例。
@angular/forms
中已经实现了多个ControlValueAccessor的实现,比如DefaultValueAccessor
、CheckboxControlValueAccessor
、SelectControlValueAccessor
等。
他们都各自实现了writeValue
和registerOnChange
。这也是我们为什么可以在<input>
、<select>
、<textarea>
、<checkbox>
等原生控件上直接使用[formControl]
指令的原因。
以下是DefaultValueAccessor
的实现:
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultValueAccessor),
multi: true
};
@Directive({
selector:
'input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]',
host: {
'(input)': '$any(this)._handleInput($event.target.value)',
'(blur)': 'onTouched()',
'(compositionstart)': '$any(this)._compositionStart()',
'(compositionend)': '$any(this)._compositionEnd($event.target.value)'
},
providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultValueAccessor extends BaseControlValueAccessor implements ControlValueAccessor {
/** Whether the user is creating a composition string (IME events). */
private _composing = false;
constructor(
renderer: Renderer2, elementRef: ElementRef,
@Optional() @Inject(COMPOSITION_BUFFER_MODE) private _compositionMode: boolean) {
super(renderer, elementRef);
if (this._compositionMode == null) {
this._compositionMode = !_isAndroid();
}
}
writeValue(value: any): void {
const normalizedValue = value == null ? '' : value;
this.setProperty('value', normalizedValue);
}
_handleInput(value: any): void {
if (!this._compositionMode || (this._compositionMode && !this._composing)) {
this.onChange(value);
}
}
_compositionStart(): void {
this._composing = true;
}
_compositionEnd(value: any): void {
this._composing = false;
this._compositionMode && this.onChange(value);
}
}
从以上的代码可以看出,我们通常使用的<input>
控件在应用了form模块下的几个指令时,也自动应用了DefaultValueAccessor这个指令。
并自动注入了DefaultValueAccessor作为NG_VALUE_ACCESSOR。
所以我们如果需要自定义控件,我们需要做的就是实现一个自定义的ControlValueAccessor
,并注入到我们自定义的控件中。