Skip to content
Permalink
Browse files

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
  • Loading branch information...
kara authored and vicb committed Aug 3, 2017
1 parent 3a227a1 commit 333a708bb632d4258ecb5fd4a0e86229fe9d26e4
@@ -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 !;
}
}
}
@@ -76,7 +76,27 @@ export function main() {

});

describe('updateOn', () => {

it('should default to on change', () => {
const c = new FormControl('');
expect(c._updateOn).toEqual('change');
});

it('should default to on change with an options obj', () => {
const c = new FormControl('', {validators: Validators.required});
expect(c._updateOn).toEqual('change');
});

it('should set updateOn when updating on blur', () => {
const c = new FormControl('', {updateOn: 'blur'});
expect(c._updateOn).toEqual('blur');
});

});

describe('validator', () => {

it('should run validator with the initial value', () => {
const c = new FormControl('value', Validators.required);
expect(c.valid).toEqual(true);
Oops, something went wrong.

0 comments on commit 333a708

Please sign in to comment.
You can’t perform that action at this time.