Skip to content

Commit

Permalink
fix(editors): update validity state sequence - 14.2.x (#13058)
Browse files Browse the repository at this point in the history
* fix(input): update validity state sequence

* fix(combo): update validity state sequence

* fix(date-picker): update validity state sequence

* fix(date-range): update validity state sequence

* fix(select): update validity state sequence

* fix(time-picker): update validity state sequence

* chore(editors): simplify if-else statements
  • Loading branch information
ddaribo committed Jul 4, 2023
1 parent 37f4eea commit 95f7620
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 59 deletions.
17 changes: 12 additions & 5 deletions projects/igniteui-angular/src/lib/combo/combo.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1223,12 +1223,11 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
}

protected onStatusChanged = () => {
if ((this.ngControl.control.touched || this.ngControl.control.dirty) &&
(this.ngControl.control.validator || this.ngControl.control.asyncValidator)) {
if (!this.collapsed || this.inputGroup.isFocused) {
this.valid = this.ngControl.invalid ? IgxComboState.INVALID : IgxComboState.VALID;
if (this.ngControl && this.isTouchedOrDirty && !this.disabled) {
if (this.hasValidators && (!this.collapsed || this.inputGroup.isFocused)) {
this.valid = this.ngControl.valid ? IgxComboState.VALID : IgxComboState.INVALID;
} else {
this.valid = this.ngControl.invalid ? IgxComboState.INVALID : IgxComboState.INITIAL;
this.valid = this.ngControl.valid ? IgxComboState.INITIAL : IgxComboState.INVALID;
}
} else {
// B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
Expand All @@ -1237,6 +1236,14 @@ export abstract class IgxComboBaseDirective extends DisplayDensityBase implement
this.manageRequiredAsterisk();
};

private get isTouchedOrDirty(): boolean {
return (this.ngControl.control.touched || this.ngControl.control.dirty);
}

private get hasValidators(): boolean {
return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
}

/** if there is a valueKey - map the keys to data items, else - just return the keys */
protected convertKeysToItems(keys: any[]) {
if (this.comboAPI.valueKey === null) {
Expand Down
27 changes: 27 additions & 0 deletions projects/igniteui-angular/src/lib/combo/combo.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const CSS_CLASS_HEADER_COMPACT = 'igx-drop-down__header--compact';
const CSS_CLASS_INPUT_COSY = 'igx-input-group--cosy';
const CSS_CLASS_INPUT_COMPACT = 'igx-input-group--compact';
const CSS_CLASS_INPUT_COMFORTABLE = 'igx-input-group--comfortable';
const CSS_CLASS_INPUT_GROUP_REQUIRED = 'igx-input-group--required';
const CSS_CLASS_INPUT_GROUP_INVALID = 'igx-input-group--invalid';
const CSS_CLASS_EMPTY = 'igx-combo__empty';
const CSS_CLASS_ITEM_CHECKBOX = 'igx-combo__checkbox';
const CSS_CLASS_ITME_CHECKBOX_CHECKED = 'igx-checkbox--checked';
Expand Down Expand Up @@ -3138,6 +3140,31 @@ describe('igxCombo', () => {
expect(asterisk).toBe('"*"');
expect(inputGroupIsRequiredClass).toBeDefined();
});
it('Should update validity state when programmatically setting errors on reactive form controls', fakeAsync(() => {
const form = fixture.componentInstance.reactiveForm;

form.markAllAsTouched();
form.get('townCombo').setErrors({ error: true });
fixture.detectChanges();

expect((combo as any).comboInput.valid).toBe(IgxInputState.INVALID);
expect((combo as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((combo as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(true);

// remove the validators and set errors
form.get('townCombo').clearValidators();
form.markAsUntouched();
fixture.detectChanges();

form.markAllAsTouched();
form.get('townCombo').setErrors({ error: true });
fixture.detectChanges();

// no validator, but there is a set error
expect((combo as any).comboInput.valid).toBe(IgxInputState.INVALID);
expect((combo as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((combo as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(false);
}));
});
describe('Template form tests: ', () => {
let inputGroupRequired: DebugElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const DATE_PICKER_TOGGLE_ICON = 'today';
const DATE_PICKER_CLEAR_ICON = 'clear';

const CSS_CLASS_INPUT_GROUP_REQUIRED = 'igx-input-group--required';
const CSS_CLASS_INPUT_GROUP_INVALID = 'igx-input-group--invalid ';
const CSS_CLASS_INPUT_GROUP_INVALID = 'igx-input-group--invalid';
const CSS_CLASS_INPUT_GROUP_LABEL = 'igx-input-group__label';

describe('IgxDatePicker', () => {
Expand Down Expand Up @@ -349,6 +349,36 @@ describe('IgxDatePicker', () => {
fixture.detectChanges();
expect((datePicker as any).inputDirective.valid).toBe(IgxInputState.INITIAL);
});

it('should update validity state when programmatically setting errors on reactive form controls', () => {
fixture = TestBed.createComponent(IgxDatePickerReactiveFormComponent);
fixture.detectChanges();
datePicker = fixture.componentInstance.datePicker;
const form = (fixture.componentInstance as IgxDatePickerReactiveFormComponent).form as UntypedFormGroup;

// the form control has validators
form.markAllAsTouched();
form.get('date').setErrors({ error: true });
fixture.detectChanges();

expect((datePicker as any).inputDirective.valid).toBe(IgxInputState.INVALID);
expect((datePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((datePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(true);

// remove the validators and set errors
(fixture.componentInstance as IgxDatePickerReactiveFormComponent).removeValidators();
form.markAsUntouched();
fixture.detectChanges();

form.markAllAsTouched();
form.get('date').setErrors({ error: true });
fixture.detectChanges();

// no validator, but there is a set error
expect((datePicker as any).inputDirective.valid).toBe(IgxInputState.INVALID);
expect((datePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((datePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(false);
});
});

describe('Projected elements', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -796,23 +796,22 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr
private updateValidity() {
// B.P. 18 May 2021: IgxDatePicker does not reset its state upon resetForm #9526
if (this._ngControl && !this.disabled && this.isTouchedOrDirty) {
if (this.inputGroup.isFocused) {
this.inputDirective.valid = this._ngControl.valid
? IgxInputState.VALID
: IgxInputState.INVALID;
if (this.hasValidators && this.inputGroup.isFocused) {
this.inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
} else {
this.inputDirective.valid = this._ngControl.valid
? IgxInputState.INITIAL
: IgxInputState.INVALID;
this.inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
} else {
this.inputDirective.valid = IgxInputState.INITIAL;
}
}

private get isTouchedOrDirty(): boolean {
return (this._ngControl.control.touched || this._ngControl.control.dirty)
&& (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
return (this._ngControl.control.touched || this._ngControl.control.dirty);
}

private get hasValidators(): boolean {
return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
}

private onStatusChanged = () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const DEFAULT_ICON_TEXT = 'date_range';
const DEFAULT_FORMAT_OPTIONS = { day: '2-digit', month: '2-digit', year: 'numeric' };
const CSS_CLASS_INPUT_GROUP = '.igx-input-group__bundle';
const CSS_CLASS_INPUT = '.igx-input-group__input';
const CSS_CLASS_INPUT_GROUP_REQUIRED = 'igx-input-group--required';
const CSS_CLASS_INPUT_GROUP_INVALID = 'igx-input-group--invalid';
const CSS_CLASS_CALENDAR = 'igx-calendar';
const CSS_CLASS_ICON = 'igx-icon';
const CSS_CLASS_DONE_BUTTON = 'igx-button--flat';
Expand Down Expand Up @@ -790,6 +792,39 @@ describe('IgxDateRangePicker', () => {
fix.detectChanges();
expect(dateRangePicker.inputDirective.valid).toBe(IgxInputState.INITIAL);
});

it('should update validity state when programmatically setting errors on reactive form controls', fakeAsync(() => {
const fix = TestBed.createComponent(DateRangeReactiveFormComponent);
tick(500);
fix.detectChanges();
const dateRangePicker = fix.componentInstance.dateRange;
const form = fix.componentInstance.form;

// the form control has validators
form.markAllAsTouched();
form.get('range').setErrors({ error: true });
tick();
fix.detectChanges();

expect((dateRangePicker as any).inputDirective.valid).toBe(IgxInputState.INVALID);
expect((dateRangePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((dateRangePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(true);
expect((dateRangePicker as any).required).toBe(true);

// remove the validators and set errors
form.controls['range'].clearValidators();
form.controls['range'].updateValueAndValidity();

form.markAllAsTouched();
form.get('range').setErrors({ error: true });
tick(500);
fix.detectChanges();
tick();

expect((dateRangePicker as any).inputDirective.valid).toBe(IgxInputState.INVALID);
expect((dateRangePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_INVALID)).toBe(true);
expect((dateRangePicker as any).inputGroup.element.nativeElement.classList.contains(CSS_CLASS_INPUT_GROUP_REQUIRED)).toBe(false);
}));
});

describe('Two Inputs', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -650,23 +650,34 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective

protected onStatusChanged = () => {
if (this.inputGroup) {
this.inputDirective.valid = this.isTouchedOrDirty && !this._ngControl.disabled
? this.getInputState(this.inputGroup.isFocused)
: IgxInputState.INITIAL;
this.setValidityState(this.inputDirective, this.inputGroup.isFocused);
} else if (this.hasProjectedInputs) {
this.projectedInputs
.forEach(i => {
i.inputDirective.valid = this.isTouchedOrDirty && !this._ngControl.disabled
? this.getInputState(i.isFocused)
: IgxInputState.INITIAL;;
.forEach((i) => {
this.setValidityState(i.inputDirective, i.isFocused);
});
}
this.setRequiredToInputs();
};

private setValidityState(inputDirective: IgxInputDirective, isFocused: boolean) {
if (this._ngControl && !this._ngControl.disabled && this.isTouchedOrDirty) {
if (this.hasValidators && isFocused) {
inputDirective.valid = this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
} else {
inputDirective.valid = this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
} else {
inputDirective.valid = IgxInputState.INITIAL;
}
}

private get isTouchedOrDirty(): boolean {
return (this._ngControl.control.touched || this._ngControl.control.dirty)
&& (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
return (this._ngControl.control.touched || this._ngControl.control.dirty);
}

private get hasValidators(): boolean {
return (!!this._ngControl.control.validator || !!this._ngControl.control.asyncValidator);
}

private handleSelection(selectionData: Date[]): void {
Expand Down Expand Up @@ -776,14 +787,6 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective
}
}

private getInputState(focused: boolean): IgxInputState {
if (focused) {
return this._ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
} else {
return this._ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
}

private setRequiredToInputs(): void {
// workaround for igxInput setting required
Promise.resolve().then(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,72 @@ describe('IgxInput', () => {
expect(igxInput.value).toEqual('');
expect(model.inputValue).toEqual('');
});

it('Should update validity state when programmatically setting errors on reactive form controls', fakeAsync(() => {
const fix = TestBed.createComponent(InputReactiveFormComponent);
fix.detectChanges();

const inputGroup = fix.componentInstance.igxInputGroup.element.nativeElement;
const formGroup = fix.componentInstance.reactiveForm;

// the form control has validators
formGroup.markAllAsTouched();
formGroup.get('fullName').setErrors({ error: true });
fix.detectChanges();

expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(true);
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true);

// remove the validators and check the same
fix.componentInstance.removeValidators(formGroup);
formGroup.markAsUntouched();
tick();
fix.detectChanges();

expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(false);
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(false);

formGroup.markAllAsTouched();
formGroup.get('fullName').setErrors({ error: true });
fix.detectChanges();

// no validator, but there is a set error
expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(true);
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(false);
}));

it('should keep state as initial on type when there are no errors and validators on reactive form controls', fakeAsync(() => {
const fix = TestBed.createComponent(InputReactiveFormComponent);
fix.detectChanges();

const formGroup = fix.componentInstance.reactiveForm;

fix.componentInstance.removeValidators(formGroup);
formGroup.markAsUntouched();
fix.detectChanges();

const igxInput = fix.componentInstance.input;
const inputElement = fix.debugElement.query(By.directive(IgxInputDirective)).nativeElement;

dispatchInputEvent('focus', inputElement, fix);
dispatchInputEvent('blur', inputElement, fix);

const inputGroupElement = fix.debugElement.query(By.css('igx-input-group')).nativeElement;
expect(inputGroupElement.classList.contains(INPUT_GROUP_VALID_CSS_CLASS)).toBe(false);
expect(inputGroupElement.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(false);
expect(inputGroupElement.classList.contains(INPUT_GROUP_FILLED_CSS_CLASS)).toBe(false);
expect(igxInput.valid).toBe(IgxInputState.INITIAL);

dispatchInputEvent('focus', inputElement, fix);
igxInput.value = 'test';
fix.detectChanges();

expect(inputGroupElement.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(false);
expect(inputGroupElement.classList.contains(INPUT_GROUP_VALID_CSS_CLASS)).toBe(false);
expect(inputGroupElement.classList.contains(INPUT_GROUP_FILLED_CSS_CLASS)).toBe(true);

expect(igxInput.valid).toBe(IgxInputState.INITIAL);
}));
});

@Component({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,24 +368,23 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy {
*/
protected updateValidityState() {
if (this.ngControl) {
if (this.ngControl.control.validator || this.ngControl.control.asyncValidator) {
// Run the validation with empty object to check if required is enabled.
const error = this.ngControl.control.validator({} as AbstractControl);
this.inputGroup.isRequired = error && error.required;
if (!this.disabled && (this.ngControl.control.touched || this.ngControl.control.dirty)) {
// the control is not disabled and is touched or dirty
this._valid = this.ngControl.invalid ?
IgxInputState.INVALID : this.focused ? IgxInputState.VALID :
IgxInputState.INITIAL;
if (!this.disabled && this.isTouchedOrDirty) {
if (this.hasValidators) {
// Run the validation with empty object to check if required is enabled.
const error = this.ngControl.control.validator({} as AbstractControl);
this.inputGroup.isRequired = error && error.required;
if (this.focused) {
this._valid = this.ngControl.valid ? IgxInputState.VALID : IgxInputState.INVALID;
} else {
this._valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
} else {
// if control is untouched, pristine, or disabled its state is initial. This is when user did not interact
// with the input or when form/control is reset
this._valid = IgxInputState.INITIAL;
// If validator is dynamically cleared, reset label's required class(asterisk) and IgxInputState #10010
this.inputGroup.isRequired = false;
this._valid = this.ngControl.valid ? IgxInputState.INITIAL : IgxInputState.INVALID;
}
} else {
// If validator is dynamically cleared, reset label's required class(asterisk) and IgxInputState #10010
this._valid = IgxInputState.INITIAL;
this.inputGroup.isRequired = false;
}
this.renderer.setAttribute(this.nativeElement, 'aria-required', this.required.toString());
const ariaInvalid = this.valid === IgxInputState.INVALID;
Expand All @@ -394,6 +393,15 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy {
this.checkNativeValidity();
}
}

private get isTouchedOrDirty(): boolean {
return (this.ngControl.control.touched || this.ngControl.control.dirty);
}

private get hasValidators(): boolean {
return (!!this.ngControl.control.validator || !!this.ngControl.control.asyncValidator);
}

/**
* Gets whether the igxInput has a placeholder.
*
Expand Down

0 comments on commit 95f7620

Please sign in to comment.