Issue
Explantion
I'm creating a dynamic input form using the FormProvider from react-hook-form.
const formMethods = useForm<TSongFormData>();
return (
<FormProvider {...formMethods}>
<SongInputForm />
</FormProvider>
)
In the SongInputForm
component I have my actual form which will have the input components which I have created for every input elements
like input
teaxtarea
and so on...
const SongForm = () => {
const { handleSubmit } = useFormContext<TSongFormData>();
return (
<form
onSubmit={handleSubmit((data) => console.log("data", data))}
>
<InputElement label="Song Name" name="name" />
<InputElement label="Author" name="author" />
<input type="submit" />
</form>
);
};
}
The InputElement
ccomponent would receive all the props
for the input
element itself and the name
prop would be a union type TSongFormDataKey
of all the keys of the form data.
interface ITextInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
name: TSongFormDataKey;
}
const InputElement = ({ name, label, ...restProps }: ITextInputProps) => {
const { register } = useFormContext<TSongFormData>();
return (
<div>
<label htmlFor={name}>{label}</label>
<input id={name} {...restProps} {...register(name)} />
</div>
);
};
Question
As of now, I have hardcoded the types TSongFormData
and TSongFormDataKey
in the InputElement
component.
But how do I pass both the types TSongFormData
and TSongFormDataKey
as generics to the InputElement
component so that I can make it dynamic and have more flexibility on the type of props passed to it??
What I'm looking for is something like this:
interface ITextInputProps<T> extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
name: T;
}
const InputElement = <T,K>({ name, label, ...restProps }: ITextInputProps<T>) => {
const { register } = useFormContext<K>();
return (
<div>
<label htmlFor={name}>{label}</label>
<input id={name} {...restProps} {...register(name)} />
</div>
);
};
where T
would be TSongFormData
and K
would be TSongFormDataKey
.
I've created a codesandbox if anyone wants to play with it: https://codesandbox.io/s/elastic-sanne-uz3ei8
I'm trying to new to typescript and trying to get my head around generics, but finding it so hard.
Any help would be greatly appreciated. Thanks
Solution
You can make the ITextInputProps
type generic and the InputElement
name generic. For name
we can use the Path
generic type from useFormContext
. keyof T
would also work, except register
expects a Path<T>
, and while that will include keyof T
, while Path
still has unresolved type parameters (such as T
) typescript won't be able to follow that relationship.
When you instantiate the component, you will have to specify the argument explicitly: <InputElement<TSongFormData> label="Song Name" name="name" />
You can also create a specialized version of input field for a specific type using an instantiation expression in newer versions of TS:
const SongInputElement = InputElement<TSongFormData>
Putting it all together we get:
interface ITextInputProps<T> extends React.InputHTMLAttributes<HTMLInputElement> {
label?: string;
name: Path<T>;
}
const InputElement = <T extends FieldValues>({ name, label, ...restProps }: ITextInputProps<T>) => {
const { register } = useFormContext<T>();
return (
<div>
<label htmlFor={name}>{label}</label>
<input id={name} {...restProps} {...register(name)} />
</div>
);
};
const SongForm = () => {
const { watch, handleSubmit } = useFormContext<TSongFormData>();
return (
<InputElement<TSongFormData> label="Author" name="author" />
);
};
const SongFormV2 = () => {
const { watch, handleSubmit } = useFormContext<TSongFormData>();
const SongInputElement = InputElement<TSongFormData>
return (
<SongInputElement label="Song Name" name="name" />
)
};
Answered By - Titian Cernicova-Dragomir
Answer Checked By - - Senaida (ReactFix Volunteer)