mediahub-fe/components/form/shared/form-checkbox.tsx

244 lines
6.7 KiB
TypeScript
Raw Normal View History

/**
* FormCheckbox Component
*
* A reusable checkbox component that handles:
* - Single checkbox
* - Checkbox groups
* - Label
* - Controller (react-hook-form)
* - Error handling
*
* This component reduces code duplication for checkbox fields across forms.
*/
import React from 'react';
import { Control, Controller, FieldError, FieldPath, FieldValues } from 'react-hook-form';
import { Label } from '@/components/ui/label';
import { Checkbox } from '@/components/ui/checkbox';
import { cn } from '@/lib/utils';
// =============================================================================
// TYPES
// =============================================================================
export interface CheckboxOption {
value: string | number;
label: string;
disabled?: boolean;
}
export interface FormCheckboxProps<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> {
// Form control
control: Control<TFieldValues>;
name: TName;
// Field configuration
label?: string;
required?: boolean;
// Checkbox configuration
single?: boolean; // Single checkbox vs checkbox group
options?: CheckboxOption[]; // For checkbox groups
// Styling
className?: string;
labelClassName?: string;
checkboxClassName?: string;
errorClassName?: string;
groupClassName?: string;
// Layout
layout?: 'horizontal' | 'vertical';
columns?: number; // For grid layout
// Validation
validation?: {
required?: boolean | string;
};
// Additional props
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
// Custom render functions
renderOption?: (option: CheckboxOption, checked: boolean, onChange: (checked: boolean) => void) => React.ReactElement;
}
// =============================================================================
// COMPONENT
// =============================================================================
export function FormCheckbox<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
control,
name,
label,
required = false,
single = false,
options = [],
className = '',
labelClassName = '',
checkboxClassName = '',
errorClassName = '',
groupClassName = '',
layout = 'horizontal',
columns = 1,
validation,
disabled = false,
size = 'md',
renderOption,
}: FormCheckboxProps<TFieldValues, TName>) {
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
const getCheckboxSize = (size: 'sm' | 'md' | 'lg') => {
switch (size) {
case 'sm':
return 'h-4 w-4';
case 'lg':
return 'h-6 w-6';
default:
return 'h-5 w-5';
}
};
const getGroupLayout = () => {
if (layout === 'vertical') {
return 'flex flex-col gap-2';
}
if (columns > 1) {
return `grid grid-cols-${columns} gap-2`;
}
return 'flex flex-wrap gap-4';
};
// =============================================================================
// RENDER FUNCTIONS
// =============================================================================
const renderSingleCheckbox = (field: any, fieldState: { error?: FieldError }) => (
<div className={cn('flex items-center space-x-2', checkboxClassName)}>
<Checkbox
id={name}
checked={field.value}
onCheckedChange={field.onChange}
disabled={disabled}
className={getCheckboxSize(size)}
/>
<Label
htmlFor={name}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
labelClassName
)}
>
{label}
</Label>
</div>
);
const renderCheckboxGroup = (field: any, fieldState: { error?: FieldError }) => {
const selectedValues = field.value || [];
const handleOptionChange = (optionValue: string | number, checked: boolean) => {
const newValues = checked
? [...selectedValues, optionValue]
: selectedValues.filter((value: any) => value !== optionValue);
field.onChange(newValues);
};
const renderDefaultOption = (option: CheckboxOption) => {
const checked = selectedValues.includes(option.value);
if (renderOption) {
return renderOption(option, checked, (checked) => handleOptionChange(option.value, checked));
}
return (
<div key={option.value} className="flex items-center space-x-2">
<Checkbox
id={`${name}-${option.value}`}
checked={checked}
onCheckedChange={(checked) => handleOptionChange(option.value, checked as boolean)}
disabled={disabled || option.disabled}
className={getCheckboxSize(size)}
/>
<Label
htmlFor={`${name}-${option.value}`}
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
labelClassName
)}
>
{option.label}
</Label>
</div>
);
};
return (
<div className={cn(getGroupLayout(), groupClassName)}>
{options.map(renderDefaultOption)}
</div>
);
};
// =============================================================================
// MAIN RENDER
// =============================================================================
return (
<div className={cn('space-y-2', className)}>
{/* Group Label */}
{label && !single && (
<Label
className={cn(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
required && 'after:content-["*"] after:ml-0.5 after:text-destructive',
labelClassName
)}
>
{label}
</Label>
)}
{/* Controller */}
<Controller
control={control}
name={name}
rules={{
required: validation?.required || required,
}}
render={({ field, fieldState }) => (
<div className="space-y-1">
{single ?
renderSingleCheckbox(field, fieldState) :
renderCheckboxGroup(field, fieldState)
}
{/* Error Message */}
{fieldState.error && (
<p className={cn(
'text-sm text-destructive',
errorClassName
)}>
{fieldState.error.message}
</p>
)}
</div>
)}
/>
</div>
);
}
export default FormCheckbox;