diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 6f69007d69dcd..6841cc389d447 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -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 = 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(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; } diff --git a/packages/forms/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index 83552d916fde6..82764121b806b 100644 --- a/packages/forms/src/directives/ng_model.ts +++ b/packages/forms/src/directives/ng_model.ts @@ -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 + *
+ * + * + *
+ * + * ``` + * + * **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 `
` tag that control the display of the form, but don't contain form data. + * + * ```html + * + * + * Show more options? + *
+ * + * ``` + * + * **updateOn**: Defaults to `'change'`. Defines the event upon which the form control + * value and validity will update. Also accepts `'blur'` and `'submit'`. + * + * ```html + * + * ``` + * + */ + @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); } diff --git a/packages/forms/src/directives/reactive_directives/form_group_directive.ts b/packages/forms/src/directives/reactive_directives/form_group_directive.ts index 8598d083ae6c1..4162cbef8d3b9 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -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 this.form.get(dir.path); } - removeControl(dir: FormControlName): void { remove(this.directives, dir); } + removeControl(dir: FormControlName): void { removeDir(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(list: T[], el: T): void { - const index = list.indexOf(el); - if (index > -1) { - list.splice(index, 1); - } -} diff --git a/packages/forms/src/directives/shared.ts b/packages/forms/src/directives/shared.ts index 50651fe55ec5a..1f66036a998ec 100644 --- a/packages/forms/src/directives/shared.ts +++ b/packages/forms/src/directives/shared.ts @@ -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(list: T[], el: T): void { + const index = list.indexOf(el); + if (index > -1) list.splice(index, 1); +} \ No newline at end of file diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index a37ea8f45873b..22ba1848f4c5b 100644 --- a/packages/forms/test/reactive_integration_spec.ts +++ b/packages/forms/test/reactive_integration_spec.ts @@ -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 = diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index 6436350d85706..916b146fcb1d7 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -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 { - + ` }) @@ -923,6 +1411,7 @@ class NgModelGroupForm { last: string; email: string; isDisabled: boolean; + options = {updateOn: 'change'}; } @Component({ diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index 2ce54bfc295d1..43d9f00700d61 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -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<{}>;