diff --git a/modules/@angular/forms/src/directives/ng_control_status.ts b/modules/@angular/forms/src/directives/ng_control_status.ts index ef4074a4f2..b5efea689c 100644 --- a/modules/@angular/forms/src/directives/ng_control_status.ts +++ b/modules/@angular/forms/src/directives/ng_control_status.ts @@ -37,6 +37,9 @@ export class AbstractControlStatus { get ngClassInvalid(): boolean { 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 = { @@ -45,7 +48,8 @@ export const ngControlStatusHost = { '[class.ng-pristine]': 'ngClassPristine', '[class.ng-dirty]': 'ngClassDirty', '[class.ng-valid]': 'ngClassValid', - '[class.ng-invalid]': 'ngClassInvalid' + '[class.ng-invalid]': 'ngClassInvalid', + '[class.ng-pending]': 'ngClassPending' }; /** diff --git a/modules/@angular/forms/test/reactive_integration_spec.ts b/modules/@angular/forms/test/reactive_integration_spec.ts index 3e63ac92af..e5fbf9f9a4 100644 --- a/modules/@angular/forms/test/reactive_integration_spec.ts +++ b/modules/@angular/forms/test/reactive_integration_spec.ts @@ -646,6 +646,60 @@ export function main() { 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', () => { const fixture = TestBed.createComponent(FormGroupComp); const form = new FormGroup({'login': new FormControl('', Validators.required)}); @@ -1736,7 +1790,7 @@ class LoginIsEmptyValidator { }] }) 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); } } diff --git a/modules/@angular/forms/test/template_integration_spec.ts b/modules/@angular/forms/test/template_integration_spec.ts index a71802258d..7d3c183319 100644 --- a/modules/@angular/forms/test/template_integration_spec.ts +++ b/modules/@angular/forms/test/template_integration_spec.ts @@ -6,9 +6,9 @@ * 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 {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 {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {dispatchEvent} from '@angular/platform-browser/testing/browser_util'; @@ -22,7 +22,8 @@ export function main() { StandaloneNgModel, NgModelForm, NgModelGroupForm, NgModelValidBinding, NgModelNgIfForm, NgModelRadioForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName, NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper, - NgModelValidationBindings, NgModelMultipleValidators + NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator, + NgModelAsyncValidation ], imports: [FormsModule] }); @@ -139,7 +140,6 @@ export function main() { fixture.detectChanges(); 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']); 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(() => { const fixture = TestBed.createComponent(NgModelGroupForm); fixture.componentInstance.first = ''; @@ -883,7 +906,7 @@ export function main() { }); }); -}; +} @Component({ selector: 'standalone-ng-model', @@ -1096,6 +1119,23 @@ class NgModelMultipleValidators { 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: `` +}) +class NgModelAsyncValidation { +} + function sortedClassList(el: HTMLElement) { const l = getDOM().classList(el); l.sort();