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 #18408, #18514, and the [design doc](https://docs.google.com/document/d/1dlJjRXYeuHRygryK0XoFrZNqW86jH4wobftCFyYa1PA/edit#heading=h.r6gn0i8f19wz).
This commit is contained in:
parent
cce2ab2625
commit
1cfa79ca4e
|
@ -16,7 +16,7 @@ import {Form} from './form_interface';
|
|||
import {NgControl} from './ng_control';
|
||||
import {NgModel} from './ng_model';
|
||||
import {NgModelGroup} from './ng_model_group';
|
||||
import {composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from './shared';
|
||||
import {composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from './shared';
|
||||
|
||||
export const formDirectiveProvider: any = {
|
||||
provide: ControlContainer,
|
||||
|
@ -65,6 +65,7 @@ const resolvedPromise = Promise.resolve(null);
|
|||
})
|
||||
export class NgForm extends ControlContainer implements Form {
|
||||
private _submitted: boolean = false;
|
||||
private _directives: NgModel[] = [];
|
||||
|
||||
form: FormGroup;
|
||||
ngSubmit = new EventEmitter();
|
||||
|
@ -93,6 +94,7 @@ export class NgForm extends ControlContainer implements Form {
|
|||
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
|
||||
setUpControl(dir.control, dir);
|
||||
dir.control.updateValueAndValidity({emitEvent: false});
|
||||
this._directives.push(dir);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -104,6 +106,7 @@ export class NgForm extends ControlContainer implements Form {
|
|||
if (container) {
|
||||
container.removeControl(dir.name);
|
||||
}
|
||||
removeDir<NgModel>(this._directives, dir);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -139,6 +142,7 @@ export class NgForm extends ControlContainer implements Form {
|
|||
|
||||
onSubmit($event: Event): boolean {
|
||||
this._submitted = true;
|
||||
syncPendingControls(this.form, this._directives);
|
||||
this.ngSubmit.emit($event);
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {Directive, EventEmitter, Host, Inject, Input, OnChanges, OnDestroy, Optional, Output, Self, SimpleChanges, forwardRef} from '@angular/core';
|
||||
|
||||
import {FormControl} from '../model';
|
||||
import {FormControl, FormHooks} from '../model';
|
||||
import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
|
||||
|
||||
import {AbstractFormGroupDirective} from './abstract_form_group_directive';
|
||||
|
@ -119,7 +119,45 @@ export class NgModel extends NgControl implements OnChanges,
|
|||
@Input() name: string;
|
||||
@Input('disabled') isDisabled: boolean;
|
||||
@Input('ngModel') model: any;
|
||||
@Input('ngModelOptions') options: {name?: string, standalone?: boolean};
|
||||
|
||||
/**
|
||||
* Options object for this `ngModel` instance. You can configure the following properties:
|
||||
*
|
||||
* **name**: An alternative to setting the name attribute on the form control element.
|
||||
* Sometimes, especially with custom form components, the name attribute might be used
|
||||
* as an `@Input` property for a different purpose. In cases like these, you can configure
|
||||
* the `ngModel` name through this option.
|
||||
*
|
||||
* ```html
|
||||
* <form>
|
||||
* <my-person-control name="Nancy" ngModel [ngModelOptions]="{name: 'user'}">
|
||||
* </my-person-control>
|
||||
* </form>
|
||||
* <!-- form value: {user: ''} -->
|
||||
* ```
|
||||
*
|
||||
* **standalone**: Defaults to false. If this is set to true, the `ngModel` will not
|
||||
* register itself with its parent form, and will act as if it's not in the form. This
|
||||
* can be handy if you have form meta-controls, a.k.a. form elements nested in
|
||||
* the `<form>` tag that control the display of the form, but don't contain form data.
|
||||
*
|
||||
* ```html
|
||||
* <form>
|
||||
* <input name="login" ngModel placeholder="Login">
|
||||
* <input type="checkbox" ngModel [ngModelOptions]="{standalone: true}"> Show more options?
|
||||
* </form>
|
||||
* <!-- form value: {login: ''} -->
|
||||
* ```
|
||||
*
|
||||
* **updateOn**: Defaults to `'change'`. Defines the event upon which the form control
|
||||
* value and validity will update. Also accepts `'blur'` and `'submit'`.
|
||||
*
|
||||
* ```html
|
||||
* <input [(ngModel)]="firstName" [ngModelOptions]="{updateOn: 'blur'}">
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
@Input('ngModelOptions') options: {name?: string, standalone?: boolean, updateOn?: FormHooks};
|
||||
|
||||
@Output('ngModelChange') update = new EventEmitter();
|
||||
|
||||
|
@ -170,11 +208,18 @@ export class NgModel extends NgControl implements OnChanges,
|
|||
}
|
||||
|
||||
private _setUpControl(): void {
|
||||
this._setUpdateStrategy();
|
||||
this._isStandalone() ? this._setUpStandalone() :
|
||||
this.formDirective.addControl(this);
|
||||
this._registered = true;
|
||||
}
|
||||
|
||||
private _setUpdateStrategy(): void {
|
||||
if (this.options && this.options.updateOn != null) {
|
||||
this._control._updateOn = this.options.updateOn;
|
||||
}
|
||||
}
|
||||
|
||||
private _isStandalone(): boolean {
|
||||
return !this._parent || !!(this.options && this.options.standalone);
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import {NG_ASYNC_VALIDATORS, NG_VALIDATORS, Validators} from '../../validators';
|
|||
import {ControlContainer} from '../control_container';
|
||||
import {Form} from '../form_interface';
|
||||
import {ReactiveErrors} from '../reactive_errors';
|
||||
import {cleanUpControl, composeAsyncValidators, composeValidators, setUpControl, setUpFormContainer} from '../shared';
|
||||
import {cleanUpControl, composeAsyncValidators, composeValidators, removeDir, setUpControl, setUpFormContainer, syncPendingControls} from '../shared';
|
||||
|
||||
import {FormControlName} from './form_control_name';
|
||||
import {FormArrayName, FormGroupName} from './form_group_name';
|
||||
|
@ -105,7 +105,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
|
||||
getControl(dir: FormControlName): FormControl { return <FormControl>this.form.get(dir.path); }
|
||||
|
||||
removeControl(dir: FormControlName): void { remove(this.directives, dir); }
|
||||
removeControl(dir: FormControlName): void { removeDir<FormControlName>(this.directives, dir); }
|
||||
|
||||
addFormGroup(dir: FormGroupName): void {
|
||||
const ctrl: any = this.form.get(dir.path);
|
||||
|
@ -134,7 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
|
||||
onSubmit($event: Event): boolean {
|
||||
this._submitted = true;
|
||||
this._syncPendingControls();
|
||||
syncPendingControls(this.form, this.directives);
|
||||
this.ngSubmit.emit($event);
|
||||
return false;
|
||||
}
|
||||
|
@ -146,15 +146,6 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
this._submitted = false;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_syncPendingControls() {
|
||||
this.form._syncPendingControls();
|
||||
this.directives.forEach(dir => {
|
||||
if (dir.control.updateOn === 'submit') {
|
||||
dir.viewToModelUpdate(dir.control._pendingValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_updateDomValue() {
|
||||
|
@ -190,10 +181,3 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function remove<T>(list: T[], el: T): void {
|
||||
const index = list.indexOf(el);
|
||||
if (index > -1) {
|
||||
list.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,6 +167,16 @@ export function isBuiltInAccessor(valueAccessor: ControlValueAccessor): boolean
|
|||
return BUILTIN_ACCESSORS.some(a => valueAccessor.constructor === a);
|
||||
}
|
||||
|
||||
export function syncPendingControls(form: FormGroup, directives: NgControl[]): void {
|
||||
form._syncPendingControls();
|
||||
directives.forEach(dir => {
|
||||
const control = dir.control as FormControl;
|
||||
if (control.updateOn === 'submit') {
|
||||
dir.viewToModelUpdate(control._pendingValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: vsavkin remove it once https://github.com/angular/angular/issues/3011 is implemented
|
||||
export function selectValueAccessor(
|
||||
dir: NgControl, valueAccessors: ControlValueAccessor[]): ControlValueAccessor|null {
|
||||
|
@ -198,3 +208,8 @@ export function selectValueAccessor(
|
|||
_throwError(dir, 'No valid value accessor for form control with');
|
||||
return null;
|
||||
}
|
||||
|
||||
export function removeDir<T>(list: T[], el: T): void {
|
||||
const index = list.indexOf(el);
|
||||
if (index > -1) list.splice(index, 1);
|
||||
}
|
|
@ -819,6 +819,22 @@ export function main() {
|
|||
expect(control.dirty).toBe(true, 'Expected control to update dirty state when blurred.');
|
||||
});
|
||||
|
||||
it('should update touched when control is blurred', () => {
|
||||
const fixture = initTest(FormControlComp);
|
||||
const control = new FormControl('', {updateOn: 'blur'});
|
||||
fixture.componentInstance.control = control;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.touched).toBe(false, 'Expected control to start out untouched.');
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(control.touched)
|
||||
.toBe(true, 'Expected control to update touched state when blurred.');
|
||||
});
|
||||
|
||||
it('should continue waiting for blur to update if previously blurred', () => {
|
||||
const fixture = initTest(FormControlComp);
|
||||
const control =
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
|
||||
import {Component, Directive, Type, forwardRef} from '@angular/core';
|
||||
import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing';
|
||||
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormsModule, NG_ASYNC_VALIDATORS, NgForm} from '@angular/forms';
|
||||
import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm} from '@angular/forms';
|
||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util';
|
||||
import {merge} from 'rxjs/observable/merge';
|
||||
|
||||
import {NgModelCustomComp, NgModelCustomWrapper} from './value_accessor_integration_spec';
|
||||
|
||||
export function main() {
|
||||
|
@ -280,6 +282,492 @@ export function main() {
|
|||
}));
|
||||
});
|
||||
|
||||
describe('updateOn', () => {
|
||||
|
||||
describe('blur', () => {
|
||||
|
||||
it('should default updateOn to change', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
const name = form.control.get('name') as FormControl;
|
||||
expect(name._updateOn).toBeUndefined();
|
||||
expect(name.updateOn).toEqual('change');
|
||||
}));
|
||||
|
||||
|
||||
it('should set control updateOn to blur properly', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
const name = form.control.get('name') as FormControl;
|
||||
expect(name._updateOn).toEqual('blur');
|
||||
expect(name.updateOn).toEqual('blur');
|
||||
}));
|
||||
|
||||
it('should always set value and validity on init', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Nancy Drew';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(input.value).toEqual('Nancy Drew', 'Expected initial view value to be set.');
|
||||
expect(form.value)
|
||||
.toEqual({name: 'Nancy Drew'}, 'Expected initial control value be set.');
|
||||
expect(form.valid).toBe(true, 'Expected validation to run on initial value.');
|
||||
}));
|
||||
|
||||
it('should always set value programmatically right away', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Nancy Drew';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(input.value)
|
||||
.toEqual('Carson', 'Expected view value to update on programmatic change.');
|
||||
expect(form.value)
|
||||
.toEqual(
|
||||
{name: 'Carson'}, 'Expected form value to update on programmatic change.');
|
||||
expect(form.valid)
|
||||
.toBe(false, 'Expected validation to run immediately on programmatic change.');
|
||||
}));
|
||||
|
||||
it('should update value/validity on blur', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Carson', 'Expected value not to update on input.');
|
||||
expect(form.valid).toBe(false, 'Expected validation not to run on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Nancy Drew', 'Expected value to update on blur.');
|
||||
expect(form.valid).toBe(true, 'Expected validation to run on blur.');
|
||||
}));
|
||||
|
||||
it('should wait for second blur to update value/validity again', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
input.value = 'Carson';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Nancy Drew', 'Expected value not to update until another blur.');
|
||||
expect(form.valid).toBe(true, 'Expected validation not to run until another blur.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Carson', 'Expected value to update on second blur.');
|
||||
expect(form.valid).toBe(false, 'Expected validation to run on second blur.');
|
||||
}));
|
||||
|
||||
it('should not update dirtiness until blur', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(form.dirty).toBe(false, 'Expected dirtiness not to update on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.dirty).toBe(true, 'Expected dirtiness to update on blur.');
|
||||
}));
|
||||
|
||||
it('should not update touched until blur', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(form.touched).toBe(false, 'Expected touched not to update on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.touched).toBe(true, 'Expected touched to update on blur.');
|
||||
}));
|
||||
|
||||
it('should not emit valueChanges or statusChanges until blur', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'blur'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const values: string[] = [];
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
const sub = merge(form.valueChanges !, form.statusChanges !)
|
||||
.subscribe(val => values.push(val));
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(values).toEqual(
|
||||
[{name: 'Nancy Drew'}, 'VALID'],
|
||||
'Expected valueChanges and statusChanges on blur.');
|
||||
|
||||
sub.unsubscribe();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('submit', () => {
|
||||
|
||||
it('should set control updateOn to submit properly', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
const name = form.control.get('name') as FormControl;
|
||||
expect(name._updateOn).toEqual('submit');
|
||||
expect(name.updateOn).toEqual('submit');
|
||||
}));
|
||||
|
||||
it('should always set value and validity on init', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Nancy Drew';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(input.value).toEqual('Nancy Drew', 'Expected initial view value to be set.');
|
||||
expect(form.value)
|
||||
.toEqual({name: 'Nancy Drew'}, 'Expected initial control value be set.');
|
||||
expect(form.valid).toBe(true, 'Expected validation to run on initial value.');
|
||||
}));
|
||||
|
||||
it('should always set value programmatically right away', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Nancy Drew';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(input.value)
|
||||
.toEqual('Carson', 'Expected view value to update on programmatic change.');
|
||||
expect(form.value)
|
||||
.toEqual(
|
||||
{name: 'Carson'}, 'Expected form value to update on programmatic change.');
|
||||
expect(form.valid)
|
||||
.toBe(false, 'Expected validation to run immediately on programmatic change.');
|
||||
}));
|
||||
|
||||
|
||||
it('should update on submit', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Carson', 'Expected value not to update on input.');
|
||||
expect(form.valid).toBe(false, 'Expected validation not to run on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Carson', 'Expected value not to update on blur.');
|
||||
expect(form.valid).toBe(false, 'Expected validation not to run on blur.');
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Nancy Drew', 'Expected value to update on submit.');
|
||||
expect(form.valid).toBe(true, 'Expected validation to run on submit.');
|
||||
}));
|
||||
|
||||
it('should wait until second submit to update again', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Carson';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
input.value = 'Carson';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Nancy Drew', 'Expected value not to update until second submit.');
|
||||
expect(form.valid).toBe(true, 'Expected validation not to run until second submit.');
|
||||
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual('Carson', 'Expected value to update on second submit.');
|
||||
expect(form.valid).toBe(false, 'Expected validation to run on second submit.');
|
||||
}));
|
||||
|
||||
it('should not run validation for onChange controls on submit', fakeAsync(() => {
|
||||
const validatorSpy = jasmine.createSpy('validator');
|
||||
const groupValidatorSpy = jasmine.createSpy('groupValidatorSpy');
|
||||
|
||||
const fixture = initTest(NgModelGroupForm);
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
form.control.get('name') !.setValidators(groupValidatorSpy);
|
||||
form.control.get('name.last') !.setValidators(validatorSpy);
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(validatorSpy).not.toHaveBeenCalled();
|
||||
expect(groupValidatorSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should not update dirtiness until submit', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(form.dirty).toBe(false, 'Expected dirtiness not to update on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(form.dirty).toBe(false, 'Expected dirtiness not to update on blur.');
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.dirty).toBe(true, 'Expected dirtiness to update on submit.');
|
||||
}));
|
||||
|
||||
it('should not update touched until submit', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
expect(form.touched).toBe(false, 'Expected touched not to update on blur.');
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.touched).toBe(true, 'Expected touched to update on submit.');
|
||||
}));
|
||||
|
||||
it('should reset properly', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = 'Nancy';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
form.resetForm();
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(input.value).toEqual('', 'Expected view value to reset.');
|
||||
expect(form.value).toEqual({name: null}, 'Expected form value to reset.');
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual(null, 'Expected ngModel value to reset.');
|
||||
expect(form.dirty).toBe(false, 'Expected dirty to stay false on reset.');
|
||||
expect(form.touched).toBe(false, 'Expected touched to stay false on reset.');
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(form.value)
|
||||
.toEqual({name: null}, 'Expected form value to stay empty on submit');
|
||||
expect(fixture.componentInstance.name)
|
||||
.toEqual(null, 'Expected ngModel value to stay empty on submit.');
|
||||
expect(form.dirty).toBe(false, 'Expected dirty to stay false on submit.');
|
||||
expect(form.touched).toBe(false, 'Expected touched to stay false on submit.');
|
||||
}));
|
||||
|
||||
it('should not emit valueChanges or statusChanges until submit', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
fixture.componentInstance.name = '';
|
||||
fixture.componentInstance.options = {updateOn: 'submit'};
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
const values: string[] = [];
|
||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
||||
|
||||
const sub = merge(form.valueChanges !, form.statusChanges !)
|
||||
.subscribe(val => values.push(val));
|
||||
|
||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||
input.value = 'Nancy Drew';
|
||||
dispatchEvent(input, 'input');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input.');
|
||||
|
||||
dispatchEvent(input, 'blur');
|
||||
fixture.detectChanges();
|
||||
tick();
|
||||
|
||||
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on blur.');
|
||||
|
||||
const formEl = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||
dispatchEvent(formEl, 'submit');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(values).toEqual(
|
||||
[{name: 'Nancy Drew'}, 'VALID'],
|
||||
'Expected valueChanges and statusChanges on submit.');
|
||||
sub.unsubscribe();
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('submit and reset events', () => {
|
||||
it('should emit ngSubmit event with the original submit event on submit', fakeAsync(() => {
|
||||
const fixture = initTest(NgModelForm);
|
||||
|
@ -914,7 +1402,7 @@ class NgModelNativeValidateForm {
|
|||
<input name="first" [(ngModel)]="first" required [disabled]="isDisabled">
|
||||
<input name="last" [(ngModel)]="last">
|
||||
</div>
|
||||
<input name="email" [(ngModel)]="email">
|
||||
<input name="email" [(ngModel)]="email" [ngModelOptions]="options">
|
||||
</form>
|
||||
`
|
||||
})
|
||||
|
@ -923,6 +1411,7 @@ class NgModelGroupForm {
|
|||
last: string;
|
||||
email: string;
|
||||
isDisabled: boolean;
|
||||
options = {updateOn: 'change'};
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -425,6 +425,7 @@ export declare class NgModel extends NgControl implements OnChanges, OnDestroy {
|
|||
options: {
|
||||
name?: string;
|
||||
standalone?: boolean;
|
||||
updateOn?: FormHooks;
|
||||
};
|
||||
readonly path: string[];
|
||||
update: EventEmitter<{}>;
|
||||
|
|
Loading…
Reference in New Issue