fix(forms): handle form groups/arrays own pending async validation (#22575)
introduce a boolean to track form groups/arrays own pending async validation to distinguish between pending state due to children and pending state due to own validation Fixes #10064 PR Close #22575
This commit is contained in:
parent
616543ded0
commit
77b62a52c0
|
@ -137,6 +137,13 @@ export abstract class AbstractControl {
|
||||||
// TODO(issue/24571): remove '!'.
|
// TODO(issue/24571): remove '!'.
|
||||||
_pendingDirty!: boolean;
|
_pendingDirty!: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a control has its own pending asynchronous validation in progress.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
_hasOwnPendingAsyncValidator = false;
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
// TODO(issue/24571): remove '!'.
|
// TODO(issue/24571): remove '!'.
|
||||||
_pendingTouched!: boolean;
|
_pendingTouched!: boolean;
|
||||||
|
@ -675,15 +682,22 @@ export abstract class AbstractControl {
|
||||||
private _runAsyncValidator(emitEvent?: boolean): void {
|
private _runAsyncValidator(emitEvent?: boolean): void {
|
||||||
if (this.asyncValidator) {
|
if (this.asyncValidator) {
|
||||||
(this as {status: string}).status = PENDING;
|
(this as {status: string}).status = PENDING;
|
||||||
|
this._hasOwnPendingAsyncValidator = true;
|
||||||
const obs = toObservable(this.asyncValidator(this));
|
const obs = toObservable(this.asyncValidator(this));
|
||||||
this._asyncValidationSubscription =
|
this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors|null) => {
|
||||||
obs.subscribe((errors: ValidationErrors|null) => this.setErrors(errors, {emitEvent}));
|
this._hasOwnPendingAsyncValidator = false;
|
||||||
|
// This will trigger the recalculation of the validation status, which depends on
|
||||||
|
// the state of the asynchronous validation (whether it is in progress or not). So, it is
|
||||||
|
// necessary that we have updated the `_hasOwnPendingAsyncValidator` boolean flag first.
|
||||||
|
this.setErrors(errors, {emitEvent});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _cancelExistingSubscription(): void {
|
private _cancelExistingSubscription(): void {
|
||||||
if (this._asyncValidationSubscription) {
|
if (this._asyncValidationSubscription) {
|
||||||
this._asyncValidationSubscription.unsubscribe();
|
this._asyncValidationSubscription.unsubscribe();
|
||||||
|
this._hasOwnPendingAsyncValidator = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -838,7 +852,7 @@ export abstract class AbstractControl {
|
||||||
private _calculateStatus(): string {
|
private _calculateStatus(): string {
|
||||||
if (this._allControlsDisabled()) return DISABLED;
|
if (this._allControlsDisabled()) return DISABLED;
|
||||||
if (this.errors) return INVALID;
|
if (this.errors) return INVALID;
|
||||||
if (this._anyControlsHaveStatus(PENDING)) return PENDING;
|
if (this._hasOwnPendingAsyncValidator || this._anyControlsHaveStatus(PENDING)) return PENDING;
|
||||||
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
|
if (this._anyControlsHaveStatus(INVALID)) return INVALID;
|
||||||
return VALID;
|
return VALID;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,39 @@ function asyncValidator(expected: string, timeouts = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function simpleAsyncValidator({
|
||||||
|
timeout = 0,
|
||||||
|
shouldFail,
|
||||||
|
customError =
|
||||||
|
{
|
||||||
|
async: true
|
||||||
|
}
|
||||||
|
}: {timeout?: number, shouldFail: boolean, customError?: any}) {
|
||||||
|
return (c: AbstractControl) => {
|
||||||
|
const res = shouldFail ? customError : null;
|
||||||
|
|
||||||
|
if (timeout === 0) {
|
||||||
|
return of(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolve: (result: any) => void = undefined!;
|
||||||
|
const promise = new Promise<ValidationErrors|null>(res => {
|
||||||
|
resolve = res;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(res);
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
return promise;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function currentStateOf(controls: AbstractControl[]):
|
||||||
|
{errors: any; pending: boolean; status: string;}[] {
|
||||||
|
return controls.map(c => ({errors: c.errors, pending: c.pending, status: c.status}));
|
||||||
|
}
|
||||||
|
|
||||||
function asyncValidatorReturningObservable(c: AbstractControl) {
|
function asyncValidatorReturningObservable(c: AbstractControl) {
|
||||||
const e = new EventEmitter();
|
const e = new EventEmitter();
|
||||||
Promise.resolve(null).then(() => {
|
Promise.resolve(null).then(() => {
|
||||||
|
@ -981,6 +1014,538 @@ describe('FormGroup', () => {
|
||||||
expect(g.errors).toEqual({'async': true});
|
expect(g.errors).toEqual({'async': true});
|
||||||
expect(g.get('one')!.errors).toEqual({'async': true});
|
expect(g.get('one')!.errors).toEqual({'async': true});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should handle successful async FormGroup resolving synchronously before a successful async child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c}, null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
|
||||||
|
// Initially, the form control validation is pending, and the form group own validation has
|
||||||
|
// synchronously resolved. Still, the form is in pending state due to its child
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle successful async FormGroup resolving after a synchronously and successfully resolving child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c}, null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
|
||||||
|
// Initially, form control validator has synchronously resolved. However, g has its own
|
||||||
|
// pending validation
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle successful async FormGroup and child control validators resolving synchronously',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c}, null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
|
||||||
|
// Both form control and form group successful async validators have resolved synchronously
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing async FormGroup and failing child control validators resolving synchronously',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 0, shouldFail: true}));
|
||||||
|
const g =
|
||||||
|
new FormGroup({'one': c}, null!, simpleAsyncValidator({timeout: 0, shouldFail: true}));
|
||||||
|
|
||||||
|
// FormControl async validator has executed and failed synchronously with the default error
|
||||||
|
// `{async: true}`. Next, the form group status is calculated. Since one of its children is
|
||||||
|
// failing, the form group itself is marked `INVALID`. And its asynchronous validation is
|
||||||
|
// not even triggered. Therefore, we end up with form group that is `INVALID` but whose
|
||||||
|
// errors are null (child errors do not propagate and own async validation not event
|
||||||
|
// triggered).
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing async FormGroup and successful child control validators resolving synchronously',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
const g =
|
||||||
|
new FormGroup({'one': c}, null!, simpleAsyncValidator({timeout: 0, shouldFail: true}));
|
||||||
|
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing async FormArray and successful children validators resolving synchronously',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c}, null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
|
||||||
|
const c2 =
|
||||||
|
new FormControl('fcVal', null!, simpleAsyncValidator({timeout: 0, shouldFail: false}));
|
||||||
|
|
||||||
|
const a =
|
||||||
|
new FormArray([g, c2], null!, simpleAsyncValidator({timeout: 0, shouldFail: true}));
|
||||||
|
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Array
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group p
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control c2
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing FormGroup validator resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g =
|
||||||
|
new FormGroup({'one': c}, null!, simpleAsyncValidator({timeout: 2, shouldFail: true}));
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation fails
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing FormArray validator resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const a = new FormArray([c], null!, simpleAsyncValidator({timeout: 2, shouldFail: true}));
|
||||||
|
|
||||||
|
// Initially, the form array and nested control are in pending state
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form array validation fails
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle successful FormGroup validator resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c}, null!, simpleAsyncValidator({timeout: 2, shouldFail: false}));
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation resolves
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle successful FormArray validator resolving after successful child validators',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c1 = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c1}, null!, simpleAsyncValidator({timeout: 2, shouldFail: false}));
|
||||||
|
const c2 =
|
||||||
|
new FormControl('fcVal', null!, simpleAsyncValidator({timeout: 3, shouldFail: false}));
|
||||||
|
|
||||||
|
const a =
|
||||||
|
new FormArray([g, c2], null!, simpleAsyncValidator({timeout: 4, shouldFail: false}));
|
||||||
|
|
||||||
|
// Initially, the form array and the tested form group and form control c2 are in pending
|
||||||
|
// state
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // g
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(2);
|
||||||
|
|
||||||
|
// After 2ms, g validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, c2 validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, FormArray own validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // c2
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing FormArray validator resolving after successful child validators',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c1 = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'one': c1}, null!, simpleAsyncValidator({timeout: 2, shouldFail: false}));
|
||||||
|
const c2 =
|
||||||
|
new FormControl('fcVal', null!, simpleAsyncValidator({timeout: 3, shouldFail: false}));
|
||||||
|
|
||||||
|
const a =
|
||||||
|
new FormArray([g, c2], null!, simpleAsyncValidator({timeout: 4, shouldFail: true}));
|
||||||
|
|
||||||
|
// Initially, the form array and the tested form group and form control c2 are in pending
|
||||||
|
// state
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // g
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(2);
|
||||||
|
|
||||||
|
// After 2ms, g validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, c2 validation has resolved
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // c2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, FormArray own validation has failed
|
||||||
|
expect(currentStateOf([a, a.at(0)!, a.at(1)!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // g
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // c2
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle multiple successful FormGroup validators resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup({'one': c}, null!, [
|
||||||
|
simpleAsyncValidator({timeout: 2, shouldFail: false}),
|
||||||
|
simpleAsyncValidator({timeout: 3, shouldFail: false})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, one form async validator has resolved but not the second
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation resolves
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle multiple FormGroup validators (success then failure) resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup({'one': c}, null!, [
|
||||||
|
simpleAsyncValidator({timeout: 2, shouldFail: false}),
|
||||||
|
simpleAsyncValidator({timeout: 3, shouldFail: true})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, one form async validator has resolved but not the second
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation fails
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should handle multiple FormGroup validators (failure then success) resolving after successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
const g = new FormGroup({'one': c}, null!, [
|
||||||
|
simpleAsyncValidator({timeout: 2, shouldFail: true}),
|
||||||
|
simpleAsyncValidator({timeout: 3, shouldFail: false})
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, only form control validation has resolved
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// All async validators are composed into one function. So, after 2ms, the FormGroup g is
|
||||||
|
// still in pending state without errors
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, the form group validation fails
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should handle async validators in nested form groups / arrays', fakeAsync(() => {
|
||||||
|
const c1 = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 1, shouldFail: false}));
|
||||||
|
|
||||||
|
const g1 = new FormGroup(
|
||||||
|
{'one': c1}, null!, simpleAsyncValidator({timeout: 2, shouldFail: true}));
|
||||||
|
|
||||||
|
const c2 =
|
||||||
|
new FormControl('fcVal', null!, simpleAsyncValidator({timeout: 3, shouldFail: false}));
|
||||||
|
|
||||||
|
const g2 =
|
||||||
|
new FormArray([c2], null!, simpleAsyncValidator({timeout: 4, shouldFail: false}));
|
||||||
|
|
||||||
|
const g = new FormGroup(
|
||||||
|
{'g1': g1, 'g2': g2}, null!, simpleAsyncValidator({timeout: 5, shouldFail: false}));
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('g1')!, g.get('g2')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g1
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(2);
|
||||||
|
|
||||||
|
// After 2ms, g1 validation fails
|
||||||
|
expect(currentStateOf([g, g.get('g1')!, g.get('g2')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group g1
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(2);
|
||||||
|
|
||||||
|
// After 2ms, g2 validation resolves
|
||||||
|
expect(currentStateOf([g, g.get('g1')!, g.get('g2')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group g
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group g1
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group g2
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, g validation fails because g1 is invalid, but since errors do not cascade, so
|
||||||
|
// we still have null errors for g
|
||||||
|
expect(currentStateOf([g, g.get('g1')!, g.get('g2')!])).toEqual([
|
||||||
|
{errors: null, pending: false, status: 'INVALID'}, // Group g
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group g1
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Group g2
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing FormGroup validator resolving before successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 2, shouldFail: false}));
|
||||||
|
const g =
|
||||||
|
new FormGroup({'one': c}, null!, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||||
|
|
||||||
|
// Initially, the form group and nested control are in pending state
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, form group validation fails
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, child validation resolves
|
||||||
|
expect(currentStateOf([g, g.get('one')!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // Group
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should handle failing FormArray validator resolving before successful child validator',
|
||||||
|
fakeAsync(() => {
|
||||||
|
const c = new FormControl(
|
||||||
|
'fcValue', null!, simpleAsyncValidator({timeout: 2, shouldFail: false}));
|
||||||
|
const a = new FormArray([c], null!, simpleAsyncValidator({timeout: 1, shouldFail: true}));
|
||||||
|
|
||||||
|
// Initially, the form array and nested control are in pending state
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // FormArray
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, form array validation fails
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
|
||||||
|
{errors: null, pending: true, status: 'PENDING'}, // Control
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(1);
|
||||||
|
|
||||||
|
// After 1ms, child validation resolves
|
||||||
|
expect(currentStateOf([a, a.at(0)!])).toEqual([
|
||||||
|
{errors: {async: true}, pending: false, status: 'INVALID'}, // FormArray
|
||||||
|
{errors: null, pending: false, status: 'VALID'}, // Control
|
||||||
|
]);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('disable() & enable()', () => {
|
describe('disable() & enable()', () => {
|
||||||
|
|
|
@ -2074,6 +2074,72 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
|
||||||
expect(control.valid).toEqual(false);
|
expect(control.valid).toEqual(false);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should handle async validation changes in parent and child controls', fakeAsync(() => {
|
||||||
|
const fixture = initTest(FormGroupComp);
|
||||||
|
const control = new FormControl(
|
||||||
|
'', Validators.required, asyncValidator(c => !!c.value && c.value.length > 3, 100));
|
||||||
|
const form = new FormGroup(
|
||||||
|
{'login': control}, null,
|
||||||
|
asyncValidator(c => c.get('login')!.value.includes('angular'), 200));
|
||||||
|
fixture.componentInstance.form = form;
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
// Initially, the form is invalid because the nested mandatory control is empty
|
||||||
|
expect(control.hasError('required')).toEqual(true);
|
||||||
|
expect(form.value).toEqual({'login': ''});
|
||||||
|
expect(form.invalid).toEqual(true);
|
||||||
|
|
||||||
|
// Setting a value in the form control that will trigger the registered asynchronous
|
||||||
|
// validation
|
||||||
|
const input = fixture.debugElement.query(By.css('input'));
|
||||||
|
input.nativeElement.value = 'angul';
|
||||||
|
dispatchEvent(input.nativeElement, 'input');
|
||||||
|
|
||||||
|
// The form control asynchronous validation is in progress (for 100 ms)
|
||||||
|
expect(control.pending).toEqual(true);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
// Now the asynchronous validation has resolved, and since the form control value
|
||||||
|
// (`angul`) has a length > 3, the validation is successful
|
||||||
|
expect(control.invalid).toEqual(false);
|
||||||
|
|
||||||
|
// Even if the child control is valid, the form control is pending because it is still
|
||||||
|
// waiting for its own validation
|
||||||
|
expect(form.pending).toEqual(true);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
// Login form control is valid. However, the form control is invalid because `angul` does
|
||||||
|
// not include `angular`
|
||||||
|
expect(control.invalid).toEqual(false);
|
||||||
|
expect(form.pending).toEqual(false);
|
||||||
|
expect(form.invalid).toEqual(true);
|
||||||
|
|
||||||
|
// Setting a value that would be trigger "VALID" form state
|
||||||
|
input.nativeElement.value = 'angular!';
|
||||||
|
dispatchEvent(input.nativeElement, 'input');
|
||||||
|
|
||||||
|
// Since the form control value changed, its asynchronous validation runs for 100ms
|
||||||
|
expect(control.pending).toEqual(true);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
// Even if the child control is valid, the form control is pending because it is still
|
||||||
|
// waiting for its own validation
|
||||||
|
expect(control.invalid).toEqual(false);
|
||||||
|
expect(form.pending).toEqual(true);
|
||||||
|
|
||||||
|
tick(100);
|
||||||
|
|
||||||
|
// Now, the form is valid because its own asynchronous validation has resolved
|
||||||
|
// successfully, because the form control value `angular` includes the `angular` string
|
||||||
|
expect(control.invalid).toEqual(false);
|
||||||
|
expect(form.pending).toEqual(false);
|
||||||
|
expect(form.invalid).toEqual(false);
|
||||||
|
}));
|
||||||
|
|
||||||
it('should cancel observable properly between validation runs', fakeAsync(() => {
|
it('should cancel observable properly between validation runs', fakeAsync(() => {
|
||||||
const fixture = initTest(FormControlComp);
|
const fixture = initTest(FormControlComp);
|
||||||
const resultArr: number[] = [];
|
const resultArr: number[] = [];
|
||||||
|
@ -2383,18 +2449,36 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqLoginAsyncValidator(expectedValue: string, timeout: number = 0) {
|
/**
|
||||||
|
* Creates an async validator using a checker function, a timeout and the error to emit in case of
|
||||||
|
* validation failure
|
||||||
|
*
|
||||||
|
* @param checker A function to decide whether the validator will resolve with success or failure
|
||||||
|
* @param timeout When the validation will resolve
|
||||||
|
* @param error The error message to be emitted in case of validation failure
|
||||||
|
*
|
||||||
|
* @returns An async validator created using a checker function, a timeout and the error to emit in
|
||||||
|
* case of validation failure
|
||||||
|
*/
|
||||||
|
function asyncValidator(
|
||||||
|
checker: (c: AbstractControl) => boolean, timeout: number = 0, error: any = {
|
||||||
|
'async': true
|
||||||
|
}) {
|
||||||
return (c: AbstractControl) => {
|
return (c: AbstractControl) => {
|
||||||
let resolve: (result: any) => void;
|
let resolve: (result: any) => void;
|
||||||
const promise = new Promise<any>(res => {
|
const promise = new Promise<any>(res => {
|
||||||
resolve = res;
|
resolve = res;
|
||||||
});
|
});
|
||||||
const res = (c.value == expectedValue) ? null : {'uniqLogin': true};
|
const res = checker(c) ? null : error;
|
||||||
setTimeout(() => resolve(res), timeout);
|
setTimeout(() => resolve(res), timeout);
|
||||||
return promise;
|
return promise;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uniqLoginAsyncValidator(expectedValue: string, timeout: number = 0) {
|
||||||
|
return asyncValidator(c => c.value === expectedValue, timeout, {'uniqLogin': true});
|
||||||
|
}
|
||||||
|
|
||||||
function observableValidator(resultArr: number[]): AsyncValidatorFn {
|
function observableValidator(resultArr: number[]): AsyncValidatorFn {
|
||||||
return (c: AbstractControl) => {
|
return (c: AbstractControl) => {
|
||||||
return timer(100).pipe(tap((resp: any) => resultArr.push(resp)));
|
return timer(100).pipe(tap((resp: any) => resultArr.push(resp)));
|
||||||
|
|
Loading…
Reference in New Issue