SingleValueFieldArray.tsx141 lines · main
1import { Plus, Trash } from 'lucide-react'
2import {
3 Control,
4 FieldArray,
5 FieldArrayPath,
6 FieldArrayWithId,
7 FieldPath,
8 FieldValues,
9 useFieldArray,
10} from 'react-hook-form'
11import { Button, cn, FormControl, FormField, FormItem, FormMessage, Input } from 'ui'
12
13export interface SingleValueFieldArrayProps<
14 TFieldValues extends FieldValues,
15 TFieldArrayName extends FieldArrayPath<TFieldValues>,
16 TItem extends FieldArray<TFieldValues, TFieldArrayName> = FieldArray<
17 TFieldValues,
18 TFieldArrayName
19 >,
20> {
21 control: Control<TFieldValues>
22 name: TFieldArrayName
23 valueFieldName: Extract<keyof TItem, string>
24 createEmptyRow: () => TItem
25 placeholder: string
26 addLabel: string
27 removeLabel?: string
28 disabled?: boolean
29 minimumRows?: number
30 inputSize?: React.ComponentProps<typeof Input>['size']
31 inputAutoComplete?: string
32 className?: string
33 rowsClassName?: string
34 rowClassName?: string
35 inputClassName?: string
36 addButtonClassName?: string
37 addButtonType?: React.ComponentProps<typeof Button>['type']
38 addButtonSize?: React.ComponentProps<typeof Button>['size']
39 removeButtonClassName?: string
40 removeButtonType?: React.ComponentProps<typeof Button>['type']
41 removeButtonSize?: React.ComponentProps<typeof Button>['size']
42}
43
44const toFieldPath = <TFieldValues extends FieldValues>(path: string) => {
45 return path as FieldPath<TFieldValues>
46}
47
48export const SingleValueFieldArray = <
49 TFieldValues extends FieldValues,
50 TFieldArrayName extends FieldArrayPath<TFieldValues>,
51 TItem extends FieldArray<TFieldValues, TFieldArrayName> = FieldArray<
52 TFieldValues,
53 TFieldArrayName
54 >,
55>({
56 control,
57 name,
58 valueFieldName,
59 createEmptyRow,
60 placeholder,
61 addLabel,
62 removeLabel = 'Remove row',
63 disabled = false,
64 minimumRows = 0,
65 inputSize = 'small',
66 inputAutoComplete,
67 className,
68 rowsClassName = 'space-y-3 mt-1',
69 rowClassName,
70 inputClassName,
71 addButtonClassName,
72 addButtonType = 'default',
73 addButtonSize,
74 removeButtonClassName,
75 removeButtonType = 'default',
76 removeButtonSize = 'tiny',
77}: SingleValueFieldArrayProps<TFieldValues, TFieldArrayName, TItem>) => {
78 const { fields, append, remove } = useFieldArray<TFieldValues, TFieldArrayName, 'fieldId'>({
79 control,
80 name,
81 keyName: 'fieldId',
82 })
83
84 const typedFields = fields as FieldArrayWithId<TFieldValues, TFieldArrayName, 'fieldId'>[]
85 const disableRemove = disabled || typedFields.length <= minimumRows
86
87 return (
88 <div className={cn('space-y-3', className)}>
89 <div className={rowsClassName}>
90 {typedFields.map((field, index) => (
91 <div key={field.fieldId} className={cn('flex items-start space-x-2', rowClassName)}>
92 <FormField
93 control={control}
94 name={toFieldPath<TFieldValues>(`${name}.${index}.${valueFieldName}`)}
95 render={({ field }) => (
96 <FormItem className="flex-1">
97 <FormControl>
98 <Input
99 {...field}
100 size={inputSize}
101 autoComplete={inputAutoComplete}
102 className={cn('w-full', inputClassName)}
103 placeholder={placeholder}
104 disabled={disabled}
105 />
106 </FormControl>
107 <FormMessage />
108 </FormItem>
109 )}
110 />
111
112 <Button
113 type={removeButtonType}
114 size={removeButtonSize}
115 htmlType="button"
116 icon={<Trash size={12} />}
117 aria-label={removeLabel}
118 disabled={disableRemove}
119 onClick={() => remove(index)}
120 className={cn('h-[34px] w-[34px] shrink-0', removeButtonClassName)}
121 />
122 </div>
123 ))}
124 </div>
125
126 <div className="flex items-center">
127 <Button
128 type={addButtonType}
129 size={addButtonSize}
130 htmlType="button"
131 icon={<Plus strokeWidth={1.5} />}
132 disabled={disabled}
133 onClick={() => append(createEmptyRow())}
134 className={addButtonClassName}
135 >
136 {addLabel}
137 </Button>
138 </div>
139 </div>
140 )
141}