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 '!'.
|
||||
_pendingDirty!: boolean;
|
||||
|
||||
/**
|
||||
* Indicates that a control has its own pending asynchronous validation in progress.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
_hasOwnPendingAsyncValidator = false;
|
||||
|
||||
/** @internal */
|
||||
// TODO(issue/24571): remove '!'.
|
||||
_pendingTouched!: boolean;
|
||||
|
@ -675,15 +682,22 @@ export abstract class AbstractControl {
|
|||
private _runAsyncValidator(emitEvent?: boolean): void {
|
||||
if (this.asyncValidator) {
|
||||
(this as {status: string}).status = PENDING;
|
||||
this._hasOwnPendingAsyncValidator = true;
|
||||
const obs = toObservable(this.asyncValidator(this));
|
||||
this._asyncValidationSubscription =
|
||||
obs.subscribe((errors: ValidationErrors|null) => this.setErrors(errors, {emitEvent}));
|
||||
this._asyncValidationSubscription = obs.subscribe((errors: ValidationErrors|null) => {
|
||||
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 {
|
||||
if (this._asyncValidationSubscription) {
|
||||
this._asyncValidationSubscription.unsubscribe();
|
||||
this._hasOwnPendingAsyncValidator = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -838,7 +852,7 @@ export abstract class AbstractControl {
|
|||
private _calculateStatus(): string {
|
||||
if (this._allControlsDisabled()) return DISABLED;
|
||||
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;
|
||||
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) {
|
||||
const e = new EventEmitter();
|
||||
Promise.resolve(null).then(() => {
|
||||
|
@ -981,6 +1014,538 @@ describe('FormGroup', () => {
|
|||
expect(g.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()', () => {
|
||||
|
|
|
@ -2074,6 +2074,72 @@ import {MyInput, MyInputForm} from './value_accessor_integration_spec';
|
|||
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(() => {
|
||||
const fixture = initTest(FormControlComp);
|
||||
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) => {
|
||||
let resolve: (result: any) => void;
|
||||
const promise = new Promise<any>(res => {
|
||||
resolve = res;
|
||||
});
|
||||
const res = (c.value == expectedValue) ? null : {'uniqLogin': true};
|
||||
const res = checker(c) ? null : error;
|
||||
setTimeout(() => resolve(res), timeout);
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
function uniqLoginAsyncValidator(expectedValue: string, timeout: number = 0) {
|
||||
return asyncValidator(c => c.value === expectedValue, timeout, {'uniqLogin': true});
|
||||
}
|
||||
|
||||
function observableValidator(resultArr: number[]): AsyncValidatorFn {
|
||||
return (c: AbstractControl) => {
|
||||
return timer(100).pipe(tap((resp: any) => resultArr.push(resp)));
|
||||
|
|
Loading…
Reference in New Issue