feat(forms): add ng-pending CSS class during async validation (#11243)
Closes #10336
This commit is contained in:
parent
445e5922ec
commit
97bc97153b
|
@ -37,6 +37,9 @@ export class AbstractControlStatus {
|
||||||
get ngClassInvalid(): boolean {
|
get ngClassInvalid(): boolean {
|
||||||
return isPresent(this._cd.control) ? this._cd.control.invalid : false;
|
return isPresent(this._cd.control) ? this._cd.control.invalid : false;
|
||||||
}
|
}
|
||||||
|
get ngClassPending(): boolean {
|
||||||
|
return isPresent(this._cd.control) ? this._cd.control.pending : false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ngControlStatusHost = {
|
export const ngControlStatusHost = {
|
||||||
|
@ -45,7 +48,8 @@ export const ngControlStatusHost = {
|
||||||
'[class.ng-pristine]': 'ngClassPristine',
|
'[class.ng-pristine]': 'ngClassPristine',
|
||||||
'[class.ng-dirty]': 'ngClassDirty',
|
'[class.ng-dirty]': 'ngClassDirty',
|
||||||
'[class.ng-valid]': 'ngClassValid',
|
'[class.ng-valid]': 'ngClassValid',
|
||||||
'[class.ng-invalid]': 'ngClassInvalid'
|
'[class.ng-invalid]': 'ngClassInvalid',
|
||||||
|
'[class.ng-pending]': 'ngClassPending'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -646,6 +646,60 @@ export function main() {
|
||||||
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should work with single fields and async validators', fakeAsync(() => {
|
||||||
|
const fixture = TestBed.createComponent(FormControlComp);
|
||||||
|
const control = new FormControl('', null, uniqLoginAsyncValidator('good'));
|
||||||
|
fixture.debugElement.componentInstance.control = control;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
|
||||||
|
|
||||||
|
input.value = 'good';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should work with single fields that combines async and sync validators', fakeAsync(() => {
|
||||||
|
const fixture = TestBed.createComponent(FormControlComp);
|
||||||
|
const control =
|
||||||
|
new FormControl('', Validators.required, uniqLoginAsyncValidator('good'));
|
||||||
|
fixture.debugElement.componentInstance.control = control;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-touched']);
|
||||||
|
|
||||||
|
input.value = 'bad';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-pending', 'ng-touched']);
|
||||||
|
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-invalid', 'ng-touched']);
|
||||||
|
|
||||||
|
input.value = 'good';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
|
||||||
|
}));
|
||||||
|
|
||||||
it('should work with single fields in parent forms', () => {
|
it('should work with single fields in parent forms', () => {
|
||||||
const fixture = TestBed.createComponent(FormGroupComp);
|
const fixture = TestBed.createComponent(FormGroupComp);
|
||||||
const form = new FormGroup({'login': new FormControl('', Validators.required)});
|
const form = new FormGroup({'login': new FormControl('', Validators.required)});
|
||||||
|
@ -1736,7 +1790,7 @@ class LoginIsEmptyValidator {
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
class UniqLoginValidator implements Validator {
|
class UniqLoginValidator implements Validator {
|
||||||
@Input('uniq-login-validator') expected: any /** TODO #9100 */;
|
@Input('uniq-login-validator') expected: any;
|
||||||
|
|
||||||
validate(c: AbstractControl) { return uniqLoginAsyncValidator(this.expected)(c); }
|
validate(c: AbstractControl) { return uniqLoginAsyncValidator(this.expected)(c); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Component, Input} from '@angular/core';
|
import {Component, Directive, Input, forwardRef} from '@angular/core';
|
||||||
import {TestBed, async, fakeAsync, tick} from '@angular/core/testing';
|
import {TestBed, async, fakeAsync, tick} from '@angular/core/testing';
|
||||||
import {ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgForm} from '@angular/forms';
|
import {AbstractControl, ControlValueAccessor, FormsModule, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, NgForm, Validator} from '@angular/forms';
|
||||||
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
import {By} from '@angular/platform-browser/src/dom/debug/by';
|
||||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||||
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
|
import {dispatchEvent} from '@angular/platform-browser/testing/browser_util';
|
||||||
|
@ -22,7 +22,8 @@ export function main() {
|
||||||
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
|
StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm,
|
||||||
NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
|
NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
|
||||||
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
|
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
|
||||||
NgModelValidationBindings, NgModelMultipleValidators
|
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
|
||||||
|
NgModelAsyncValidation
|
||||||
],
|
],
|
||||||
imports: [FormsModule]
|
imports: [FormsModule]
|
||||||
});
|
});
|
||||||
|
@ -139,7 +140,6 @@ export function main() {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
const form = fixture.debugElement.children[0].injector.get(NgForm);
|
|
||||||
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
|
expect(sortedClassList(input)).toEqual(['ng-invalid', 'ng-pristine', 'ng-untouched']);
|
||||||
|
|
||||||
dispatchEvent(input, 'blur');
|
dispatchEvent(input, 'blur');
|
||||||
|
@ -154,6 +154,29 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should set status classes with ngModel and async validators', fakeAsync(() => {
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(NgModelAsyncValidation);
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const input = fixture.debugElement.query(By.css('input')).nativeElement;
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-untouched']);
|
||||||
|
|
||||||
|
dispatchEvent(input, 'blur');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-pending', 'ng-pristine', 'ng-touched']);
|
||||||
|
|
||||||
|
input.value = 'updatedValue';
|
||||||
|
dispatchEvent(input, 'input');
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(sortedClassList(input)).toEqual(['ng-dirty', 'ng-touched', 'ng-valid']);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('should set status classes with ngModelGroup and ngForm', async(() => {
|
it('should set status classes with ngModelGroup and ngForm', async(() => {
|
||||||
const fixture = TestBed.createComponent(NgModelGroupForm);
|
const fixture = TestBed.createComponent(NgModelGroupForm);
|
||||||
fixture.componentInstance.first = '';
|
fixture.componentInstance.first = '';
|
||||||
|
@ -883,7 +906,7 @@ export function main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'standalone-ng-model',
|
selector: 'standalone-ng-model',
|
||||||
|
@ -1096,6 +1119,23 @@ class NgModelMultipleValidators {
|
||||||
pattern: string;
|
pattern: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[ng-async-validator]',
|
||||||
|
providers: [
|
||||||
|
{provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => NgAsyncValidator), multi: true}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
class NgAsyncValidator implements Validator {
|
||||||
|
validate(c: AbstractControl) { return Promise.resolve(null); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-model-async-validation',
|
||||||
|
template: `<input name="async" ngModel ng-async-validator>`
|
||||||
|
})
|
||||||
|
class NgModelAsyncValidation {
|
||||||
|
}
|
||||||
|
|
||||||
function sortedClassList(el: HTMLElement) {
|
function sortedClassList(el: HTMLElement) {
|
||||||
const l = getDOM().classList(el);
|
const l = getDOM().classList(el);
|
||||||
l.sort();
|
l.sort();
|
||||||
|
|
Loading…
Reference in New Issue