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](https://github.com/angular/angular/pull/18577).
This commit is contained in:
Kara Erickson 2017-08-08 14:37:29 -07:00 committed by Hans
parent 43226cb93d
commit 0d45828460
3 changed files with 156 additions and 8 deletions

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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 {NG_ASYNC_VALIDATORS, NG_VALIDATORS} from '../validators';
import {ControlContainer} from './control_container'; import {ControlContainer} from './control_container';
@ -63,13 +63,30 @@ const resolvedPromise = Promise.resolve(null);
outputs: ['ngSubmit'], outputs: ['ngSubmit'],
exportAs: 'ngForm' exportAs: 'ngForm'
}) })
export class NgForm extends ControlContainer implements Form { export class NgForm extends ControlContainer implements Form,
AfterViewInit {
private _submitted: boolean = false; private _submitted: boolean = false;
private _directives: NgModel[] = []; private _directives: NgModel[] = [];
form: FormGroup; form: FormGroup;
ngSubmit = new EventEmitter(); 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( constructor(
@Optional() @Self() @Inject(NG_VALIDATORS) validators: any[], @Optional() @Self() @Inject(NG_VALIDATORS) validators: any[],
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: 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)); new FormGroup({}, composeValidators(validators), composeAsyncValidators(asyncValidators));
} }
ngAfterViewInit() { this._setUpdateStrategy(); }
get submitted(): boolean { return this._submitted; } get submitted(): boolean { return this._submitted; }
get formDirective(): Form { return this; } get formDirective(): Form { return this; }
@ -154,6 +173,12 @@ export class NgForm extends ControlContainer implements Form {
this._submitted = false; this._submitted = false;
} }
private _setUpdateStrategy() {
if (this.options && this.options.updateOn != null) {
this.form._updateOn = this.options.updateOn;
}
}
/** @internal */ /** @internal */
_findContainer(path: string[]): FormGroup { _findContainer(path: string[]): FormGroup {
path.pop(); path.pop();

View File

@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license * 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 {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 {By} from '@angular/platform-browser/src/dom/debug/by';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {dispatchEvent} from '@angular/platform-browser/testing/src/browser_util'; 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', () => { describe('submit and reset events', () => {
@ -1473,15 +1590,17 @@ class InvalidNgModelNoName {
@Component({ @Component({
selector: 'ng-model-options-standalone', selector: 'ng-model-options-standalone',
template: ` template: `
<form> <form [ngFormOptions]="formOptions">
<input name="one" [(ngModel)]="one"> <input name="one" [(ngModel)]="one">
<input [(ngModel)]="two" [ngModelOptions]="{standalone: true}"> <input [(ngModel)]="two" [ngModelOptions]="options">
</form> </form>
` `
}) })
class NgModelOptionsStandalone { class NgModelOptionsStandalone {
one: string; one: string;
two: string; two: string;
options: {name?: string, standalone?: boolean, updateOn?: string} = {standalone: true};
formOptions = {};
} }
@Component({ @Component({

View File

@ -388,7 +388,7 @@ export declare class NgControlStatusGroup extends AbstractControlStatus {
} }
/** @stable */ /** @stable */
export declare class NgForm extends ControlContainer implements Form { export declare class NgForm extends ControlContainer implements Form, AfterViewInit {
readonly control: FormGroup; readonly control: FormGroup;
readonly controls: { readonly controls: {
[key: string]: AbstractControl; [key: string]: AbstractControl;
@ -396,6 +396,9 @@ export declare class NgForm extends ControlContainer implements Form {
form: FormGroup; form: FormGroup;
readonly formDirective: Form; readonly formDirective: Form;
ngSubmit: EventEmitter<{}>; ngSubmit: EventEmitter<{}>;
options: {
updateOn?: FormHooks;
};
readonly path: string[]; readonly path: string[];
readonly submitted: boolean; readonly submitted: boolean;
constructor(validators: any[], asyncValidators: any[]); constructor(validators: any[], asyncValidators: any[]);
@ -403,6 +406,7 @@ export declare class NgForm extends ControlContainer implements Form {
addFormGroup(dir: NgModelGroup): void; addFormGroup(dir: NgModelGroup): void;
getControl(dir: NgModel): FormControl; getControl(dir: NgModel): FormControl;
getFormGroup(dir: NgModelGroup): FormGroup; getFormGroup(dir: NgModelGroup): FormGroup;
ngAfterViewInit(): void;
onReset(): void; onReset(): void;
onSubmit($event: Event): boolean; onSubmit($event: Event): boolean;
removeControl(dir: NgModel): void; removeControl(dir: NgModel): void;