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:
parent
43226cb93d
commit
0d45828460
|
@ -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();
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue