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
|
||||
*/
|
||||
|
||||
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,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;
|
||||
|
|
Loading…
Reference in New Issue