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,并实现了OnChangesOnDestroy接口。 其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
}

其中writeValueregisterOnChange是必须实现的。
writeValue是用来把Form API中的值写入到原生控件中,registerOnChange是用来把原生控件的值更新到Form API。

从上文中可以得知FormControl指令之所以可以和原生控件交互,是因为FormControl注入了ControlValueAccessor实例。
@angular/forms中已经实现了多个ControlValueAccessor的实现,比如DefaultValueAccessorCheckboxControlValueAccessorSelectControlValueAccessor等。
他们都各自实现了writeValueregisterOnChange。这也是我们为什么可以在<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,并注入到我们自定义的控件中。

© 2024 Hogan Hu