Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions packages/forms/src/directives/ng_form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
* <form [ngFormOptions]="{updateOn: 'blur'}">
* <input name="one" ngModel> <!-- this ngModel will update on blur -->
* </form>
* ```
*
*/
@Input('ngFormOptions') options: {updateOn?: FormHooks};

constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: any[]) {
Expand All @@ -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; }
Expand Down Expand Up @@ -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();
Expand Down
127 changes: 123 additions & 4 deletions packages/forms/test/template_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -1473,15 +1590,17 @@ class InvalidNgModelNoName {
@Component({
selector: 'ng-model-options-standalone',
template: `
<form>
<form [ngFormOptions]="formOptions">
<input name="one" [(ngModel)]="one">
<input [(ngModel)]="two" [ngModelOptions]="{standalone: true}">
<input [(ngModel)]="two" [ngModelOptions]="options">
</form>
`
})
class NgModelOptionsStandalone {
one: string;
two: string;
options: {name?: string, standalone?: boolean, updateOn?: string} = {standalone: true};
formOptions = {};
}

@Component({
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/forms/forms.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,21 +388,25 @@ 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;
};
form: FormGroup;
readonly formDirective: Form;
ngSubmit: EventEmitter<{}>;
options: {
updateOn?: FormHooks;
};
readonly path: string[];
readonly submitted: boolean;
constructor(validators: any[], asyncValidators: any[]);
addControl(dir: NgModel): void;
addFormGroup(dir: NgModelGroup): void;
getControl(dir: NgModel): FormControl;
getFormGroup(dir: NgModelGroup): FormGroup;
ngAfterViewInit(): void;
onReset(): void;
onSubmit($event: Event): boolean;
removeControl(dir: NgModel): void;
Expand Down