feat(forms): add updateOn submit option to FormControls (#18514)
This commit is contained in:
parent
685cc26ab2
commit
f69561b2de
@ -134,6 +134,7 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||||||
|
|
||||||
onSubmit($event: Event): boolean {
|
onSubmit($event: Event): boolean {
|
||||||
this._submitted = true;
|
this._submitted = true;
|
||||||
|
this._syncPendingControls();
|
||||||
this.ngSubmit.emit($event);
|
this.ngSubmit.emit($event);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -145,6 +146,16 @@ export class FormGroupDirective extends ControlContainer implements Form,
|
|||||||
this._submitted = false;
|
this._submitted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_syncPendingControls() {
|
||||||
|
this.form._syncPendingControls();
|
||||||
|
this.directives.forEach(dir => {
|
||||||
|
if (dir.control._updateOn === 'submit') {
|
||||||
|
dir.viewToModelUpdate(dir.control._pendingValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_updateDomValue() {
|
_updateDomValue() {
|
||||||
this.directives.forEach(dir => {
|
this.directives.forEach(dir => {
|
||||||
|
@ -84,23 +84,23 @@ function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
|
|||||||
control._pendingValue = newValue;
|
control._pendingValue = newValue;
|
||||||
control._pendingDirty = true;
|
control._pendingDirty = true;
|
||||||
|
|
||||||
if (control._updateOn === 'change') {
|
if (control._updateOn === 'change') updateControl(control, dir);
|
||||||
dir.viewToModelUpdate(newValue);
|
|
||||||
control.markAsDirty();
|
|
||||||
control.setValue(newValue, {emitModelToViewChange: false});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
|
function setUpBlurPipeline(control: FormControl, dir: NgControl): void {
|
||||||
dir.valueAccessor !.registerOnTouched(() => {
|
dir.valueAccessor !.registerOnTouched(() => {
|
||||||
if (control._updateOn === 'blur') {
|
control._pendingTouched = true;
|
||||||
|
|
||||||
|
if (control._updateOn === 'blur') updateControl(control, dir);
|
||||||
|
if (control._updateOn !== 'submit') control.markAsTouched();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateControl(control: FormControl, dir: NgControl): void {
|
||||||
dir.viewToModelUpdate(control._pendingValue);
|
dir.viewToModelUpdate(control._pendingValue);
|
||||||
if (control._pendingDirty) control.markAsDirty();
|
if (control._pendingDirty) control.markAsDirty();
|
||||||
control.setValue(control._pendingValue, {emitModelToViewChange: false});
|
control.setValue(control._pendingValue, {emitModelToViewChange: false});
|
||||||
}
|
|
||||||
control.markAsTouched();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
|
function setUpModelChangePipeline(control: FormControl, dir: NgControl): void {
|
||||||
|
@ -78,7 +78,7 @@ function coerceToAsyncValidator(
|
|||||||
origAsyncValidator || null;
|
origAsyncValidator || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FormHooks = 'change' | 'blur';
|
export type FormHooks = 'change' | 'blur' | 'submit';
|
||||||
|
|
||||||
export interface AbstractControlOptions {
|
export interface AbstractControlOptions {
|
||||||
validators?: ValidatorFn|ValidatorFn[]|null;
|
validators?: ValidatorFn|ValidatorFn[]|null;
|
||||||
@ -108,6 +108,13 @@ function isOptionsObj(
|
|||||||
export abstract class AbstractControl {
|
export abstract class AbstractControl {
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_value: any;
|
_value: any;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_pendingDirty: boolean;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_pendingTouched: boolean;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_onCollectionChange = () => {};
|
_onCollectionChange = () => {};
|
||||||
|
|
||||||
@ -284,6 +291,7 @@ export abstract class AbstractControl {
|
|||||||
*/
|
*/
|
||||||
markAsUntouched(opts: {onlySelf?: boolean} = {}): void {
|
markAsUntouched(opts: {onlySelf?: boolean} = {}): void {
|
||||||
this._touched = false;
|
this._touched = false;
|
||||||
|
this._pendingTouched = false;
|
||||||
|
|
||||||
this._forEachChild(
|
this._forEachChild(
|
||||||
(control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); });
|
(control: AbstractControl) => { control.markAsUntouched({onlySelf: true}); });
|
||||||
@ -316,6 +324,7 @@ export abstract class AbstractControl {
|
|||||||
*/
|
*/
|
||||||
markAsPristine(opts: {onlySelf?: boolean} = {}): void {
|
markAsPristine(opts: {onlySelf?: boolean} = {}): void {
|
||||||
this._pristine = true;
|
this._pristine = true;
|
||||||
|
this._pendingDirty = false;
|
||||||
|
|
||||||
this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); });
|
this._forEachChild((control: AbstractControl) => { control.markAsPristine({onlySelf: true}); });
|
||||||
|
|
||||||
@ -568,6 +577,9 @@ export abstract class AbstractControl {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
abstract _allControlsDisabled(): boolean;
|
abstract _allControlsDisabled(): boolean;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
abstract _syncPendingControls(): boolean;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_anyControlsHaveStatus(status: string): boolean {
|
_anyControlsHaveStatus(status: string): boolean {
|
||||||
return this._anyControls((control: AbstractControl) => control.status === status);
|
return this._anyControls((control: AbstractControl) => control.status === status);
|
||||||
@ -672,6 +684,9 @@ export abstract class AbstractControl {
|
|||||||
* const c = new FormControl('', { updateOn: 'blur' });
|
* const c = new FormControl('', { updateOn: 'blur' });
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
|
* You can also set `updateOn` to `'submit'`, which will delay value and validity
|
||||||
|
* updates until the parent form of the control fires a submit event.
|
||||||
|
*
|
||||||
* See its superclass, {@link AbstractControl}, for more properties and methods.
|
* See its superclass, {@link AbstractControl}, for more properties and methods.
|
||||||
*
|
*
|
||||||
* * **npm package**: `@angular/forms`
|
* * **npm package**: `@angular/forms`
|
||||||
@ -688,9 +703,6 @@ export class FormControl extends AbstractControl {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_pendingValue: any;
|
_pendingValue: any;
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
_pendingDirty: boolean;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
formState: any = null,
|
formState: any = null,
|
||||||
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
validatorOrOpts?: ValidatorFn|ValidatorFn[]|AbstractControlOptions|null,
|
||||||
@ -782,7 +794,6 @@ export class FormControl extends AbstractControl {
|
|||||||
reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
|
reset(formState: any = null, options: {onlySelf?: boolean, emitEvent?: boolean} = {}): void {
|
||||||
this._applyFormState(formState);
|
this._applyFormState(formState);
|
||||||
this.markAsPristine(options);
|
this.markAsPristine(options);
|
||||||
this._pendingDirty = false;
|
|
||||||
this.markAsUntouched(options);
|
this.markAsUntouched(options);
|
||||||
this.setValue(this._value, options);
|
this.setValue(this._value, options);
|
||||||
}
|
}
|
||||||
@ -828,6 +839,17 @@ export class FormControl extends AbstractControl {
|
|||||||
*/
|
*/
|
||||||
_forEachChild(cb: Function): void {}
|
_forEachChild(cb: Function): void {}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_syncPendingControls(): boolean {
|
||||||
|
if (this._updateOn === 'submit') {
|
||||||
|
this.setValue(this._pendingValue, {onlySelf: true, emitModelToViewChange: false});
|
||||||
|
if (this._pendingDirty) this.markAsDirty();
|
||||||
|
if (this._pendingTouched) this.markAsTouched();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private _applyFormState(formState: any) {
|
private _applyFormState(formState: any) {
|
||||||
if (this._isBoxedValue(formState)) {
|
if (this._isBoxedValue(formState)) {
|
||||||
this._value = this._pendingValue = formState.value;
|
this._value = this._pendingValue = formState.value;
|
||||||
@ -1092,6 +1114,15 @@ export class FormGroup extends AbstractControl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_syncPendingControls(): boolean {
|
||||||
|
let subtreeUpdated = this._reduceChildren(false, (updated: boolean, child: AbstractControl) => {
|
||||||
|
return child._syncPendingControls() ? true : updated;
|
||||||
|
});
|
||||||
|
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
|
||||||
|
return subtreeUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_throwIfControlMissing(name: string): void {
|
_throwIfControlMissing(name: string): void {
|
||||||
if (!Object.keys(this.controls).length) {
|
if (!Object.keys(this.controls).length) {
|
||||||
@ -1404,6 +1435,15 @@ export class FormArray extends AbstractControl {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_syncPendingControls(): boolean {
|
||||||
|
let subtreeUpdated = this.controls.reduce((updated: boolean, child: AbstractControl) => {
|
||||||
|
return child._syncPendingControls() ? true : updated;
|
||||||
|
}, false);
|
||||||
|
if (subtreeUpdated) this.updateValueAndValidity({onlySelf: true});
|
||||||
|
return subtreeUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_throwIfControlMissing(index: number): void {
|
_throwIfControlMissing(index: number): void {
|
||||||
if (!this.controls.length) {
|
if (!this.controls.length) {
|
||||||
|
@ -12,8 +12,10 @@ import {AbstractControl, AsyncValidator, AsyncValidatorFn, COMPOSITION_BUFFER_MO
|
|||||||
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';
|
||||||
|
import {merge} from 'rxjs/observable/merge';
|
||||||
import {timer} from 'rxjs/observable/timer';
|
import {timer} from 'rxjs/observable/timer';
|
||||||
import {_do} from 'rxjs/operator/do';
|
import {_do} from 'rxjs/operator/do';
|
||||||
|
|
||||||
import {MyInput, MyInputForm} from './value_accessor_integration_spec';
|
import {MyInput, MyInputForm} from './value_accessor_integration_spec';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
@ -898,8 +900,320 @@ export function main() {
|
|||||||
dispatchEvent(input, 'blur');
|
dispatchEvent(input, 'blur');
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.');
|
expect(input.value).toEqual('', 'Expected view value to reset');
|
||||||
expect(control.value).toBe(null, 'Expected pending value to reset.');
|
expect(control.value).toBe(null, 'Expected pending value to reset.');
|
||||||
|
expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit valueChanges or statusChanges until blur', () => {
|
||||||
|
const fixture = initTest(FormControlComp);
|
||||||
|
const control = new FormControl('', {validators: Validators.required, updateOn: 'blur'});
|
||||||
|
fixture.componentInstance.control = control;
|
||||||
|
fixture.detectChanges();
|
||||||
|
const values: string[] = [];
|
||||||
|
|
||||||
|
const sub =
|
||||||
|
merge(control.valueChanges, control.statusChanges).subscribe(val => values.push(val));
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input.');
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(values).toEqual(
|
||||||
|
['Nancy', 'VALID'], 'Expected valueChanges and statusChanges on blur.');
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should mark as pristine properly if pending dirty', () => {
|
||||||
|
const fixture = initTest(FormControlComp);
|
||||||
|
const control = new FormControl('', {updateOn: 'blur'});
|
||||||
|
fixture.componentInstance.control = control;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'aa';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
control.markAsPristine();
|
||||||
|
expect(control.dirty).toBe(false, 'Expected control to become pristine.');
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(control.dirty).toBe(false, 'Expected pending dirty value to reset.');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('on submit', () => {
|
||||||
|
|
||||||
|
it('should set initial value and validity on init', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const form = new FormGroup({
|
||||||
|
login:
|
||||||
|
new FormControl('Nancy', {validators: Validators.required, updateOn: 'submit'})
|
||||||
|
});
|
||||||
|
fixture.componentInstance.form = form;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
expect(input.value).toEqual('Nancy', 'Expected initial value to propagate to view.');
|
||||||
|
expect(form.value).toEqual({login: 'Nancy'}, 'Expected initial value to be set.');
|
||||||
|
expect(form.valid).toBe(true, 'Expected form to run validation on initial value.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update value or validity until submit', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup(
|
||||||
|
{login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: ''}, 'Expected form value to remain unchanged on input.');
|
||||||
|
expect(formGroup.valid).toBe(false, 'Expected form validation not to run on input.');
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: ''}, 'Expected form value to remain unchanged on blur.');
|
||||||
|
expect(formGroup.valid).toBe(false, 'Expected form validation not to run on blur.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: 'Nancy'}, 'Expected form value to update on submit.');
|
||||||
|
expect(formGroup.valid).toBe(true, 'Expected form validation to run on submit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update after submit until a second submit', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup(
|
||||||
|
{login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: 'Nancy'}, 'Expected value not to change until a second submit.');
|
||||||
|
expect(formGroup.valid)
|
||||||
|
.toBe(true, 'Expected validation not to run until a second submit.');
|
||||||
|
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: ''}, 'Expected value to update on the second submit.');
|
||||||
|
expect(formGroup.valid).toBe(false, 'Expected validation to run on a second submit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not wait for submit to set value programmatically', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup(
|
||||||
|
{login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
formGroup.setValue({login: 'Nancy'});
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
expect(input.value).toEqual('Nancy', 'Expected view value to update immediately.');
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: 'Nancy'}, 'Expected form value to update immediately.');
|
||||||
|
expect(formGroup.valid).toBe(true, 'Expected form validation to run immediately.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update dirty until submit', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.dirty).toBe(false, 'Expected dirty not to change on input.');
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.dirty).toBe(false, 'Expected dirty not to change on blur.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.dirty).toBe(true, 'Expected dirty to update on submit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update touched until submit', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.touched).toBe(false, 'Expected touched not to change until submit.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.touched).toBe(true, 'Expected touched to update on submit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset properly', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup(
|
||||||
|
{login: new FormControl('', {validators: Validators.required, updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
formGroup.reset();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(input.value).toEqual('', 'Expected view value to reset.');
|
||||||
|
expect(formGroup.value).toEqual({login: null}, 'Expected form value to reset');
|
||||||
|
expect(formGroup.dirty).toBe(false, 'Expected dirty to stay false on reset.');
|
||||||
|
expect(formGroup.touched).toBe(false, 'Expected touched to stay false on reset.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.value)
|
||||||
|
.toEqual({login: null}, 'Expected form value to stay empty on submit');
|
||||||
|
expect(formGroup.dirty).toBe(false, 'Expected dirty to stay false on submit.');
|
||||||
|
expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not emit valueChanges or statusChanges until submit', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const control =
|
||||||
|
new FormControl('', {validators: Validators.required, updateOn: 'submit'});
|
||||||
|
const formGroup = new FormGroup({login: control});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const values: string[] = [];
|
||||||
|
const streams = merge(
|
||||||
|
control.valueChanges, control.statusChanges, formGroup.valueChanges,
|
||||||
|
formGroup.statusChanges);
|
||||||
|
const sub = streams.subscribe(val => values.push(val));
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on input');
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(values).toEqual([], 'Expected no valueChanges or statusChanges on blur');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(values).toEqual(
|
||||||
|
['Nancy', 'VALID', {login: 'Nancy'}, 'VALID'],
|
||||||
|
'Expected valueChanges and statusChanges to update on submit.');
|
||||||
|
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not run validation for onChange controls on submit', () => {
|
||||||
|
const validatorSpy = jasmine.createSpy('validator');
|
||||||
|
const groupValidatorSpy = jasmine.createSpy('groupValidatorSpy');
|
||||||
|
|
||||||
|
const fixture = initTest(NestedFormGroupComp);
|
||||||
|
const formGroup = new FormGroup({
|
||||||
|
signin: new FormGroup({login: new FormControl(), password: new FormControl()}),
|
||||||
|
email: new FormControl('', {updateOn: 'submit'})
|
||||||
|
});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
formGroup.get('signin.login') !.setValidators(validatorSpy);
|
||||||
|
formGroup.get('signin') !.setValidators(groupValidatorSpy);
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(validatorSpy).not.toHaveBeenCalled();
|
||||||
|
expect(groupValidatorSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should mark as untouched properly if pending touched', () => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
formGroup.markAsUntouched();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.touched).toBe(false, 'Expected group to become untouched.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(formGroup.touched).toBe(false, 'Expected touched to stay false on submit.');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -960,6 +1274,33 @@ export function main() {
|
|||||||
expect(input.selectionStart).toEqual(1);
|
expect(input.selectionStart).toEqual(1);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should work with updateOn submit', fakeAsync(() => {
|
||||||
|
const fixture = initTest(FormGroupNgModel);
|
||||||
|
const formGroup = new FormGroup({login: new FormControl('', {updateOn: 'submit'})});
|
||||||
|
fixture.componentInstance.form = formGroup;
|
||||||
|
fixture.componentInstance.login = 'initial';
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
input.value = 'Nancy';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(fixture.componentInstance.login)
|
||||||
|
.toEqual('initial', 'Expected ngModel value to remain unchanged on input.');
|
||||||
|
|
||||||
|
const form = fixture.debugElement.query(By.css('form')).nativeElement;
|
||||||
|
dispatchEvent(form, 'submit');
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
expect(fixture.componentInstance.login)
|
||||||
|
.toEqual('Nancy', 'Expected ngModel value to update on submit.');
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('validations', () => {
|
describe('validations', () => {
|
||||||
@ -1697,13 +2038,12 @@ class FormArrayNestedGroup {
|
|||||||
cityArray: FormArray;
|
cityArray: FormArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'form-group-ng-model',
|
selector: 'form-group-ng-model',
|
||||||
template: `
|
template: `
|
||||||
<div [formGroup]="form">
|
<form [formGroup]="form">
|
||||||
<input type="text" formControlName="login" [(ngModel)]="login">
|
<input type="text" formControlName="login" [(ngModel)]="login">
|
||||||
</div>`
|
</form>`
|
||||||
})
|
})
|
||||||
class FormGroupNgModel {
|
class FormGroupNgModel {
|
||||||
form: FormGroup;
|
form: FormGroup;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user