import { booleanAttribute, ChangeDetectionStrategy, Component, computed, forwardRef, input, model, signal, } from '@angular/core'; import { type ControlValueAccessor, NG_VALUE_ACCESSOR, } from '@angular/forms'; import { FORM_AFFIX_CLASS, FORM_CONTROL_INNER_CLASS, FORM_DISABLED_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FIELD_ERROR_CLASS, FORM_HINT_CLASS, FORM_LABEL_CLASS, } from '../form-field.classes'; let nextTextInputId = 0; /** * Native `type` values supported by {@link TextInputComponent}. * * Keep this list to single-line text controls — multiline is * {@link TextareaComponent}, numeric is {@link NumberInputComponent}. */ export type TextInputType = | 'text' | 'email' | 'password' | 'search' | 'tel' | 'url'; /** * Single-line text field — **reference** form control for `@africanies/africanies-ui`. * * Follows the shared AFRICANIES form field pattern: label, hint, field-level * `error` (distinct from `ErrorStateComponent`), prefix/suffix projection * slots, and {@link ControlValueAccessor} plus `value` / `valueChange` for * template bindings. * * @example * ```html * * * * * * * * * * * ``` */ @Component({ selector: 'africanies-text-input', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true, }, ], template: `
@if (error(); as err) {

{{ err }}

} @else if (hint(); as h) {

{{ h }}

} `, }) /** * Single-line text field — **reference** form control for `@africanies/africanies-ui`. */ export class TextInputComponent implements ControlValueAccessor { /** Stable ids so label / aria-describedby stay paired across re-renders. */ protected readonly controlId = `africanies-text-input-${++nextTextInputId}`; protected readonly hintId = `${this.controlId}-hint`; protected readonly errorId = `${this.controlId}-error`; protected readonly labelClass = FORM_LABEL_CLASS; protected readonly affixClass = FORM_AFFIX_CLASS; protected readonly innerClass = FORM_CONTROL_INNER_CLASS; protected readonly hintClass = FORM_HINT_CLASS; protected readonly errorClass = FORM_ERROR_CLASS; /** * Visible field label. */ readonly label = input(''); /** * Helper copy under the field. Hidden while {@link error} is set (error * replaces hint visually). */ readonly hint = input(undefined); /** * Field-level validation message (`string | null`). Not an async page error — * use `ErrorStateComponent` for failed fetches. */ readonly error = input(null); /** * Native placeholder when the value is empty. */ readonly placeholder = input(''); /** * Native input type. Defaults to `text`. Use `password` / `email` for * auth fields; toggle visibility by binding `type` between `password` and * `text`. */ readonly type = input('text'); /** * Native `autocomplete` hint (e.g. `email`, `current-password`). * Omitted from the DOM when empty. */ readonly autocomplete = input(''); /** * Current text. Supports `[(value)]` and Reactive Forms via CVA. */ readonly value = model(''); /** * Optional host disable (in addition to CVA `setDisabledState`). */ readonly disabledInput = input(false, { alias: 'disabled', transform: booleanAttribute, }); protected readonly cvaDisabled = signal(false); protected readonly disabled = computed( () => this.disabledInput() || this.cvaDisabled(), ); protected readonly shellClass = computed(() => { let classes = FORM_FIELD_CLASS; if (this.error()) { classes += ` ${FORM_FIELD_ERROR_CLASS}`; } if (this.disabled()) { classes += ` ${FORM_DISABLED_CLASS}`; } return classes; }); protected readonly describedBy = computed(() => { if (this.error()) { return this.errorId; } if (this.hint()) { return this.hintId; } return null; }); private onChange: (value: string) => void = () => undefined; private onTouched: () => void = () => undefined; /** * @param value - Form model value; `null` becomes `''`. */ writeValue(value: string | null): void { this.value.set(value ?? ''); } /** * @param fn - Called when the user edits the field. */ registerOnChange(fn: (value: string) => void): void { this.onChange = fn; } /** * @param fn - Called on blur. */ registerOnTouched(fn: () => void): void { this.onTouched = fn; } /** * @param isDisabled - When true, blocks editing. */ setDisabledState(isDisabled: boolean): void { this.cvaDisabled.set(isDisabled); } protected onInput(event: Event): void { const next = (event.target as HTMLInputElement).value; this.value.set(next); this.onChange(next); } protected onBlur(): void { this.onTouched(); } }