Skip to content

Commit 5659a2e

Browse files
authored
fix(input): update styles and asterisk on dynamically added/removed validator #10010 (#10023)
1 parent f3c2e08 commit 5659a2e

File tree

2 files changed

+135
-2
lines changed

2 files changed

+135
-2
lines changed

projects/igniteui-angular/src/lib/directives/input/input.directive.spec.ts

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { Component, ViewChild, ViewChildren, QueryList, DebugElement } from '@angular/core';
22
import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing';
3-
import { FormsModule, FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
3+
import { FormsModule, FormBuilder, ReactiveFormsModule, Validators, FormControl, FormGroup } from '@angular/forms';
44
import { By } from '@angular/platform-browser';
55
import { IgxInputGroupComponent, IgxInputGroupModule } from '../../input-group/input-group.component';
66
import { IgxInputDirective, IgxInputState } from './input.directive';
77
import { configureTestSuite } from '../../test-utils/configure-suite';
88

99
const INPUT_CSS_CLASS = 'igx-input-group__input';
10+
const CSS_CLASS_INPUT_GROUP_LABEL = 'igx-input-group__label';
1011
const TEXTAREA_CSS_CLASS = 'igx-input-group__textarea';
1112

1213
const INPUT_GROUP_FOCUSED_CSS_CLASS = 'igx-input-group--focused';
@@ -35,7 +36,8 @@ describe('IgxInput', () => {
3536
DataBoundDisabledInputWithoutValueComponent,
3637
ReactiveFormComponent,
3738
InputsWithSameNameAttributesComponent,
38-
ToggleRequiredWithNgModelInputComponent
39+
ToggleRequiredWithNgModelInputComponent,
40+
InputReactiveFormComponent
3941
],
4042
imports: [
4143
IgxInputGroupModule,
@@ -668,6 +670,69 @@ describe('IgxInput', () => {
668670
igxInput.value = 'Test';
669671
expect(igxInput.value).toBe('Test');
670672
});
673+
674+
it('Should properly initialize when used as a reactive form control - without initial validators/toggle validators', fakeAsync(() => {
675+
const fix = TestBed.createComponent(InputReactiveFormComponent);
676+
fix.detectChanges();
677+
// 1) check if label's --required class and its asterisk are applied
678+
const dom = fix.debugElement;
679+
const input = fix.componentInstance.input;
680+
const inputGroup = fix.componentInstance.igxInputGroup.element.nativeElement;
681+
const formGroup: FormGroup = fix.componentInstance.reactiveForm;
682+
683+
// interaction test - expect actual asterisk
684+
// The only way to get a pseudo elements like :before OR :after is to use getComputedStyle(element [, pseudoElt]),
685+
// as these are not in the actual DOM
686+
let asterisk = window.getComputedStyle(dom.query(By.css('.' + CSS_CLASS_INPUT_GROUP_LABEL)).nativeElement, ':after').content;
687+
expect(asterisk).toBe('"*"');
688+
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true);
689+
690+
// 2) check that input group's --invalid class is NOT applied
691+
expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(false);
692+
693+
// interaction test - focus&blur, so the --invalid and --required classes are applied
694+
// *Use markAsTouched() instead of user interaction ( calling focus + blur) because:
695+
// Angular handles blur and marks the component as touched, however:
696+
// in order to ensure Angular handles blur prior to our blur handler (where we check touched),
697+
// we have to call blur twice.
698+
fix.debugElement.componentInstance.markAsTouched();
699+
tick();
700+
fix.detectChanges();
701+
702+
expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(true);
703+
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true);
704+
705+
// 3) Check if the input group's --invalid and --required classes are removed when validator is dynamically cleared
706+
fix.componentInstance.removeValidators(formGroup);
707+
fix.detectChanges();
708+
tick();
709+
710+
const formReference = fix.componentInstance.reactiveForm.controls.fullName;
711+
// interaction test - expect no asterisk
712+
asterisk = window.getComputedStyle(dom.query(By.css('.' + CSS_CLASS_INPUT_GROUP_LABEL)).nativeElement, ':after').content;
713+
expect(formReference).toBeDefined();
714+
expect(input).toBeDefined();
715+
expect(input.nativeElement.value).toEqual('');
716+
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toEqual(false);
717+
expect(asterisk).toBe('none');
718+
expect(input.valid).toEqual(IgxInputState.INITIAL);
719+
720+
// interact with the input and expect no changes
721+
input.nativeElement.dispatchEvent(new Event('focus'));
722+
input.nativeElement.dispatchEvent(new Event('blur'));
723+
tick();
724+
fix.detectChanges();
725+
expect(input.valid).toEqual(IgxInputState.INITIAL);
726+
727+
// Re-add all Validators
728+
fix.componentInstance.addValidators(formGroup);
729+
fix.detectChanges();
730+
731+
expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true);
732+
// interaction test - expect actual asterisk
733+
asterisk = window.getComputedStyle(dom.query(By.css('.' + CSS_CLASS_INPUT_GROUP_LABEL)).nativeElement, ':after').content;
734+
expect(asterisk).toBe('"*"');
735+
}));
671736
});
672737

673738
@Component({
@@ -922,6 +987,67 @@ class ToggleRequiredWithNgModelInputComponent {
922987
public data1 = '';
923988
public isRequired = false;
924989
}
990+
@Component({
991+
template: `
992+
<form [formGroup]="reactiveForm" (ngSubmit)="onSubmitReactive()">
993+
<igx-input-group #igxInputGroup>
994+
<input igxInput #inputReactive name="fullName" type="text" formControlName="fullName" />
995+
<label igxLabel for="fullName">Full Name</label>
996+
<igx-suffix>
997+
<igx-icon>person</igx-icon>
998+
</igx-suffix>
999+
</igx-input-group>
1000+
</form>
1001+
`
1002+
})
1003+
1004+
class InputReactiveFormComponent {
1005+
@ViewChild('igxInputGroup', { static: true }) public igxInputGroup: IgxInputGroupComponent;
1006+
@ViewChild('inputReactive', { read: IgxInputDirective }) public input: IgxInputDirective;
1007+
public reactiveForm: FormGroup;
1008+
1009+
public validationType = {
1010+
fullName: [Validators.required]
1011+
};
1012+
1013+
constructor(fb: FormBuilder) {
1014+
this.reactiveForm = fb.group({
1015+
fullName: new FormControl('', Validators.required)
1016+
});
1017+
}
1018+
public onSubmitReactive() { }
1019+
1020+
public removeValidators(form: FormGroup) {
1021+
for (const key in form.controls) {
1022+
if (form.controls.hasOwnProperty(key)) {
1023+
form.get(key).clearValidators();
1024+
form.get(key).updateValueAndValidity();
1025+
}
1026+
}
1027+
}
1028+
1029+
public addValidators(form: FormGroup) {
1030+
for (const key in form.controls) {
1031+
if (form.controls.hasOwnProperty(key)) {
1032+
form.get(key).setValidators(this.validationType[key]);
1033+
form.get(key).updateValueAndValidity();
1034+
}
1035+
}
1036+
}
1037+
1038+
public markAsTouched() {
1039+
if (!this.reactiveForm.valid) {
1040+
for (const key in this.reactiveForm.controls) {
1041+
if (this.reactiveForm.controls.hasOwnProperty(key)) {
1042+
if (this.reactiveForm.controls[key]) {
1043+
this.reactiveForm.controls[key].markAsTouched();
1044+
this.reactiveForm.controls[key].updateValueAndValidity();
1045+
}
1046+
}
1047+
}
1048+
}
1049+
}
1050+
}
9251051

9261052
const testRequiredValidation = (inputElement, fixture) => {
9271053
dispatchInputEvent('focus', inputElement, fixture);

projects/igniteui-angular/src/lib/directives/input/input.directive.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy {
350350
protected updateValidityState() {
351351
if (this.ngControl) {
352352
if (this.ngControl.control.validator || this.ngControl.control.asyncValidator) {
353+
// Run the validation with empty object to check if required is enabled.
354+
const error = this.ngControl.control.validator({} as AbstractControl);
355+
this.inputGroup.isRequired = error && error.required;
353356
if (!this.disabled && (this.ngControl.control.touched || this.ngControl.control.dirty)) {
354357
// the control is not disabled and is touched or dirty
355358
this._valid = this.ngControl.invalid ?
@@ -360,6 +363,10 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy {
360363
// with the input or when form/control is reset
361364
this._valid = IgxInputState.INITIAL;
362365
}
366+
} else {
367+
// If validator is dynamically cleared, reset label's required class(asterisk) and IgxInputState #10010
368+
this._valid = IgxInputState.INITIAL;
369+
this.inputGroup.isRequired = false;
363370
}
364371
} else {
365372
this.checkNativeValidity();

0 commit comments

Comments
 (0)