Navigation Menu

Skip to content

Commit

Permalink
feat(forms): add updateOn blur option to FormControls (#18408)
Browse files Browse the repository at this point in the history
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 333a708
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 29 deletions.
53 changes: 37 additions & 16 deletions packages/forms/src/directives/shared.ts
Expand Up @@ -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(
Expand Down Expand Up @@ -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');
Expand Down
36 changes: 33 additions & 3 deletions packages/forms/src/model.ts
Expand Up @@ -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) &&
Expand Down Expand Up @@ -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`
Expand All @@ -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,
Expand All @@ -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();
}
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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 !;
}
}
}
Expand Down
20 changes: 20 additions & 0 deletions packages/forms/test/form_control_spec.ts
Expand Up @@ -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);
Expand Down

0 comments on commit 333a708

Please sign in to comment.