Closed alexwhb closed 2 months ago
@alexwhb Hey Alex, thank you for the kind words! Before I answer this, what happens with the images after you upload them?
@AlemTuzlak Absolutely. This library is a huge help, and you also introduced me to Zod, which I've subsequently used on another project too.
I'll answer that in two ways, because I'm not 100% sure what way you mean.
1.) I'm currently uploading the file just to my local file system, but eventually I'll likely use S3. I've tested using S3 with S3rver
locally using the image buffer and that also works when I'm not using Remix-Hook-Form.
2.) So when I attempt to upload an image this way either I get just a string with the image name in my request data in the action, or I get nothing in the action depending on how I configure the form. If I attempt to use the standard Remix multipart file upload method I get an error: TypeError: Could not parse content as FormData.
here's what my action code looks like in that case:
export const action: ActionFunction = async ({request}) => {
const directory = "/public/images"
const uploadHandler = unstable_composeUploadHandlers(
unstable_createFileUploadHandler({
directory,
maxPartSize: 5_000_000,
file: ({filename}) => {
const newFileName = `${uuidv4()}.${filename.split('.').pop()}`
prisma.image.create({
data: {
url: `/images/${newFileName}`
}
}).then(res => console.log(res))
return newFileName
},
}),
// parse everything else into memory
);
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
console.log(formData) // this never gets hit.
return {};
Let me know if you want any additional info. Happy to to provide it. And thank you for taking a look. Much appreciated.
Hi, I'm also having trouble with file upload mixed with other fields.
The setup you use in the documentation for "File upload" doesn't seem to work. I've encountered your problem @alexwhb, where the action says it could not parse as FormData. I resolved it but I don't remember how exactly (I'll try to remember).
My problem resides in the parsing of values not working correctly when mixing files and other content. The validateFormData
function returns errors because my resolver expects an array and it encounters a string, as you can see here:
Entering action function:
Files: [{}]
{
files: {
message: 'Expected array, received string',
type: 'invalid_type',
ref: undefined
},
images: {
message: 'Expected array, received string',
type: 'invalid_type',
ref: undefined
},
published: {
message: 'Expected boolean, received string',
type: 'invalid_type',
ref: undefined
}
}
My action (down below) function is almost exactly like the example, however, I've tried countless variations. I've tried with stringifyAllValues true and false. I've even tried using a custom generateFormData
.
const formData = await unstable_parseMultipartFormData(
request,
unstable_createMemoryUploadHandler(),
)
// The file will be there
console.log("Files: ", formData.get("files"))
// validate the form data
const { errors, data: product } = await validateFormData<Product>(
formData,
resolver,
)
if (errors) {
console.log(errors)
return json({ errors, product })
}
@alexwhb Sorry for the slow reply! So I wrote an article a while back where I upload images directly to supabase (which I think uploads it to an S3 bucket) so you could do it like this:
https://alemtuzlak.hashnode.dev/uploading-images-to-supabase-with-remix
What I usually would recommend with file uploads is to have validation where its:
z.instanceOf(File).or(z.string())
and when you upload it you parse it and return the url on the server so you can actually save the url to wherever and pass the validation.
When it comes to your problem @aknegtel did you try using the text decoder (you can refer to the same link I provided above) for parsing the rest of the values?
unstable_parseMultipartFormData
relies on you parsing everything and giving it back to formData, so you need to parse the non file values as well and return them as text.
@AlemTuzlak Awesome! Thanks for that resource. I'll give it a try.
@alexwhb Sorry for the slow reply! So I wrote an article a while back where I upload images directly to supabase (which I think uploads it to an S3 bucket) so you could do it like this: https://alemtuzlak.hashnode.dev/uploading-images-to-supabase-with-remix What I usually would recommend with file uploads is to have validation where its:
z.instanceOf(File).or(z.string())
and when you upload it you parse it and return the url on the server so you can actually save the url to wherever and pass the validation.When it comes to your problem @aknegtel did you try using the text decoder (you can refer to the same link I provided above) for parsing the rest of the values?
unstable_parseMultipartFormData
relies on you parsing everything and giving it back to formData, so you need to parse the non file values as well and return them as text.
I wrote mine like this which works quite well 😄
image: zod.instanceof(File).refine((file) => {
return file && file.size <= MAX_FILE_SIZE
}, `Max image size is 5MB.`)
.refine(
(file) => file && ACCEPTED_IMAGE_TYPES.includes(file.type),
"Only .jpg, .jpeg, .png formats are supported."
).or(zod.string().url()).optional(),
Hi, I'm also having trouble with file upload mixed with other fields.
The setup you use in the documentation for "File upload" doesn't seem to work. I've encountered your problem @alexwhb, where the action says it could not parse as FormData. I resolved it but I don't remember how exactly (I'll try to remember).
My problem resides in the parsing of values not working correctly when mixing files and other content. The
validateFormData
function returns errors because my resolver expects an array and it encounters a string, as you can see here:Entering action function: Files: [{}] { files: { message: 'Expected array, received string', type: 'invalid_type', ref: undefined }, images: { message: 'Expected array, received string', type: 'invalid_type', ref: undefined }, published: { message: 'Expected boolean, received string', type: 'invalid_type', ref: undefined } }
My action (down below) function is almost exactly like the example, however, I've tried countless variations. I've tried with stringifyAllValues true and false. I've even tried using a custom
generateFormData
.const formData = await unstable_parseMultipartFormData( request, unstable_createMemoryUploadHandler(), ) // The file will be there console.log("Files: ", formData.get("files")) // validate the form data const { errors, data: product } = await validateFormData<Product>( formData, resolver, ) if (errors) { console.log(errors) return json({ errors, product }) }
Hi!
I had similar issued until I used parseFormData
before calling validateFormData
. That seems to do the trick with getting everything parsed correctly 😄
I also was able to get it working by following the various useful tips in the comments, this is the code in case anybody needs it for reference
async function decodeTextFields({ filename, data }) {
if (!filename) {
const chunks = [];
for await (const chunk of data) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
const textDecoder = new TextDecoder();
return textDecoder.decode(buffer);
}
// if it has a filename it's the file we want to upload, by returning undefined we delegate the processing of the file to
// the next uploadHandler, in this example the s3UploadHandler
return undefined;
};
export async function action({ request }: ActionFunctionArgs) {
const uploadHandler: UploadHandler = composeUploadHandlers(
decodeTextFields,
s3UploadHandler,
);
const formData = await parseMultipartFormData(request, uploadHandler);
const avatarUrl = formData.get("avatar");
const {
errors,
data,
receivedValues: defaultValues,
} = await getValidatedFormData<CreateEventSchema>(
formData as unknown as Request,
resolver,
);
if (errors) {
return json({ errors, defaultValues }, { status: 422 });
}
// do whatever you need to do with the data here, send it to the db I guess :)
}
@AlemTuzlak I have a couple of proposals:
getValidatedFormData
directly instead of parseFormData
and validateFormData
as @cbude suggested since getValidatedFormData
is already calling parseFormData
internally, but Typescript was screaming at me cause I'm passing FormData instead of Request, when it actually works with a FormData as well. Do you think it would be correct to change the type from Request
to Request | FormData
(same as parseFormData
)?while doing all of this, I still get "Could not parse content as FormData.".
I added some console.log inside remix node_modules/@remix-run/server-runtime/dist/formData.js
and I see that content type is application/x-www-form-urlencoded
and not multipart/form-data
as required. @AlemTuzlak do you change it somewhere?
my Form encType is multipart/form-data
while doing all of this, I still get "Could not parse content as FormData.". I added some console.log inside remix
node_modules/@remix-run/server-runtime/dist/formData.js
and I see that content type isapplication/x-www-form-urlencoded
and notmultipart/form-data
as required. @AlemTuzlak do you change it somewhere?my Form encType is
multipart/form-data
adding
submitConfig: {
encType: "multipart/form-data",
},
to my useRemixForm
fixed that issue, although I would expect the encType from the Form element to be used.
That's s good point, I forgot to mention it in my example. @AlemTuzlak sorry for pinging you again, but it would be nice to update the docs, I can open a PR but first I want to be sure you'll be up to review and eventually merge it
That's s good point, I forgot to mention it in my example. @AlemTuzlak sorry for pinging you again, but it would be nice to update the docs, I can open a PR but first I want to be sure you'll be up to review and eventually merge it
I think this is something to fix in the library and not something to document
I don't think so cause the library is using useSubmit under the hood and this is how useSubmit works
do you mean useSubmit
changes the encType? so why have the encType prop on the form component anyway?
You don't need it on the form, when using useSubmit you are managing the form submission through the submit api and not through the Form component. You can check how it works on Remix documentation
You don't need it on the form, when using useSubmit you are managing the form submission through the submit api and not through the Form component. You can check how it works on Remix documentation
remix docs says to put it on the form. I don't use useSubmit
directly
About to release a new release with the following improvements:
I'm closing this as completed with v5.0.0
Hey I love this library! It works super well. I do, however have one conundrum I've been struggling with for a while. I have a form that has a file upload input as well as a few other text inputs, because I want to submit all of this data to the server at once. I can't seem to figure out a way to do it. Any advice? I've tried everything from sending the file as submit data in useRemixForm to messing with stringifyAllValues, and many other things. No luck.
I'm using react dropzone for my file upload component. I have gotten the file upload to work without the use of remix-hook-form, but I really want to be able to validate my other form elements.
here's what my form looks like currently, though I've played with a million variations:
note I'm registering anything using setValue in a useEffect.
on the action side I've attempted everything from removing my upload value from zod all together to using remix's Multipart form parser and many other variations.
Should I maybe use multiple actions, and somehow split up my file upload from the other form data processing? And if so... how can I do that in the same component?