Long post warning - apologies for all the code. I'm trying to be as specific as possible because I've been hacking away at this one for hours. I'd love any help with it. Thank you in advance!
I'm using Mantine UI (https://mantine.dev) and React Hook Form (https://react-hook-form.com) to create some custom components. In particular, I'm using RHF's Controller component to automatically register my inputs.
Many of these components look very similar. For example, here's the TextInput component and the DatePicker component:
TextInput:
export type RegisteredFormComponentProps<T extends FieldValues> = {
name: Path<T> // these types come from React Hoook Form
rules?: RegisterOptions
}
type TextInputProps<T extends FieldValues> = Omit<
ComponentProps<typeof MantineTextInput>,
'name'
> &
RegisteredFormComponentProps<T>
const TextInput = <T extends FieldValues>({
name,
...otherProps
}: TextInputProps<T>) => {
return (
<Controller
name={name}
rules={{
required: 'plez',
}}
render={({
field: { name, value, ref, onChange },
fieldState: { error },
}) => (
<MantineTextInput
{...otherProps}
name={name}
error={error?.message}
value={value}
styles={(theme) => ({
label: {
color: error ? theme.colors.red[5] : theme.colors.main[5],
},
})}
ref={ref}
onChange={(e) => {
onChange(e.target.value)
}}
/>
)}
/>
)
}
and the DatePicker component:
type DatePickerProps<T extends FieldValues> = Omit<
ComponentProps<typeof MantineDatePicker>,
'name'
> &
RegisteredFormComponentProps<T>
const DatePicker = <T extends FieldValues>({
name,
...otherProps
}: DatePickerProps<T>) => {
return (
<Controller
name={name}
render={({
field: { name, value, ref, onChange },
fieldState: { error },
}) => (
<MantineDatePicker
{...otherProps}
error={error?.message}
name={name}
dropdownPosition="bottom-start"
styles={(theme) => ({
label: {
color: error ? theme.colors.red[5] : theme.colors.main[5],
},
})}
ref={ref}
value={value}
onChange={(date) => {
onChange(date)
}}
/>
)}
/>
)
}
You can see that in both cases I'm rendering a Mantine component and passing through the props that it's expecting. I replace the 'name' prop from Mantine with the one from React Hook Form to prevent a clash there.
In a perfect world, I would be able to create some abstraction where all of the code that is being re-used here is only declared once. I've tried doing that with an HOC like this:
const withForm =
<
T extends FieldValues,
C extends typeof MantineDatePicker | typeof MantineNumberInput
>(
Component: C
) =>
({
name,
rules,
...otherProps
}: Omit<ComponentProps<C>, 'name'> & RegisteredFormComponentProps<T>) => {
return (
<Controller
name={name}
rules={rules}
render={({
field: { name, value, ref, onChange },
fieldState: { error },
}) => (
<Component
{...otherProps}
error={error?.message}
name={name}
dropdownPosition="bottom-start"
styles={(theme) => ({
label: {
color: error ? theme.colors.red[5] : theme.colors.main[5],
},
})}
ref={ref}
value={value}
onChange={(date) => {
onChange(date)
}}
/>
)}
/>
)
}
However, I simply can't get the typing right. In the case of the above HOC, I get the following error:
Type 'Omit<Omit<ComponentProps<C>, "name"> & RegisteredFormComponentProps<T>, "name" | "rules">
& { error: string | undefined; ... 5 more ...; onChange: (date: number | ... 2 more ... |
undefined) => void; }'
is not assignable to type 'IntrinsicAttributes & LibraryManagedAttributes<C,
DatePickerProps & RefAttributes<HTMLInputElement> & NumberInputProps>'.
There are a few issues that I'm having to face - firstly, each Mantine component involves a forward ref, so perhaps I need to type my Component in the HOC regarding that? (I've tried this, and haven't succeeded).
Likewise, the onChange
prop will have a slightly different implementation depending on which component I pass through. So I could try and add the onChange as an additional prop to the HOC?
Again, I'm just not sure how to do this. It would be really helpful for someone who's a bit better at TS than me to help out. Many thanks for reading this very long post, and please let me know if I can improve it at all.