import { BooleanInput } from '@angular/cdk/coercion'; import { booleanAttribute, ChangeDetectionStrategy, Component, computed, effect, EffectRef, inject, input, OnDestroy, } from '@angular/core'; import { BrnField, BrnFieldA11yService } from '@spartan-ng/brain/field'; import { classes } from '@spartan-ng/styles/utils'; @Component({ selector: 'hlm-field-error', changeDetection: ChangeDetectionStrategy.OnPush, host: { role: 'alert', 'data-slot': 'field-error', '[attr.id]': 'id()', '[hidden]': '!_display()', }, template: ` @if (_display()) { } `, }) export class HlmFieldError implements OnDestroy { private static _id = 0; private readonly _field = inject(BrnField, { optional: true }); private readonly _a11y = inject(BrnFieldA11yService, { optional: true, host: true }); private _registeredId?: string | undefined; private readonly _hasParentField = !!this._field; /** The unique ID for the field error. If none is supplied, it will be auto-generated. */ public readonly id = input(`hlm-field-error-${HlmFieldError._id++}`); /** * The name of the specific validator error key to match (e.g. 'required'). * When omitted, the error is shown if any validation error is present. */ public readonly validator = input(); /** Forces the error message to be visible regardless of the control's validation state. */ public readonly forceShow = input(false, { transform: booleanAttribute }); protected readonly _display = computed( () => !this._hasParentField || this.forceShow() || this._hasError(), ); protected readonly _hasError = computed(() => { const errors = this._field?.errors(); if (!errors) return false; const validator = this.validator(); const spartanInvalid = this._field?.controlState()?.spartanInvalid; if (!spartanInvalid) return false; return validator ? validator in errors : Object.keys(errors).length > 0; }); private readonly _cleanup: EffectRef | null = this._a11y ? effect(() => { const a11y = this._a11y; if (!a11y) return; const id = this.id(); const hasError = this._hasError(); if (this._registeredId && (this._registeredId !== id || !hasError)) { a11y.unregisterError(this._registeredId); this._registeredId = undefined; } if (hasError && this._registeredId !== id) { a11y.registerError(id); this._registeredId = id; } }) : null; constructor() { classes(() => 'text-destructive text-sm font-normal'); } ngOnDestroy() { this._cleanup?.destroy(); if (this._registeredId) { this._a11y?.unregisterError(this._registeredId); } } }