I have many forms defined with react-hook-form. Sometimes I use them independently and some other times I used them combined. Imagine I have a Task form and a Note form. Sometimes I have one form to submit a Task and sometimes I have one form to submit a Note and Task. I am trying to manage name collisions between the various forms by prefixing the input elements with a string.
I want to define a Task form and use it as <TaskForm/> when used independently and <TaskForm prefix="task"/> when combining with other forms. So far I have something working, but typescript is giving a very hard time.
Assuming a simple data type
type TaskFormData = {
title: string;
}
I would like to use a simple Form, in which there is no name clashes:
export const Form = () => {
const methods = useForm<TaskFormData>({
defaultValues: { title: 'Some input value' }
});
const { handleSubmit } = methods;
const onSubmit = (data: TaskFormData) => {
alert(JSON.stringify(data));
// should be { "title": "input value" }
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<TaskForm /> <!-- with a field description -->
</form>
</FormProvider>
);
}
Or a prefixed Form, in which I would use potentially various forms with field names clashing.
export const PrefixedForm = () => {
const methods = useForm<{ task: NestedValue<TaskFormData> }>({
defaultValues: { task: { title: 'Some input value' } }
});
const { handleSubmit } = methods;
const onSubmit = (data: { task: TaskFormData }) => {
alert(JSON.stringify(data));
// should be { "task": { "title": "input value" } }
};
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(onSubmit)}>
<NoteForm prefix="note" /> <!-- with a field note.description -->
<TaskForm prefix="task" /> <!-- with a field task.description -->
</form>
</FormProvider>
);
};
So far this is the working example of a TaskForm
export const TaskForm = ({ prefix }: { prefix?: string }) => {
const { register } = useFormContext<any>();
return (
<input {...register(prefix ? `${prefix}.title` : 'title')} />
);
};
The useFormContext<any> is really giving me a hard time here. I would like to keep the type information inside each of the forms. But can't seem to be able to get to the correct typescript.
After much research I really thought that this would work:
export type PrefixedTaskFormData<P extends string> = {
[K in keyof TaskFormData as `${P}.K`]: TaskFormData[K]
}
export const TaskForm = <P extends string>({ prefix }: { prefix?: P }) => {
const { register } = useFormContext<TaskFormData | PrefixedTaskFormData<P>>();
return (
<input {...register(prefix ? `${prefix}.title` : 'title')} />
);
};
Also tried
export type PrefixedTaskFormData<P extends string> = {
[K in P]: NestedValue<TaskFormData>
}
export const TaskForm = <P extends string>({ prefix }: { prefix?: P }) => {
const { register } = useFormContext<TaskFormData | PrefixedTaskFormData<P>>();
return (
<input {...register(prefix ? `${prefix}.title` : 'title')} />
);
};
And nothing.
Any ideas?