import { ChangeDetectionStrategy, Component, ElementRef, computed, effect, input, model, output, signal, untracked, viewChild } from '@angular/core'; import { NgClass } from '@angular/common'; export type AppInputType = | 'search' | 'user' | 'text' | 'number' | 'email' | 'password' | 'password-toggle' | 'currency' | 'percent' | 'date' | 'period' | 'editable' | 'file'; const DEFAULT_TYPE: AppInputType = 'text'; const INPUT_TYPES = new Set([ 'search', 'user', 'text', 'number', 'email', 'password', 'password-toggle', 'currency', 'percent', 'date', 'period', 'editable', 'file' ]); function normalizeType(value: AppInputType | string | undefined): AppInputType { return value && INPUT_TYPES.has(value as AppInputType) ? (value as AppInputType) : DEFAULT_TYPE; } @Component({ selector: 'app-input', imports: [NgClass], templateUrl: './input.component.html', styleUrl: './input.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'form-group' } }) export class InputComponent { private readonly inputElement = viewChild>('inputElement'); private readonly selectedFileName = signal(''); private readonly editableDisabled = signal(false); private readonly passwordVisible = signal(false); private lastClearTrigger = 0; private readonly defaultId = `app-input-${globalThis.crypto.randomUUID()}`; readonly invalid = input(false); readonly id = input(this.defaultId); readonly placeholder = input(''); readonly value = model(''); readonly maxlength = input(null); readonly accept = input(''); readonly fileName = input(''); readonly clearTrigger = input(0); readonly disabled = input(false); readonly visible = input(null); readonly type = input(DEFAULT_TYPE, { transform: normalizeType }); readonly disabledChange = output(); readonly fileChange = output(); readonly pasteEvent = output(); readonly visibleChange = output(); protected readonly inputType = computed(() => { const type = this.type(); if (type === 'password-toggle') { return this.isPasswordVisible() ? 'text' : 'password'; } if ( type === 'search' || type === 'user' || type === 'currency' || type === 'percent' || type === 'editable' ) { return 'text'; } if (type === 'period') { return 'month'; } return type; }); protected readonly inputClasses = computed(() => ({ 'form-control': true, 'is-invalid': this.invalid(), 'input-with-search-icon': this.type() === 'search', 'input-with-action-icon': this.type() === 'user' || this.type() === 'password-toggle', 'input-with-prefix': this.type() === 'currency', 'input-with-suffix': this.type() === 'percent', 'input-with-icon': this.type() === 'editable' })); protected readonly isInputDisabled = computed(() => this.type() === 'editable' ? this.editableDisabled() : this.disabled() ); protected readonly isEditableActive = computed( () => this.type() === 'editable' && !this.editableDisabled() ); protected readonly isPasswordVisible = computed(() => { const visible = this.visible(); return visible === null ? this.passwordVisible() : visible; }); protected readonly resolvedFileName = computed( () => this.selectedFileName() || this.fileName() || '' ); constructor() { effect(() => { this.editableDisabled.set(this.disabled()); }); effect(() => { if (this.type() !== 'file') { return; } const clearTrigger = this.clearTrigger(); const value = this.value(); const fileName = this.fileName(); untracked(() => { if (clearTrigger !== this.lastClearTrigger) { this.lastClearTrigger = clearTrigger; this.clearFileInput(); return; } if (!value && !fileName) { this.clearFileInput(); } }); }); } focus(): void { this.inputElement()?.nativeElement.focus(); } protected onValueChange(event: Event): void { this.value.set((event.target as HTMLInputElement).value); } protected onFileChange(event: Event): void { const file = (event.target as HTMLInputElement).files?.[0] ?? null; this.selectedFileName.set(file?.name ?? ''); this.fileChange.emit(file); } protected onPaste(event: ClipboardEvent): void { this.pasteEvent.emit(event); } protected toggleEditableState(): void { if (this.type() !== 'editable') { return; } const nextState = !this.editableDisabled(); this.editableDisabled.set(nextState); this.disabledChange.emit(nextState); } protected togglePasswordVisibility(): void { if (this.type() !== 'password-toggle' || this.disabled()) { return; } const nextState = !this.isPasswordVisible(); if (this.visible() === null) { this.passwordVisible.set(nextState); } this.visibleChange.emit(nextState); } private clearFileInput(): void { this.inputElement()?.nativeElement && (this.inputElement()!.nativeElement.value = ''); this.selectedFileName.set(''); } }