import { AfterViewInit, ChangeDetectorRef, Directive, ElementRef, HostBinding, HostListener, Inject, Input, OnDestroy, Optional, Renderer2, Self, } from '@angular/core'; import { AbstractControl, NgControl, NgModel } from '@angular/forms'; import { Subscription } from 'rxjs'; import { IgxInputGroupBase } from '../../input-group/input-group.common'; const nativeValidationAttributes = [ 'required', 'pattern', 'minlength', 'maxlength', 'min', 'max', 'step', ]; export enum IgxInputState { INITIAL, VALID, INVALID, } /** * The `igxInput` directive creates single- or multiline text elements, covering common scenarios when dealing with form inputs. * * @igxModule IgxInputGroupModule * * @igxParent Data Entry & Display * * @igxTheme igx-input-group-theme * * @igxKeywords input, input group, form, field, validation * * @igxGroup presentation * * @example * ```html * * * * * ``` */ @Directive({ selector: '[igxInput]', exportAs: 'igxInput', standalone: true }) export class IgxInputDirective implements AfterViewInit, OnDestroy { private static ngAcceptInputType_required: boolean | ''; private static ngAcceptInputType_disabled: boolean | ''; /** * Sets/gets whether the `"igx-input-group__input"` class is added to the host element. * Default value is `false`. * * @example * ```typescript * this.igxInput.isInput = true; * ``` * * @example * ```typescript * let isCLassAdded = this.igxInput.isInput; * ``` */ @HostBinding('class.igx-input-group__input') public isInput = false; /** * Sets/gets whether the `"class.igx-input-group__textarea"` class is added to the host element. * Default value is `false`. * * @example * ```typescript * this.igxInput.isTextArea = true; * ``` * * @example * ```typescript * let isCLassAdded = this.igxInput.isTextArea; * ``` */ @HostBinding('class.igx-input-group__textarea') public isTextArea = false; private _valid = IgxInputState.INITIAL; private _statusChanges$: Subscription; private _valueChanges$: Subscription; private _fileNames: string; private _disabled = false; constructor( public inputGroup: IgxInputGroupBase, @Optional() @Self() @Inject(NgModel) protected ngModel: NgModel, @Optional() @Self() @Inject(NgControl) protected formControl: NgControl, protected element: ElementRef, protected cdr: ChangeDetectorRef, protected renderer: Renderer2 ) { } private get ngControl(): NgControl { return this.ngModel ? this.ngModel : this.formControl; } /** * Sets the `value` property. * * @example * ```html * * * * ``` */ @Input() public set value(value: any) { this.nativeElement.value = value ?? ''; this.updateValidityState(); } /** * Gets the `value` property. * * @example * ```typescript * @ViewChild('igxInput', {read: IgxInputDirective}) * public igxInput: IgxInputDirective; * let inputValue = this.igxInput.value; * ``` */ public get value() { return this.nativeElement.value; } /** * Sets the `disabled` property. * * @example * ```html * * * * ``` */ @Input() @HostBinding('disabled') public set disabled(value: boolean) { this._disabled = this.inputGroup.disabled = !!((value as any === '') || value); if (this.focused && this._disabled) { // Browser focus may not fire in good time and mess with change detection, adjust here in advance: this.inputGroup.isFocused = false; } } /** * Gets the `disabled` property * * @example * ```typescript * @ViewChild('igxInput', {read: IgxInputDirective}) * public igxInput: IgxInputDirective; * let isDisabled = this.igxInput.disabled; * ``` */ public get disabled() { return this._disabled; } /** * Sets the `required` property. * * @example * ```html * * * * ``` */ @Input() public set required(value: boolean) { this.nativeElement.required = this.inputGroup.isRequired = (value as any === '') || value; } /** * Gets whether the igxInput is required. * * @example * ```typescript * let isRequired = this.igxInput.required; * ``` */ 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'); } /** * @hidden * @internal */ @HostListener('focus') public onFocus() { this.inputGroup.isFocused = true; } /** * @param event The event to invoke the handler * * @hidden * @internal */ @HostListener('blur') public onBlur() { this.inputGroup.isFocused = false; this.updateValidityState(); } /** @hidden @internal */ @HostListener('input') public onInput() { this.checkNativeValidity(); } /** @hidden @internal */ @HostListener('change', ['$event']) public change(event: Event) { if (this.type === 'file') { const fileList: FileList | null = (event.target as HTMLInputElement) .files; const fileArray: File[] = []; if (fileList) { for (const file of Array.from(fileList)) { fileArray.push(file); } } this._fileNames = (fileArray || []).map((f: File) => f.name).join(', '); if (this.required && fileList?.length > 0) { this._valid = IgxInputState.INITIAL; } } } /** @hidden @internal */ public get fileNames() { return this._fileNames; } /** @hidden @internal */ public clear() { this.ngControl?.control?.setValue(''); this.nativeElement.value = null; this._fileNames = ''; } /** @hidden @internal */ public ngAfterViewInit() { this.inputGroup.hasPlaceholder = this.nativeElement.hasAttribute( 'placeholder' ); if (this.ngControl && this.ngControl.disabled !== null) { this.disabled = this.ngControl.disabled; } this.inputGroup.disabled = this.inputGroup.disabled || this.nativeElement.hasAttribute('disabled'); this.inputGroup.isRequired = this.nativeElement.hasAttribute( 'required' ); // Make sure we do not invalidate the input on init if (!this.ngControl) { this._valid = IgxInputState.INITIAL; } // Also check the control's validators for required if (this.required && !this.inputGroup.isRequired) { 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; } else { this.isInput = true; } if (this.ngControl) { this._statusChanges$ = this.ngControl.statusChanges.subscribe( this.onStatusChanged.bind(this) ); this._valueChanges$ = this.ngControl.valueChanges.subscribe( this.onValueChanged.bind(this) ); } this.cdr.detectChanges(); } /** @hidden @internal */ public ngOnDestroy() { if (this._statusChanges$) { this._statusChanges$.unsubscribe(); } if (this._valueChanges$) { this._valueChanges$.unsubscribe(); } } /** * Sets a focus on the igxInput. * * @example * ```typescript * this.igxInput.focus(); * ``` */ public focus() { this.nativeElement.focus(); } /** * Gets the `nativeElement` of the igxInput. * * @example * ```typescript * let igxInputNativeElement = this.igxInput.nativeElement; * ``` */ public get nativeElement() { return this.element.nativeElement; } /** @hidden @internal */ protected onStatusChanged() { // Enable/Disable control based on ngControl #7086 if (this.disabled !== this.ngControl.disabled) { this.disabled = this.ngControl.disabled; } this.updateValidityState(); } /** @hidden @internal */ protected onValueChanged() { if (this._fileNames && !this.value) { this._fileNames = ''; } } /** * @hidden * @internal */ protected updateValidityState() { if (this.ngControl) { 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 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 { 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 { 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. * * @example * ```typescript * let hasPlaceholder = this.igxInput.hasPlaceholder; * ``` */ public get hasPlaceholder() { return this.nativeElement.hasAttribute('placeholder'); } /** * Gets the placeholder element of the igxInput. * * @example * ```typescript * let igxInputPlaceholder = this.igxInput.placeholder; * ``` */ public get placeholder() { return this.nativeElement.placeholder; } /** * @returns An indicator of whether the input has validator attributes or not * * @hidden * @internal */ private _hasValidators(): boolean { for (const nativeValidationAttribute of nativeValidationAttributes) { if (this.nativeElement.hasAttribute(nativeValidationAttribute)) { return true; } } return false; } /** * Gets whether the igxInput is focused. * * @example * ```typescript * let isFocused = this.igxInput.focused; * ``` */ public get focused() { return this.inputGroup.isFocused; } /** * Gets the state of the igxInput. * * @example * ```typescript * let igxInputState = this.igxInput.valid; * ``` */ public get valid(): IgxInputState { return this._valid; } /** * Sets the state of the igxInput. * * @example * ```typescript * this.igxInput.valid = IgxInputState.INVALID; * ``` */ public set valid(value: IgxInputState) { this._valid = value; } /** * Gets whether the igxInput is valid. * * @example * ```typescript * let valid = this.igxInput.isValid; * ``` */ public get isValid(): boolean { return this.valid !== IgxInputState.INVALID; } /** * A function to assign a native validity property of an input. * This should be used when there's no ngControl * * @hidden * @internal */ private checkNativeValidity() { if (!this.disabled && this._hasValidators()) { this._valid = this.nativeElement.checkValidity() ? this.focused ? IgxInputState.VALID : IgxInputState.INITIAL : IgxInputState.INVALID; } } /** * Returns the input type. * * @hidden * @internal */ public get type() { return this.nativeElement.type; } }