Skip to content

Commit

Permalink
feat(forms): add ng-pending CSS class during async validation (#11243)
Browse files Browse the repository at this point in the history
Closes #10336
  • Loading branch information
pkozlowski-opensource authored and alxhub committed Oct 19, 2016
1 parent 445e592 commit 97bc971
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 7 deletions.
6 changes: 5 additions & 1 deletion modules/@angular/forms/src/directives/ng_control_status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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'
};

/**
Expand Down
56 changes: 55 additions & 1 deletion modules/@angular/forms/test/reactive_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)});
Expand Down Expand Up @@ -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); }
}
Expand Down
50 changes: 45 additions & 5 deletions modules/@angular/forms/test/template_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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]
});
Expand Down Expand Up @@ -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');
Expand All @@ -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 = '';
Expand Down Expand Up @@ -883,7 +906,7 @@ export function main() {
});

});
};
}

@Component({
selector: 'standalone-ng-model',
Expand Down Expand Up @@ -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: `<input name="async" ngModel ng-async-validator>`
})
class NgModelAsyncValidation {
}

function sortedClassList(el: HTMLElement) {
const l = getDOM().classList(el);
l.sort();
Expand Down

0 comments on commit 97bc971

Please sign in to comment.