diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 6f69007d69dcde..b372fc70649004 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -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); } + remove(this._directives, dir); }); } @@ -139,6 +142,7 @@ export class NgForm extends ControlContainer implements Form { onSubmit($event: Event): boolean { this._submitted = true; + this._syncPendingControls(); this.ngSubmit.emit($event); return false; } @@ -150,9 +154,23 @@ export class NgForm extends ControlContainer implements Form { this._submitted = false; } + private _syncPendingControls(): void { + this.form._syncPendingControls(); + this._directives.forEach(dir => { + if (dir.control.updateOn === 'submit') { + dir.viewToModelUpdate(dir.control._pendingValue); + } + }); + } + /** @internal */ _findContainer(path: string[]): FormGroup { path.pop(); return path.length ? this.form.get(path) : this.form; } } + +function remove(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/src/directives/ng_model.ts b/packages/forms/src/directives/ng_model.ts index 83552d916fde68..82764121b806b6 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 8598d083ae6c1e..4cd0b86e0cb6b6 100644 --- a/packages/forms/src/directives/reactive_directives/form_group_directive.ts +++ b/packages/forms/src/directives/reactive_directives/form_group_directive.ts @@ -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() { @@ -170,6 +161,15 @@ export class FormGroupDirective extends ControlContainer implements Form, this.form._updateTreeValidity({emitEvent: false}); } + private _syncPendingControls() { + this.form._syncPendingControls(); + this.directives.forEach(dir => { + if (dir.control.updateOn === 'submit') { + dir.viewToModelUpdate(dir.control._pendingValue); + } + }); + } + private _updateRegistrations() { this.form._registerOnCollectionChange(() => this._updateDomValue()); if (this._oldForm) this._oldForm._registerOnCollectionChange(() => {}); diff --git a/packages/forms/test/reactive_integration_spec.ts b/packages/forms/test/reactive_integration_spec.ts index a37ea8f45873be..22ba1848f4c5b4 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 6436350d85706b..916b146fcb1d74 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 e5d27c631f8a07..89f5ed5ce78c3c 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -424,6 +424,7 @@ export declare class NgModel extends NgControl implements OnChanges, OnDestroy { options: { name?: string; standalone?: boolean; + updateOn?: FormHooks; }; readonly path: string[]; update: EventEmitter<{}>;