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 new to typescript and trying to get my head around generics, but finding it so hard.
Any help would be greatly appreciated. Thanks
CodePudding user response:
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" />
)
};