import { Component, ChangeDetectorRef, Input, ViewChild, forwardRef, Output, EventEmitter, OnInit, AfterViewInit, OnDestroy, inject, } from "@angular/core"; import { NG_VALUE_ACCESSOR, ControlValueAccessor, FormControl, Validators, ValidatorFn, } from "@angular/forms"; import { Subject, Subscription } from "rxjs"; import { takeUntil, filter } from "rxjs/operators"; import { LabelWrapper } from "../../wrappers/label-wrapper"; import { SamFormService, SamFormEvent } from "../../form-service"; export interface SamTextFocusEvent { type: "focus" | "blur"; event: Event; } export interface SamTextChangeEvent { type: "input" | "change"; event: Event; } /** * The component provides a text input form control */ @Component({ selector: "sam-text", templateUrl: "text.template.html", providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SamTextComponent), multi: true, }, ], standalone: false, }) export class SamTextComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnDestroy { private samFormService = inject(SamFormService); private cdr = inject(ChangeDetectorRef); /** * Sets the text input value */ @Input() public value = ""; /** * Sets the label text */ @Input() public label: string; /** * Sets the id attribute */ @Input() public id: string; /** * Sets the name attribute */ @Input() public name: string; /** * Sets the helpful hint text */ @Input() public hint: string; /** * Sets the general error message */ @Input() public errorMessage: string; /** * Sets the disabled attribute */ @Input() public disabled: boolean; /** * Sets the required attribute */ @Input() public required: boolean; /** * Passes in the Angular FormControl */ @Input() public control: FormControl; /** * Sets the maxlength attribute */ @Input() public maxlength: number; /** * Toggles validations to display with SamFormService events */ @Input() public useFormService: boolean; /** * Optional text to be displayed when the text area is empty */ @Input() public placeholder: string; /** * Sets the title attribute on the input for accessibility */ @Input() public title: string; /** * Sets full hint toggle option for label wrapper */ @Input() public showFullHint: boolean = false; /** * Changes the HTML event the changes emit on */ @Input() public emitOn: "input" | "change" = "input"; /** * (deprecated) Lose focus event emit */ // eslint-disable-next-line @angular-eslint/no-output-on-prefix -- renaming is a breaking change for consumers already bound to (onBlur) @Output() public onBlur = new EventEmitter(); /** * Lose focus event emit */ // eslint-disable-next-line @angular-eslint/no-output-native -- renaming is a breaking change for consumers already bound to (blur) @Output() public blur = new EventEmitter(); @ViewChild(LabelWrapper, { static: true }) public wrapper: LabelWrapper; public onChange: (value: string) => void = () => null; public onTouched: () => void = () => null; public focusEvent = new Subject(); public changeEvent = new Subject(); private _focusSubscription: Subscription; private _changeSubsription: Subscription; private ngUnsubscribe = new Subject(); public ngOnInit(): void { this._validateInputs(); this._setupSubscriptions(); this._setupFormControl(); } public ngAfterViewInit(): void { if (this.control) { this.wrapper.formatErrors(this.control); this.cdr.detectChanges(); } } public ngOnDestroy(): void { this._unsubscribe(); this.cdr.detach(); } public onLoseFocus(): void { this._trimWhitespace(); this.onBlur.emit(true); this.blur.emit(true); } public registerOnChange(fn: (value: string) => void): void { this.onChange = fn; } public registerOnTouched(fn: () => void): void { this.onTouched = fn; } public setDisabledState(disabled: boolean): void { this.disabled = disabled; } public writeValue(value: string | null): void { this.value = value !== null ? "" + value : ""; if (!this.cdr["destroyed"]) { this.cdr.detectChanges(); } } private _validateInputs(): void { if (!this.name) { throw new Error( " requires a [name] parameter\ for 508 compliance" ); } } private _setupSubscriptions(): void { this._focusSubscription = this.focusEvent.subscribe( this._handleFocusEvents.bind(this) ); this._changeSubsription = this.changeEvent .pipe(filter((event) => event.type === this.emitOn)) .subscribe((e) => this._setValue.call(this, (e.event.target as HTMLInputElement).value) ); } private _setupFormControl(): void { if (!this.control) { return; } this.control.setValidators(this._getValidators()); if (!this.useFormService) { this.control.statusChanges .pipe(takeUntil(this.ngUnsubscribe)) .subscribe(() => { this.wrapper.formatErrors(this.control); this.cdr.detectChanges(); }); } else { this.samFormService.formEventsUpdated$.subscribe((evt: SamFormEvent) => { if (this._isSubmitEvent(evt)) { this.wrapper.formatErrors(this.control); } else if (this._isResetEvent(evt)) { this.wrapper.clearError(); } }); } } private _unsubscribe(): void { this.ngUnsubscribe.next(); this.ngUnsubscribe.complete(); if (this._focusSubscription) { this._focusSubscription.unsubscribe(); } if (this._changeSubsription) { this._changeSubsription.unsubscribe(); } } private _trimWhitespace() { const trimmed = this.value.trim(); if (trimmed !== this.value) { this._setValue(trimmed); } } private _setValue(value: string): void { this.value = value; this.onChange(this.value); } private _handleFocusEvents(event: SamTextFocusEvent): void { if (event.type === "focus") { this.onTouched(); } else if (event.type === "blur") { this.onLoseFocus(); } } private _getValidators(): ValidatorFn[] { const validators: ValidatorFn[] = []; if (this.control.validator) { validators.push(this.control.validator); } if (this.required) { validators.push(Validators.required); } if (this.maxlength) { validators.push(Validators.maxLength(this.maxlength)); } return validators; } private _isSubmitEvent(evt: SamFormEvent): boolean { return ( (!evt.root || evt.root === this.control.root) && evt.eventType && evt.eventType === "submit" ); } private _isResetEvent(evt: SamFormEvent): boolean { return ( (!evt.root || evt.root === this.control.root) && evt.eventType && evt.eventType === "reset" ); } }