Skip to content
Permalink
Browse files

feat(forms): add updateOn and ngFormOptions to NgForm

This commit introduces a new Input property called
`ngFormOptions` to the `NgForm` directive. You can use it
to set default `updateOn` values for all the form's child
controls. This default will be used unless the child has
already explicitly set its own `updateOn` value in
`ngModelOptions`.

Potential values: `change` | `blur` | `submit`

```html
<form [ngFormOptions]="{updateOn: blur}">
  <input name="one" ngModel>  <!-- will update on blur-->
</form>
```

For more context, see [#18577](#18577).
  • Loading branch information...
kara authored and hansl committed Aug 8, 2017
1 parent 43226cb commit 0d458284601f1bbeece1718a304c8ec7c442f0a8
@@ -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
* <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[]) {
@@ -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();
@@ -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: `
<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({
@@ -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;

0 comments on commit 0d45828

Please sign in to comment.
You can’t perform that action at this time.