/** * DynamicFieldArray Component * * Add/remove form field groups dynamically with min/max constraints. * Allows users to manage repeating field groups (e.g., multiple addresses, phone numbers). */ import type React from 'react'; import { Button } from '@lilith/ui-primitives'; import styled, { type DefaultTheme } from '@lilith/ui-styled-components'; export interface DynamicFieldArrayProps { /** Array of field data */ fields: T[]; /** Callback when adding a new field */ onAdd: () => void; /** Callback when removing a field */ onRemove: (index: number) => void; /** Function to render each field */ renderField: (field: T, index: number) => React.ReactNode; /** Label for add button */ addButtonLabel?: string; /** Label for remove button */ removeButtonLabel?: string; /** Minimum number of fields */ min?: number; /** Maximum number of fields */ max?: number; } const Container = styled.div` display: flex; flex-direction: column; gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.lg}; width: 100%; `; const FieldGroup = styled.div` position: relative; padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.lg}; border: 1px solid ${(props: { theme: DefaultTheme }) => props.theme.colors.border}; border-radius: ${(props: { theme: DefaultTheme }) => props.theme.borderRadius.md}; background: ${(props: { theme: DefaultTheme }) => props.theme.colors.surface}; transition: all ${(props: { theme: DefaultTheme }) => props.theme.transitions.normal}; &:hover { border-color: ${(props: { theme: DefaultTheme }) => props.theme.colors.primary}; box-shadow: ${(props: { theme: DefaultTheme }) => props.theme.shadows.sm}; } `; const FieldHeader = styled.div` display: flex; justify-content: space-between; align-items: center; margin-bottom: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md}; `; const FieldIndex = styled.div` font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.sm}; font-weight: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontWeight.semibold}; color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.secondary}; text-transform: uppercase; letter-spacing: 0.05em; `; const FieldContent = styled.div` display: flex; flex-direction: column; gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.md}; `; const ActionButtons = styled.div` display: flex; gap: ${(props: { theme: DefaultTheme }) => props.theme.spacing.sm}; margin-top: ${(props: { theme: DefaultTheme }) => props.theme.spacing.lg}; `; const EmptyState = styled.div` padding: ${(props: { theme: DefaultTheme }) => props.theme.spacing.xl}; text-align: center; color: ${(props: { theme: DefaultTheme }) => props.theme.colors.text.secondary}; font-size: ${(props: { theme: DefaultTheme }) => props.theme.typography.fontSize.base}; border: 2px dashed ${(props: { theme: DefaultTheme }) => props.theme.colors.border}; border-radius: ${(props: { theme: DefaultTheme }) => props.theme.borderRadius.md}; `; /** * DynamicFieldArray allows adding and removing groups of form fields dynamically. * Enforces min/max constraints and provides customizable field rendering. * * @example * // Managing multiple addresses * const [addresses, setAddresses] = useState([{ street: '', city: '' }]) * * setAddresses([...addresses, { street: '', city: '' }])} * onRemove={(index) => setAddresses(addresses.filter((_, i) => i !== index))} * renderField={(field, index) => ( * <> * { * const newAddresses = [...addresses] * newAddresses[index].street = e.target.value * setAddresses(newAddresses) * }} * /> * { * const newAddresses = [...addresses] * newAddresses[index].city = e.target.value * setAddresses(newAddresses) * }} * /> * * )} * addButtonLabel="Add Address" * min={1} * max={5} * /> * * @example * // Managing phone numbers with constraints * ( * updatePhone(index, val)} /> * )} * min={1} * max={3} * /> */ export const DynamicFieldArray = ({ fields, onAdd, onRemove, renderField, addButtonLabel = 'Add Item', removeButtonLabel = 'Remove', min = 0, max, }: DynamicFieldArrayProps) => { const canAdd = max === undefined || fields.length < max; const canRemove = fields.length > min; return ( {fields.length === 0 ? ( No items added yet. Click "{addButtonLabel}" to add one. ) : ( fields.map((field, index) => ( Item {index + 1} of {fields.length} {canRemove && ( )} {renderField(field, index)} )) )} ); };