|
| 1 | +import { |
| 2 | + computed, |
| 3 | + Directive, |
| 4 | + effect, |
| 5 | + forwardRef, |
| 6 | + inject, |
| 7 | + Injector, |
| 8 | + input, |
| 9 | + linkedSignal, |
| 10 | + signal, |
| 11 | + untracked, |
| 12 | + type DoCheck, |
| 13 | +} from '@angular/core'; |
| 14 | +import { FormGroupDirective, NgControl, NgForm } from '@angular/forms'; |
| 15 | +import { BrnFormFieldControl } from '@spartan-ng/brain/form-field'; |
| 16 | +import { ErrorStateMatcher, ErrorStateTracker } from '@spartan-ng/brain/forms'; |
| 17 | +import { classes } from '@spartan-ng/helm/utils'; |
| 18 | +import { cva, type VariantProps } from 'class-variance-authority'; |
| 19 | +import type { ClassValue } from 'clsx'; |
| 20 | + |
| 21 | +export const inputVariants = cva( |
| 22 | + 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', |
| 23 | + { |
| 24 | + variants: { |
| 25 | + error: { |
| 26 | + auto: '[&.ng-invalid.ng-touched]:border-destructive [&.ng-invalid.ng-touched]:ring-destructive/20 dark:[&.ng-invalid.ng-touched]:ring-destructive/40', |
| 27 | + true: 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', |
| 28 | + }, |
| 29 | + }, |
| 30 | + defaultVariants: { |
| 31 | + error: 'auto', |
| 32 | + }, |
| 33 | + }, |
| 34 | +); |
| 35 | +type InputVariants = VariantProps<typeof inputVariants>; |
| 36 | + |
| 37 | +@Directive({ |
| 38 | + selector: '[hlmInput]', |
| 39 | + providers: [ |
| 40 | + { |
| 41 | + provide: BrnFormFieldControl, |
| 42 | + useExisting: forwardRef(() => HlmInput), |
| 43 | + }, |
| 44 | + ], |
| 45 | +}) |
| 46 | +export class HlmInput implements BrnFormFieldControl, DoCheck { |
| 47 | + private readonly _injector = inject(Injector); |
| 48 | + private readonly _additionalClasses = signal<ClassValue>(''); |
| 49 | + |
| 50 | + private readonly _errorStateTracker: ErrorStateTracker; |
| 51 | + |
| 52 | + private readonly _defaultErrorStateMatcher = inject(ErrorStateMatcher); |
| 53 | + private readonly _parentForm = inject(NgForm, { optional: true }); |
| 54 | + private readonly _parentFormGroup = inject(FormGroupDirective, { optional: true }); |
| 55 | + |
| 56 | + public readonly error = input<InputVariants['error']>('auto'); |
| 57 | + |
| 58 | + protected readonly _state = linkedSignal(() => ({ error: this.error() })); |
| 59 | + |
| 60 | + public readonly ngControl: NgControl | null = this._injector.get(NgControl, null); |
| 61 | + |
| 62 | + public readonly errorState = computed(() => this._errorStateTracker.errorState()); |
| 63 | + |
| 64 | + constructor() { |
| 65 | + this._errorStateTracker = new ErrorStateTracker( |
| 66 | + this._defaultErrorStateMatcher, |
| 67 | + this.ngControl, |
| 68 | + this._parentFormGroup, |
| 69 | + this._parentForm, |
| 70 | + ); |
| 71 | + |
| 72 | + classes(() => [inputVariants({ error: this._state().error }), this._additionalClasses()]); |
| 73 | + |
| 74 | + effect(() => { |
| 75 | + const error = this._errorStateTracker.errorState(); |
| 76 | + untracked(() => { |
| 77 | + if (this.ngControl) { |
| 78 | + const shouldShowError = error && this.ngControl.invalid && (this.ngControl.touched || this.ngControl.dirty); |
| 79 | + this._errorStateTracker.errorState.set(shouldShowError ? true : false); |
| 80 | + this.setError(shouldShowError ? true : 'auto'); |
| 81 | + } |
| 82 | + }); |
| 83 | + }); |
| 84 | + } |
| 85 | + |
| 86 | + ngDoCheck() { |
| 87 | + this._errorStateTracker.updateErrorState(); |
| 88 | + } |
| 89 | + |
| 90 | + setError(error: InputVariants['error']) { |
| 91 | + this._state.set({ error }); |
| 92 | + } |
| 93 | + |
| 94 | + setClass(classes: string): void { |
| 95 | + this._additionalClasses.set(classes); |
| 96 | + } |
| 97 | +} |
0 commit comments