feat(forms): add updateOn blur option to FormControls (#18408)
By default, the value and validation status of a `FormControl` updates
whenever its value changes. If an application has heavy validation
requirements, updating on every text change can sometimes be too expensive.
This commit introduces a new option that improves performance by delaying
form control updates until the "blur" event. To use it, set the `updateOn`
option to `blur` when instantiating the `FormControl`.
```ts
// example without validators
const c = new FormControl(, { updateOn: blur });
// example with validators
const c= new FormControl(, {
validators: Validators.required,
updateOn: blur
});
```
Like in AngularJS, setting `updateOn` to `blur` will delay the update of
the value as well as the validation status. Updating value and validity
together keeps the system easy to reason about, as the two will always be
in sync. It's also worth noting that the value/validation pipeline does
still run when the form is initialized (in order to support initial values).
Closes #7113
This commit is contained in:
@@ -38,23 +38,10 @@ export function setUpControl(control: FormControl, dir: NgControl): void {
|
||||
control.asyncValidator = Validators.composeAsync([control.asyncValidator !, dir.asyncValidator]);
|
||||
dir.valueAccessor !.writeValue(control.value);
|
||||
|
||||
// view -> model
|
||||
dir.valueAccessor !.registerOnChange((newValue: any) => {
|
||||
dir.viewToModelUpdate(newValue);
|
||||
control.markAsDirty();
|
||||
control.setValue(newValue, {emitModelToViewChange: false});
|
||||
});
|
||||
setUpViewChangePipeline(control, dir);
|
||||
setUpModelChangePipeline(control, dir);
|
||||
|
||||
// touched
|
||||
dir.valueAccessor !.registerOnTouched(() => control.markAsTouched());
|
||||
|
||||
control.registerOnChange((newValue: any, emitModelEvent: boolean) => {
|
||||
// control -> view
|
||||
dir.valueAccessor !.writeValue(newValue);
|
||||
|
||||
// control -> ngModel
|
||||
if (emitModelEvent) dir.viewToModelUpdate(newValue);
|
||||
});
|
||||
setUpBlurPipeline(control, dir);
|
||||
|
||||
if (dir.valueAccessor !.setDisabledState) {
|
||||
control.registerOnDisabledChange(
|
||||
@@ -92,6 +79,40 @@ export function cleanUpControl(control: FormControl, dir: NgControl) {
|
||||
if (control) control._clearChangeFns();
|
||||
}
|
||||
|
||||
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
|
||||
dir.valueAccessor !.registerOnChange((newValue: any) => {
|
||||
control._pendingValue = newValue;
|
||||
control._pendingDirty = true;
|
||||
|
||||
if (control._updateOn === 'change') {
|
||||
dir.viewToModelUpdate(newValue);
|
||||
control.markAsDirty();
|
||||
control.setValue(newValue, {emitModelToViewChange: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
|
||||
dir.valueAccessor !.registerOnTouched(() => {
|
||||
if (control._updateOn === 'blur') {
|
||||
dir.viewToModelUpdate(control._pendingValue);
|
||||
if (control._pendingDirty) control.markAsDirty();
|
||||
control.setValue(control._pendingValue, {emitModelToViewChange: false});
|
||||
}
|
||||
control.markAsTouched();
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
export function setUpFormContainer(
|
||||
control: FormGroup | FormArray, dir: AbstractFormGroupDirective | FormArrayName) {
|
||||
if (control == null) _throwError(dir, 'Cannot find control with');
|
||||
|
||||
@@ -78,11 +78,15 @@ function coerceToAsyncValidator(
|
||||
origAsyncValidator || null;
|
||||
}
|
||||
|
||||
export type FormHooks = 'change' | 'blur';
|
||||
|
||||
export interface AbstractControlOptions {
|
||||
validators?: ValidatorFn|ValidatorFn[]|null;
|
||||
asyncValidators?: AsyncValidatorFn|AsyncValidatorFn[]|null;
|
||||
updateOn?: FormHooks;
|
||||
}
|
||||
|
||||
|
||||
function isOptionsObj(
|
||||
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null): boolean {
|
||||
return validatorOrOpts != null && !Array.isArray(validatorOrOpts) &&
|
||||
@@ -659,6 +663,15 @@ export abstract class AbstractControl {
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* The options object can also be used to define when the control should update.
|
||||
* By default, the value and validity of a control updates whenever the value
|
||||
* changes. You can configure it to update on the blur event instead by setting
|
||||
* the `updateOn` option to `'blur'`.
|
||||
*
|
||||
* ```ts
|
||||
* const c = new FormControl('', { updateOn: 'blur' });
|
||||
* ```
|
||||
*
|
||||
* See its superclass, {@link AbstractControl}, for more properties and methods.
|
||||
*
|
||||
* * **npm package**: `@angular/forms`
|
||||
@@ -669,6 +682,15 @@ export class FormControl extends AbstractControl {
|
||||
/** @internal */
|
||||
_onChange: Function[] = [];
|
||||
|
||||
/** @internal */
|
||||
_updateOn: FormHooks = 'change';
|
||||
|
||||
/** @internal */
|
||||
_pendingValue: any;
|
||||
|
||||
/** @internal */
|
||||
_pendingDirty: boolean;
|
||||
|
||||
constructor(
|
||||
formState: any = null,
|
||||
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
||||
@@ -677,6 +699,7 @@ export class FormControl extends AbstractControl {
|
||||
coerceToValidator(validatorOrOpts),
|
||||
coerceToAsyncValidator(asyncValidator, validatorOrOpts));
|
||||
this._applyFormState(formState);
|
||||
this._setUpdateStrategy(validatorOrOpts);
|
||||
this.updateValueAndValidity({onlySelf: true, emitEvent: false});
|
||||
this._initObservables();
|
||||
}
|
||||
@@ -704,7 +727,7 @@ export class FormControl extends AbstractControl {
|
||||
emitModelToViewChange?: boolean,
|
||||
emitViewToModelChange?: boolean
|
||||
} = {}): void {
|
||||
this._value = value;
|
||||
this._value = this._pendingValue = value;
|
||||
if (this._onChange.length && options.emitModelToViewChange !== false) {
|
||||
this._onChange.forEach(
|
||||
(changeFn) => changeFn(this._value, options.emitViewToModelChange !== false));
|
||||
@@ -759,6 +782,7 @@ export class FormControl extends AbstractControl {
|
||||
reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
|
||||
this._applyFormState(formState);
|
||||
this.markAsPristine(options);
|
||||
this._pendingDirty = false;
|
||||
this.markAsUntouched(options);
|
||||
this.setValue(this._value, options);
|
||||
}
|
||||
@@ -806,11 +830,17 @@ export class FormControl extends AbstractControl {
|
||||
|
||||
private _applyFormState(formState: any) {
|
||||
if (this._isBoxedValue(formState)) {
|
||||
this._value = formState.value;
|
||||
this._value = this._pendingValue = formState.value;
|
||||
formState.disabled ? this.disable({onlySelf: true, emitEvent: false}) :
|
||||
this.enable({onlySelf: true, emitEvent: false});
|
||||
} else {
|
||||
this._value = formState;
|
||||
this._value = this._pendingValue = formState;
|
||||
}
|
||||
}
|
||||
|
||||
private _setUpdateStrategy(opts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null): void {
|
||||
if (isOptionsObj(opts) && (opts as AbstractControlOptions).updateOn != null) {
|
||||
this._updateOn = (opts as AbstractControlOptions).updateOn !;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user