/** * FormSelect Component * * A reusable select/dropdown component that handles: * - Label * - Controller (react-hook-form) * - Select with options * - Error handling * - Loading states * * This component reduces code duplication for select fields across forms. */ import React from 'react'; import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; // ============================================================================= // TYPES // ============================================================================= export interface SelectOption { value: string | number; label: string; disabled?: boolean; } export interface FormSelectProps< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath > { // Form control control: Control; name: TName; // Field configuration label?: string; placeholder?: string; required?: boolean; // Options options: SelectOption[]; // Styling className?: string; labelClassName?: string; selectClassName?: string; errorClassName?: string; // Validation validation?: { required?: boolean | string; }; // Additional props disabled?: boolean; size?: 'sm' | 'md' | 'lg'; loading?: boolean; // Custom render functions renderOption?: (option: SelectOption) => React.ReactElement; renderTrigger?: (props: { field: any; fieldState: { error?: FieldError }; }) => React.ReactElement; } // ============================================================================= // COMPONENT // ============================================================================= export function FormSelect< TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath >({ control, name, label, placeholder = 'Select an option', required = false, options, className = '', labelClassName = '', selectClassName = '', errorClassName = '', validation, disabled = false, size = 'md', loading = false, renderOption, renderTrigger, }: FormSelectProps) { // ============================================================================= // HELPER FUNCTIONS // ============================================================================= const getSelectSize = (size: 'sm' | 'md' | 'lg') => { switch (size) { case 'sm': return 'h-8 text-sm'; case 'lg': return 'h-12 text-base'; default: return 'h-10 text-sm'; } }; // ============================================================================= // RENDER FUNCTIONS // ============================================================================= const renderDefaultOption = (option: SelectOption) => ( {option.label} ); const renderDefaultTrigger = (field: any, fieldState: { error?: FieldError }) => ( ); // ============================================================================= // MAIN RENDER // ============================================================================= return (
{/* Label */} {label && ( )} {/* Controller */} (
{/* Error Message */} {fieldState.error && (

{fieldState.error.message}

)}
)} />
); } export default FormSelect;