Files
igny8/frontend/src/components/form/FormFieldRenderer.tsx
2025-11-09 10:27:02 +00:00

168 lines
4.7 KiB
TypeScript

/**
* FormFieldRenderer - Dynamic form field renderer
* Renders form fields based on configuration objects
*
* Usage:
* ```typescript
* <FormFieldRenderer
* fields={formFieldsConfig}
* values={formValues}
* onChange={handleFieldChange}
* />
* ```
*/
import React from 'react';
import Input from './input/InputField';
import SelectDropdown from './SelectDropdown';
import Label from './Label';
export interface FormFieldConfig {
key: string;
label: string;
type: 'text' | 'number' | 'email' | 'password' | 'select' | 'textarea';
placeholder?: string;
options?: Array<{ value: string; label: string }>;
required?: boolean;
min?: number;
max?: number;
rows?: number;
className?: string;
gridCols?: 1 | 2; // For inline fields (e.g., Volume & Difficulty)
}
interface FormFieldRendererProps {
fields: FormFieldConfig[];
values: Record<string, any>;
onChange: (key: string, value: any) => void;
errors?: Record<string, string>;
disabled?: boolean;
}
export default function FormFieldRenderer({
fields,
values,
onChange,
errors = {},
disabled = false,
}: FormFieldRendererProps) {
// Group fields by grid layout
const fieldGroups: FormFieldConfig[][] = [];
let currentGroup: FormFieldConfig[] = [];
fields.forEach((field) => {
if (field.gridCols === 2) {
// Start new group for inline fields
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
currentGroup.push(field);
if (currentGroup.length === 2) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
} else {
// Full-width field
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
currentGroup = [];
}
fieldGroups.push([field]);
}
});
if (currentGroup.length > 0) {
fieldGroups.push(currentGroup);
}
const renderField = (field: FormFieldConfig) => {
const value = values[field.key] || '';
const error = errors[field.key];
const fieldId = `field-${field.key}`;
return (
<div key={field.key} className={field.className}>
<Label htmlFor={fieldId} className="mb-2">
{field.label}
{field.required && <span className="text-error-500 ml-1">*</span>}
</Label>
{field.type === 'select' ? (
<SelectDropdown
options={field.options || []}
placeholder={field.placeholder || field.label}
value={value}
onChange={(val) => onChange(field.key, val)}
className="w-full"
/>
) : field.type === 'textarea' ? (
<textarea
id={fieldId}
className={`w-full rounded-lg border ${
error
? 'border-error-500'
: 'border-gray-300 dark:border-gray-700'
} bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800`}
value={value}
onChange={(e) => onChange(field.key, e.target.value)}
placeholder={field.placeholder}
required={field.required}
rows={field.rows || 4}
disabled={disabled}
/>
) : (
<Input
id={fieldId}
type={field.type}
value={value}
onChange={(e) => {
const newValue =
field.type === 'number'
? e.target.value === ''
? ''
: parseInt(e.target.value) || 0
: e.target.value;
onChange(field.key, newValue);
}}
placeholder={field.placeholder}
required={field.required}
min={field.min}
max={field.max}
error={!!error}
disabled={disabled}
className="w-full"
/>
)}
{error && (
<p className="mt-1 text-sm text-error-500">{error}</p>
)}
</div>
);
};
return (
<div className="space-y-4">
{fieldGroups.map((group, groupIndex) => {
if (group.length === 2 && group[0].gridCols === 2) {
// Render inline fields
return (
<div key={`group-${groupIndex}`} className="grid grid-cols-2 gap-4">
{group.map((field) => renderField(field))}
</div>
);
} else {
// Render full-width field
return (
<div key={`group-${groupIndex}`}>
{group.map((field) => renderField(field))}
</div>
);
}
})}
</div>
);
}