diff --git a/packages/forms/src/directives/ng_form.ts b/packages/forms/src/directives/ng_form.ts index 6841cc389d..a338f62e24 100644 --- a/packages/forms/src/directives/ng_form.ts +++ b/packages/forms/src/directives/ng_form.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, EventEmitter, Inject, Optional, Self, forwardRef} from '@angular/core'; +import {AfterViewInit, Directive, EventEmitter, Inject, Input, Optional, Self, forwardRef} from '@angular/core'; -import {AbstractControl, FormControl, FormGroup} from '../model'; +import {AbstractControl, FormControl, FormGroup, FormHooks} from '../model'; import {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators'; import {ControlContainer} from './control_container'; @@ -63,13 +63,30 @@ const resolvedPromise = Promise.resolve(null); outputs: ['ngSubmit'], exportAs: 'ngForm' }) -export class NgForm extends ControlContainer implements Form { +export class NgForm extends ControlContainer implements Form, + AfterViewInit { private _submitted: boolean = false; private _directives: NgModel[] = []; form: FormGroup; ngSubmit = new EventEmitter(); + /** + * Options for the `NgForm` instance. Accepts the following properties: + * + * **updateOn**: Serves as the default `updateOn` value for all child `NgModels` below it + * (unless a child has explicitly set its own value for this in `ngModelOptions`). + * Potential values: `'change'` | `'blur'` | `'submit'` + * + * ```html + *
+ * + *
+ * ``` + * + */ + @Input('ngFormOptions') options: {updateOn?: FormHooks}; + constructor( @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) { @@ -78,6 +95,8 @@ export class NgForm extends ControlContainer implements Form { new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators)); } + ngAfterViewInit() { this._setUpdateStrategy(); } + get submitted(): boolean { return this._submitted; } get formDirective(): Form { return this; } @@ -154,6 +173,12 @@ export class NgForm extends ControlContainer implements Form { this._submitted = false; } + private _setUpdateStrategy() { + if (this.options && this.options.updateOn != null) { + this.form._updateOn = this.options.updateOn; + } + } + /** @internal */ _findContainer(path: string[]): FormGroup { path.pop(); diff --git a/packages/forms/test/template_integration_spec.ts b/packages/forms/test/template_integration_spec.ts index 916b146fcb..1ff07fe41d 100644 --- a/packages/forms/test/template_integration_spec.ts +++ b/packages/forms/test/template_integration_spec.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive, Type, forwardRef} from '@angular/core'; +import {Component, Directive, Type, ViewChild, forwardRef} from '@angular/core'; import {ComponentFixture, TestBed, async, fakeAsync, tick} from '@angular/core/testing'; -import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm} from '@angular/forms'; +import {AbstractControl, AsyncValidator, COMPOSITION_BUFFER_MODE, FormControl, FormsModule, NG_ASYNC_VALIDATORS, NgForm, NgModel} 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'; @@ -766,6 +766,123 @@ export function main() { }); + describe('ngFormOptions', () => { + + it('should use ngFormOptions value when ngModelOptions are not set', fakeAsync(() => { + const fixture = initTest(NgModelOptionsStandalone); + fixture.componentInstance.options = {name: 'two'}; + fixture.componentInstance.formOptions = {updateOn: 'blur'}; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + const controlOne = form.control.get('one') !as FormControl; + expect(controlOne._updateOn).toBeUndefined(); + expect(controlOne.updateOn) + .toEqual('blur', 'Expected first control to inherit updateOn from parent form.'); + + const controlTwo = form.control.get('two') !as FormControl; + expect(controlTwo._updateOn).toBeUndefined(); + expect(controlTwo.updateOn) + .toEqual('blur', 'Expected last control to inherit updateOn from parent form.'); + })); + + it('should actually update using ngFormOptions value', fakeAsync(() => { + const fixture = initTest(NgModelOptionsStandalone); + fixture.componentInstance.one = ''; + fixture.componentInstance.formOptions = {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.value).toEqual({one: ''}, 'Expected value not to update on input.'); + + dispatchEvent(input, 'blur'); + fixture.detectChanges(); + + expect(form.value).toEqual({one: 'Nancy Drew'}, 'Expected value to update on blur.'); + })); + + it('should allow ngModelOptions updateOn to override ngFormOptions', fakeAsync(() => { + const fixture = initTest(NgModelOptionsStandalone); + fixture.componentInstance.options = {updateOn: 'blur', name: 'two'}; + fixture.componentInstance.formOptions = {updateOn: 'change'}; + fixture.detectChanges(); + tick(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + const controlOne = form.control.get('one') !as FormControl; + expect(controlOne._updateOn).toBeUndefined(); + expect(controlOne.updateOn) + .toEqual('change', 'Expected control updateOn to inherit form updateOn.'); + + const controlTwo = form.control.get('two') !as FormControl; + expect(controlTwo._updateOn).toEqual('blur', 'Expected control to set blur override.'); + expect(controlTwo.updateOn) + .toEqual('blur', 'Expected control updateOn to override form updateOn.'); + })); + + it('should update using ngModelOptions override', fakeAsync(() => { + const fixture = initTest(NgModelOptionsStandalone); + fixture.componentInstance.one = ''; + fixture.componentInstance.two = ''; + fixture.componentInstance.options = {updateOn: 'blur', name: 'two'}; + fixture.componentInstance.formOptions = {updateOn: 'change'}; + fixture.detectChanges(); + tick(); + + const [inputOne, inputTwo] = fixture.debugElement.queryAll(By.css('input')); + inputOne.nativeElement.value = 'Nancy Drew'; + dispatchEvent(inputOne.nativeElement, 'input'); + fixture.detectChanges(); + + const form = fixture.debugElement.children[0].injector.get(NgForm); + expect(form.value) + .toEqual({one: 'Nancy Drew', two: ''}, 'Expected first value to update on input.'); + + inputTwo.nativeElement.value = 'Carson Drew'; + dispatchEvent(inputTwo.nativeElement, 'input'); + fixture.detectChanges(); + tick(); + + expect(form.value) + .toEqual( + {one: 'Nancy Drew', two: ''}, 'Expected second value not to update on input.'); + + dispatchEvent(inputTwo.nativeElement, 'blur'); + fixture.detectChanges(); + + expect(form.value) + .toEqual( + {one: 'Nancy Drew', two: 'Carson Drew'}, + 'Expected second value to update on blur.'); + })); + + it('should not use ngFormOptions for standalone ngModels', fakeAsync(() => { + const fixture = initTest(NgModelOptionsStandalone); + fixture.componentInstance.two = ''; + fixture.componentInstance.options = {standalone: true}; + fixture.componentInstance.formOptions = {updateOn: 'blur'}; + fixture.detectChanges(); + tick(); + + const inputTwo = fixture.debugElement.queryAll(By.css('input'))[1].nativeElement; + inputTwo.value = 'Nancy Drew'; + dispatchEvent(inputTwo, 'input'); + fixture.detectChanges(); + + expect(fixture.componentInstance.two) + .toEqual('Nancy Drew', 'Expected standalone ngModel not to inherit blur update.'); + })); + + }); + }); describe('submit and reset events', () => { @@ -1473,15 +1590,17 @@ class InvalidNgModelNoName { @Component({ selector: 'ng-model-options-standalone', template: ` -
+ - +
` }) class NgModelOptionsStandalone { one: string; two: string; + options: {name?: string, standalone?: boolean, updateOn?: string} = {standalone: true}; + formOptions = {}; } @Component({ diff --git a/tools/public_api_guard/forms/forms.d.ts b/tools/public_api_guard/forms/forms.d.ts index 43d9f00700..7a478b028a 100644 --- a/tools/public_api_guard/forms/forms.d.ts +++ b/tools/public_api_guard/forms/forms.d.ts @@ -388,7 +388,7 @@ export declare class NgControlStatusGroup extends AbstractControlStatus { } /** @stable */ -export declare class NgForm extends ControlContainer implements Form { +export declare class NgForm extends ControlContainer implements Form, AfterViewInit { readonly control: FormGroup; readonly controls: { [key: string]: AbstractControl; @@ -396,6 +396,9 @@ export declare class NgForm extends ControlContainer implements Form { form: FormGroup; readonly formDirective: Form; ngSubmit: EventEmitter<{}>; + options: { + updateOn?: FormHooks; + }; readonly path: string[]; readonly submitted: boolean; constructor(validators: any[], asyncValidators: any[]); @@ -403,6 +406,7 @@ export declare class NgForm extends ControlContainer implements Form { addFormGroup(dir: NgModelGroup): void; getControl(dir: NgModel): FormControl; getFormGroup(dir: NgModelGroup): FormGroup; + ngAfterViewInit(): void; onReset(): void; onSubmit($event: Event): boolean; removeControl(dir: NgModel): void;