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:
Oussama Ben Brahim 2020-06-13 21:47:30 +02:00 committed by Andrew Kushnir
parent 616543ded0
commit 77b62a52c0
3 changed files with 668 additions and 5 deletions

View File

@ -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;
}

View File

@ -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()', () => {

View File

@ -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)));