diff --git a/projects/igniteui-angular/src/lib/combo/combo.common.ts b/projects/igniteui-angular/src/lib/combo/combo.common.ts index e78a67c49b4..6b8c879a316 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.common.ts +++ b/projects/igniteui-angular/src/lib/combo/combo.common.ts @@ -1300,6 +1300,11 @@ export abstract class IgxComboBaseDirective implements IgxComboBase, AfterViewCh this.manageRequiredAsterisk(); }; + /** @hidden @internal */ + protected externalValidate(): IgxInputState { + return this._valid; + } + private updateValidity() { if (this.ngControl && this.ngControl.invalid) { this.valid = IgxInputState.INVALID; diff --git a/projects/igniteui-angular/src/lib/combo/combo.component.html b/projects/igniteui-angular/src/lib/combo/combo.component.html index 9e795a6f3d3..d61e4750354 100644 --- a/projects/igniteui-angular/src/lib/combo/combo.component.html +++ b/projects/igniteui-angular/src/lib/combo/combo.component.html @@ -9,6 +9,7 @@ { it('should add/remove asterisk when setting validators dynamically', () => { let inputGroupIsRequiredClass = fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUTGROUP_REQUIRED)); let asterisk = window.getComputedStyle(fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUTGROUP_LABEL)).nativeElement, ':after').content; + input = fixture.debugElement.query(By.css(`.${CSS_CLASS_COMBO_INPUTGROUP}`)); expect(asterisk).toBe('"*"'); expect(inputGroupIsRequiredClass).toBeDefined(); + expect(input.nativeElement.getAttribute('required')).not.toBeNull(); fixture.componentInstance.reactiveForm.controls.townCombo.clearValidators(); fixture.componentInstance.reactiveForm.controls.townCombo.updateValueAndValidity(); @@ -3446,6 +3448,7 @@ describe('igxCombo', () => { asterisk = window.getComputedStyle(fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUTGROUP_LABEL)).nativeElement, ':after').content; expect(asterisk).toBe('none'); expect(inputGroupIsRequiredClass).toBeNull(); + expect(input.nativeElement.getAttribute('required')).toBeNull(); fixture.componentInstance.reactiveForm.controls.townCombo.setValidators(Validators.required); fixture.componentInstance.reactiveForm.controls.townCombo.updateValueAndValidity(); @@ -3454,6 +3457,7 @@ describe('igxCombo', () => { asterisk = window.getComputedStyle(fixture.debugElement.query(By.css('.' + CSS_CLASS_INPUTGROUP_LABEL)).nativeElement, ':after').content; expect(asterisk).toBe('"*"'); expect(inputGroupIsRequiredClass).toBeDefined(); + expect(input.nativeElement.getAttribute('required')).not.toBeNull(); }); it('Should update validity state when programmatically setting errors on reactive form controls', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.html b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.html index 81159a032bd..3771ae42017 100644 --- a/projects/igniteui-angular/src/lib/date-picker/date-picker.component.html +++ b/projects/igniteui-angular/src/lib/date-picker/date-picker.component.html @@ -15,7 +15,7 @@ } { expect(datePicker).toBeDefined(); expect(inputGroup.isRequired).toBeTruthy(); + expect((datePicker as any).inputDirective.nativeElement.getAttribute('required')).not.toBeNull(); }); it('should update inputGroup isRequired correctly', () => { const inputGroup = (datePicker as any).inputGroup; + const inputEl = (datePicker as any).inputDirective.nativeElement; expect(datePicker).toBeDefined(); expect(inputGroup.isRequired).toBeTruthy(); + expect(inputEl.getAttribute('required')).not.toBeNull(); (fixture.componentInstance as IgxDatePickerNgModelComponent).isRequired = false; fixture.detectChanges(); expect(inputGroup.isRequired).toBeFalsy(); + expect(inputEl.getAttribute('required')).toBeNull(); }); it('should set validity to initial when the form is reset', fakeAsync(() => { diff --git a/projects/igniteui-angular/src/lib/directives/input/input.directive.spec.ts b/projects/igniteui-angular/src/lib/directives/input/input.directive.spec.ts index 5142e7030fd..dcb2cbe4a69 100644 --- a/projects/igniteui-angular/src/lib/directives/input/input.directive.spec.ts +++ b/projects/igniteui-angular/src/lib/directives/input/input.directive.spec.ts @@ -702,7 +702,7 @@ describe('IgxInput', () => { let asterisk = window.getComputedStyle(dom.query(By.css('.' + CSS_CLASS_INPUT_GROUP_LABEL)).nativeElement, ':after').content; expect(asterisk).toBe('"*"'); expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true); - expect(input.nativeElement.attributes.getNamedItem('aria-required').nodeValue).toEqual('true'); + expect(input.nativeElement.getAttribute('required')).not.toBeNull(); // 2) check that input group's --invalid class is NOT applied expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(false); @@ -718,10 +718,13 @@ describe('IgxInput', () => { expect(inputGroup.classList.contains(INPUT_GROUP_INVALID_CSS_CLASS)).toBe(true); expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toBe(true); - expect(input.nativeElement.attributes.getNamedItem('aria-required').nodeValue).toEqual('true'); + expect(input.nativeElement.getAttribute('required')).not.toBeNull(); // 3) Check if the input group's --invalid and --required classes are removed when validator is dynamically cleared fix.componentInstance.removeValidators(formGroup); + // the component cannot both take required on the input and validators. If validators write required, their removal + // cannot cause required to be removed as it may have been part of the initial setup + input.nativeElement.removeAttribute('required'); fix.detectChanges(); tick(); @@ -731,7 +734,7 @@ describe('IgxInput', () => { expect(formReference).toBeDefined(); expect(input).toBeDefined(); expect(input.nativeElement.value).toEqual(''); - expect(input.nativeElement.attributes.getNamedItem('aria-required').nodeValue).toEqual('false'); + expect(input.nativeElement.getAttribute('required')).toBeNull(); expect(inputGroup.classList.contains(INPUT_GROUP_REQUIRED_CSS_CLASS)).toEqual(false); expect(asterisk).toBe('none'); expect(input.valid).toEqual(IgxInputState.INITIAL); @@ -751,7 +754,7 @@ describe('IgxInput', () => { // interaction test - expect actual asterisk asterisk = window.getComputedStyle(dom.query(By.css('.' + CSS_CLASS_INPUT_GROUP_LABEL)).nativeElement, ':after').content; expect(asterisk).toBe('"*"'); - expect(input.nativeElement.attributes.getNamedItem('aria-required').nodeValue).toEqual('true'); + expect(input.nativeElement.getAttribute('required')).not.toBeNull(); })); it('should not hold old file input value in form after clearing the input', () => { diff --git a/projects/igniteui-angular/src/lib/directives/input/input.directive.ts b/projects/igniteui-angular/src/lib/directives/input/input.directive.ts index daf01c89904..2b532cb02aa 100644 --- a/projects/igniteui-angular/src/lib/directives/input/input.directive.ts +++ b/projects/igniteui-angular/src/lib/directives/input/input.directive.ts @@ -102,6 +102,8 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy { private _valueChanges$: Subscription; private _fileNames: string; private _disabled = false; + private _defaultRequired: boolean = null; + private _externalValidate: () => IgxInputState = null; constructor( public inputGroup: IgxInputGroupBase, @@ -181,35 +183,52 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy { } /** - * Sets the `required` property. - * - * @example - * ```html - * - * - * - * ``` - */ - @Input({ transform: booleanAttribute }) - public set required(value: boolean) { - this.nativeElement.required = this.inputGroup.isRequired = value; + * @hidden @internal + * Sets a function to validate the input externally. + * This function should return an `IgxInputState` value. + */ + @Input() + public set externalValidate(fn: () => IgxInputState) { + this._externalValidate = fn; + } + + public get externalValidate(): () => IgxInputState { + return this._externalValidate; } /** - * Gets whether the igxInput is required. + * Gets/Sets whether the igxInput is required. * * @example * ```typescript - * let isRequired = this.igxInput.required; + * + * + * * ``` */ + @Input({ transform: booleanAttribute }) public get required() { let validation; if (this.ngControl && (this.ngControl.control.validator || this.ngControl.control.asyncValidator)) { validation = this.ngControl.control.validator({} as AbstractControl); } - return validation && validation.required || this.nativeElement.hasAttribute('required'); + let required; + if (validation && validation.required !== undefined) { + required = validation.required; + } else { + required = this.nativeElement.required; + } + return required; + } + public set required(value: boolean) { + this.nativeElement.required = this.inputGroup.isRequired = value; + } + + @HostBinding('attr.required') + public get hostBindingRequired(): string { + return this.required ? '' : null; } + /** * @hidden * @internal @@ -293,8 +312,6 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy { this.inputGroup.isRequired = this.required; } - this.renderer.setAttribute(this.nativeElement, 'aria-required', this.required.toString()); - const elTag = this.nativeElement.tagName.toLowerCase(); if (elTag === 'textarea') { this.isTextArea = true; @@ -367,7 +384,9 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy { * @internal */ protected updateValidityState() { - if (this.ngControl) { + if (this._externalValidate) { + this._valid = this._externalValidate(); + } else if (this.ngControl) { if (!this.disabled && this.isTouchedOrDirty) { if (this.hasValidators) { // Run the validation with empty object to check if required is enabled. @@ -386,7 +405,6 @@ export class IgxInputDirective implements AfterViewInit, OnDestroy { } else { this._valid = IgxInputState.INITIAL; } - this.renderer.setAttribute(this.nativeElement, 'aria-required', this.required.toString()); const ariaInvalid = this.valid === IgxInputState.INVALID; this.renderer.setAttribute(this.nativeElement, 'aria-invalid', ariaInvalid.toString()); } else { diff --git a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.html b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.html index 21252f137c7..69a72363d52 100644 --- a/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.html +++ b/projects/igniteui-angular/src/lib/simple-combo/simple-combo.component.html @@ -13,6 +13,7 @@