forge42dev / remix-hook-form

Open source wrapper for react-hook-form aimed at Remix.run
MIT License
354 stars 29 forks source link

Using react-hook-form Controller with file type input #5

Closed eovandodev closed 1 year ago

eovandodev commented 1 year ago

Great job with this awesome library! Unfortunately, I haven't been able to get it to work with a FileInput. I'm using Controller from react-hook-form and useRemixForm to handle the rest. The problem I'm facing is that whenever I submit the form, the value of the "avatar" field is always an empty object. Interestingly, on the client side, I've set up a watcher for the "avatar" field, and it is not empty. Do you have any ideas on what might be causing this issue?

Relevant code:

<Controller
    control={control} 
    name="avatar"
    render={({ field: { onChange } }) => (
      <FileInput
        w="100%"
        size="md"
        icon={<IconPhotoUp />}
        label="Your Photo"
        placeholder="Upload your profile photo"
        onChange={onChange}
        accept="image/png,image/jpeg,image/jpg,image/webp"
      />
    )}
  />

This is my squema

const schema = zod.object({
  avatar: zod.any() as zod.ZodType<File>,
  firstName: zod.string().optional(),
  lastName: zod.string().optional(),
  email: zod.string().email({ message: "Invalid email address" }),
  role: zod.string().optional(),
  country: zod.string().optional(),
  timezone: zod.string().optional(),
  username: zod.string().optional()
});
type FormData = zod.infer<typeof schema>;
const resolver = zodResolver(schema);

//on action 

const { errors: formErrors, data: formData, receivedValues } = await getValidatedFormData<FormData>(
    request,
    resolver
);
AlemTuzlak commented 1 year ago

Hello! Thank you so much for the support, I think the issue you are facing is with the fact how Remix handles file uploads, check this part of the documentation: https://remix.run/docs/en/1.17.1/utils/parse-multipart-form-data The package tries to get all the data from the passed formData but it natively doesn't handle file uploads and I am waiting for the API to become stable to actually add it to the package, what you can do is try getting the data by hand and then passing it into validateFormData, I will in any case investigate this and add a readme update to help clear this up

AlemTuzlak commented 1 year ago
  // You get the parsed file here
  const formData = await unstable_parseMultipartFormData(request);
  // Then you validate
  const { errors, data } =
    await validateFormData<FormData>(formData, resolver);
  if (errors) {
    return json(errors);
  }
  // Do something with the data
};

Do let me know if this works. Currently sick but I am planning to look into this further when I am feeling a bit better

eovandodev commented 1 year ago

@AlemTuzlak Thanks for the quick responses. Unfortunately, I wasn't able to solve the problem with just that. I followed the suggested path, but I ended up removing the react-hook-form package to keep things moving forward due to time constraints. I appreciate your help, and I hope this issue can be resolved in the future.

eovandodev commented 1 year ago
  // You get the parsed file here
  const formData = await unstable_parseMultipartFormData(request);
  // Then you validate
  const { errors, data } =
    await validateFormData<FormData>(formData, resolver);
  if (errors) {
    return json(errors);
  }
  // Do something with the data
};

Do let me know if this works. Currently sick but I am planning to look into this further when I am feeling a bit better

The code you provided is still missing the uploadHandler. Remix provides two utilities to handle this: unstable_createFileUploadHandler and unstable_createMemoryUploadHandler. It is our responsibility to decide which one fits the use case better. However, we can always build our custom uploadHandler, which is what I ended up doing in my case since I'm using Supabase buckets for storage.

Here's an example of a custom uploadHandler implementation:

const uploadHandler = async (part: UploadHandlerPart): Promise<string> =>
  new Promise(async (resolve, reject) => {
    try {
      // Handle the file here
    } catch (error) {
      reject(error);
    }
  });

const formData = await unstable_parseMultipartFormData(request, uploadHandler);
AlemTuzlak commented 1 year ago

@eovandodev yes that is right, you need that as well, I have a project where I upload to supabase buckets as well and what I do is use the uploadHandler to upload it and return a url as the form value and then validate it as a string on the BE but what you're doing is right

AlemTuzlak commented 1 year ago

will be closing this issue for now, file upload handling will be tacked as soon as the API in remix is stabilized

eovandodev commented 1 year ago

will be closing this issue for now, file upload handling will be tacked as soon as the API in remix is stabilized

@AlemTuzlak Thanks for the assistance!

oltodo commented 1 year ago

Hi Thanks for your amazing work on this very useful library!

I've been trying to upload files too using the solution described above. I met an issue because I'm using the onValid handler which is supposed to submit the form at the end:

const submit = useSubmit();

const form = useRemixForm<Output<typeof schema>>({
  submitHandlers: {
    onValid: (data) => {
      // ...

      const formData = createFormData(data);

      submit(formData, { method: "post", encType: "multipart/form-data" });
    },
  },
}

The problem I met is that the createFormData function returns a FormData object that contains a formData, that contains a JSON-stringified version of the data. Then unstable_parseMultipartFormData is not able to handle the data sent properly.

To fix it I had to turn the handler into this:

onValid: (data) => {
  // ...
  const formData = new FormData();
  for (const key in data) {
    formData.append(key, data[key]);
  }

  submit(formData, { method: "post", encType: "multipart/form-data" });
},

Does it make sense to you?

AlemTuzlak commented 1 year ago

@oltodo @eovandodev the package now supports file uploads

oltodo commented 1 year ago

Thanks @AlemTuzlak!

Do I still need to use unstable_parseMultipartFormData to get the files?

So far, getValidatedFormData returns an object where the file is a stringified version of the File object:

{
  firstName: 'Bob',
  lastName: 'Smith',
  email: 'bob.smith@example.com',
  bio: 'Nulla reprehenderit pariatur magna eu aliqua aliquip dolore mollit ullamco culpa exercitation aliquip exercitation id.',
  picture: '[object File]'
} 

Maybe could provide an example?

oltodo commented 1 year ago

The following code worked for me:

const formData = await unstable_parseMultipartFormData(
  request.clone(),
  unstable_createFileUploadHandler({
    directory: "/Users/nicolas/Code/AdventPrayers/public/assets/uploads",
  }),
);

const {
  errors,
  data,
  receivedValues: defaultValues,
} = await getValidatedFormData<Output<typeof schema>>(
  request,
  resolver(schema),
);

if (errors) {
  return json({ errors, defaultValues });
}

const picture = formData.get("picture") as NodeOnDiskFile | null;

const user = await createUser({
  ...data,
  picture: picture?.name || null,
});
AlemTuzlak commented 1 year ago

@oltodo yes you have to use unstable_... Because of way how Remix works with files, but yes this is one of the ways you can do it, I'd recommend directly uploading it to a bucket if you're using one then returning the url from the handler