Skip to content

Commit 1cfa79c

Browse files
karahansl
authored andcommitted
feat(forms): add updateOn support to ngModelOptions
This commit introduces a new option to template-driven forms that improves performance by delaying form control updates until the "blur" or "submit" event. To use it, set the `updateOn` property in `ngModelOptions`. ```html <input ngModel [ngModelOptions]="{updateOn: blur}"> ``` Like in AngularJS, setting `updateOn` to `blur` or `submit` 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). Upcoming PRs will address: * Support for setting group-level `updateOn` in template-driven forms * Option for skipping initial validation run or more global error display configuration * Better support of reactive validation strategies See more context in angular#18408, angular#18514, and the [design doc](https://docs.google.com/document/d/1dlJjRXYeuHRygryK0XoFrZNqW86jH4wobftCFyYa1PA/edit#heading=h.r6gn0i8f19wz).
1 parent cce2ab2 commit 1cfa79c

7 files changed

Lines changed: 578 additions & 24 deletions

File tree

packages/forms/src/directives/ng_form.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {Form} from './form_interface';
1616
import {NgControl} from './ng_control';
1717
import {NgModel} from './ng_model';
1818
import {NgModelGroup} from './ng_model_group';
19-
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from './shared';
19+
import {composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from './shared';
2020

2121
export const formDirectiveProvider: any = {
2222
provide: ControlContainer,
@@ -65,6 +65,7 @@ const resolvedPromise = Promise.resolve(null);
6565
})
6666
export class NgForm extends ControlContainer implements Form {
6767
private _submitted: boolean = false;
68+
private _directives: NgModel[] = [];
6869

6970
form: FormGroup;
7071
ngSubmit = new EventEmitter();
@@ -93,6 +94,7 @@ export class NgForm extends ControlContainer implements Form {
9394
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
9495
setUpControl(dir.control, dir);
9596
dir.control.updateValueAndValidity({emitEvent: false});
97+
this._directives.push(dir);
9698
});
9799
}
98100

@@ -104,6 +106,7 @@ export class NgForm extends ControlContainer implements Form {
104106
if (container) {
105107
container.removeControl(dir.name);
106108
}
109+
removeDir<NgModel>(this._directives, dir);
107110
});
108111
}
109112

@@ -139,6 +142,7 @@ export class NgForm extends ControlContainer implements Form {
139142

140143
onSubmit($event: Event): boolean {
141144
this._submitted = true;
145+
syncPendingControls(this.form, this._directives);
142146
this.ngSubmit.emit($event);
143147
return false;
144148
}

packages/forms/src/directives/ng_model.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
1010

11-
import {FormControl} from '../model';
11+
import {FormControl, FormHooks} from '../model';
1212
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
1313

1414
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
@@ -119,7 +119,45 @@ export class NgModel extends NgControl implements OnChanges,
119119
@Input() name: string;
120120
@Input('disabled') isDisabled: boolean;
121121
@Input('ngModel') model: any;
122-
@Input('ngModelOptions') options: {name?: string, standalone?: boolean};
122+
123+
/**
124+
* Options object for this `ngModel` instance. You can configure the following properties:
125+
*
126+
* **name**: An alternative to setting the name attribute on the form control element.
127+
* Sometimes, especially with custom form components, the name attribute might be used
128+
* as an `@Input` property for a different purpose. In cases like these, you can configure
129+
* the `ngModel` name through this option.
130+
*
131+
* ```html
132+
* <form>
133+
* <my-person-control name="Nancy" ngModel [ngModelOptions]="{name: 'user'}">
134+
* </my-person-control>
135+
* </form>
136+
* <!-- form value: {user: ''} -->
137+
* ```
138+
*
139+
* **standalone**: Defaults to false. If this is set to true, the `ngModel` will not
140+
* register itself with its parent form, and will act as if it's not in the form. This
141+
* can be handy if you have form meta-controls, a.k.a. form elements nested in
142+
* the `<form>` tag that control the display of the form, but don't contain form data.
143+
*
144+
* ```html
145+
* <form>
146+
* <input name="login" ngModel placeholder="Login">
147+
* <input type="checkbox" ngModel [ngModelOptions]="{standalone: true}"> Show more options?
148+
* </form>
149+
* <!-- form value: {login: ''} -->
150+
* ```
151+
*
152+
* **updateOn**: Defaults to `'change'`. Defines the event upon which the form control
153+
* value and validity will update. Also accepts `'blur'` and `'submit'`.
154+
*
155+
* ```html
156+
* <input [(ngModel)]="firstName" [ngModelOptions]="{updateOn: 'blur'}">
157+
* ```
158+
*
159+
*/
160+
@Input('ngModelOptions') options: {name?: string, standalone?: boolean, updateOn?: FormHooks};
123161

124162
@Output('ngModelChange') update = new EventEmitter();
125163

@@ -170,11 +208,18 @@ export class NgModel extends NgControl implements OnChanges,
170208
}
171209

172210
private _setUpControl(): void {
211+
this._setUpdateStrategy();
173212
this._isStandalone() ? this._setUpStandalone() :
174213
this.formDirective.addControl(this);
175214
this._registered = true;
176215
}
177216

217+
private _setUpdateStrategy(): void {
218+
if (this.options && this.options.updateOn != null) {
219+
this._control._updateOn = this.options.updateOn;
220+
}
221+
}
222+
178223
private _isStandalone(): boolean {
179224
return !this._parent || !!(this.options && this.options.standalone);
180225
}

packages/forms/src/directives/reactive_directives/form_group_directive.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
1212
import {ControlContainer} from '../control_container';
1313
import {Form} from '../form_interface';
1414
import {ReactiveErrors} from '../reactive_errors';
15-
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
15+
import {cleanUpControl, composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from '../shared';
1616

1717
import {FormControlName} from './form_control_name';
1818
import {FormArrayName, FormGroupName} from './form_group_name';
@@ -105,7 +105,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
105105

106106
getControl(dir: FormControlName): FormControl { return <FormControl>this.form.get(dir.path); }
107107

108-
removeControl(dir: FormControlName): void { remove(this.directives, dir); }
108+
removeControl(dir: FormControlName): void { removeDir<FormControlName>(this.directives, dir); }
109109

110110
addFormGroup(dir: FormGroupName): void {
111111
const ctrl: any = this.form.get(dir.path);
@@ -134,7 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
134134

135135
onSubmit($event: Event): boolean {
136136
this._submitted = true;
137-
this._syncPendingControls();
137+
syncPendingControls(this.form, this.directives);
138138
this.ngSubmit.emit($event);
139139
return false;
140140
}
@@ -146,15 +146,6 @@ export class FormGroupDirective extends ControlContainer implements Form,
146146
this._submitted = false;
147147
}
148148

149-
/** @internal */
150-
_syncPendingControls() {
151-
this.form._syncPendingControls();
152-
this.directives.forEach(dir => {
153-
if (dir.control.updateOn === 'submit') {
154-
dir.viewToModelUpdate(dir.control._pendingValue);
155-
}
156-
});
157-
}
158149

159150
/** @internal */
160151
_updateDomValue() {
@@ -190,10 +181,3 @@ export class FormGroupDirective extends ControlContainer implements Form,
190181
}
191182
}
192183
}
193-
194-
function remove<T>(list: T[], el: T): void {
195-
const index = list.indexOf(el);
196-
if (index > -1) {
197-
list.splice(index, 1);
198-
}
199-
}

packages/forms/src/directives/shared.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,16 @@ export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean
167167
return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);
168168
}
169169

170+
export function syncPendingControls(form: FormGroup, directives: NgControl[]): void {
171+
form._syncPendingControls();
172+
directives.forEach(dir => {
173+
const control = dir.control as FormControl;
174+
if (control.updateOn === 'submit') {
175+
dir.viewToModelUpdate(control._pendingValue);
176+
}
177+
});
178+
}
179+
170180
// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
171181
export function selectValueAccessor(
172182
dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor|null {
@@ -198,3 +208,8 @@ export function selectValueAccessor(
198208
_throwError(dir, 'No valid value accessor for form control with');
199209
return null;
200210
}
211+
212+
export function removeDir<T>(list: T[], el: T): void {
213+
const index = list.indexOf(el);
214+
if (index > -1) list.splice(index, 1);
215+
}

packages/forms/test/reactive_integration_spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,22 @@ export function main() {
819819
expect(control.dirty).toBe(true, 'Expected control to update dirty state when blurred.');
820820
});
821821

822+
it('should update touched when control is blurred', () => {
823+
const fixture = initTest(FormControlComp);
824+
const control = new FormControl('', {updateOn: 'blur'});
825+
fixture.componentInstance.control = control;
826+
fixture.detectChanges();
827+
828+
expect(control.touched).toBe(false, 'Expected control to start out untouched.');
829+
830+
const input = fixture.debugElement.query(By.css('input')).nativeElement;
831+
dispatchEvent(input, 'blur');
832+
fixture.detectChanges();
833+
834+
expect(control.touched)
835+
.toBe(true, 'Expected control to update touched state when blurred.');
836+
});
837+
822838
it('should continue waiting for blur to update if previously blurred', () => {
823839
const fixture = initTest(FormControlComp);
824840
const control =

0 commit comments

Comments
 (0)