nuxt / ui

A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.
https://ui.nuxt.com
MIT License
4k stars 504 forks source link

UInput with type='file', how to validate with Zod? #2462

Open pieterjanmaes opened 3 days ago

pieterjanmaes commented 3 days ago

For what version of Nuxt UI are you asking this question?

v2.x

Description

How can i use Zod to validate an UInput file field?

Let's say you have the following code: (I found the example over here.)

<UForm :schema="schema" :state="state" @submit="onSubmit">

  <UFormGroup name="picture" label="Picture">
    <UInput v-model="state.picture" type="file" @change="onChangeFile"/>
  </UFormGroup>

  <UButton type="submit">
    Submit
  </UButton>

</UForm>
<script setup lang="ts">
  import { z } from 'zod';

  const state = reactive({
    picture: undefined,
  })

  const schema = z.object({
    picture: z.custom<FileList>()
      .transform((val) => {
        if (val instanceof File) return val;
        if (val instanceof FileList) return val[0];
        return null;
      })
      .superRefine((file, ctx) => {
        if (!(file instanceof File)) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            fatal: true,
            message: 'Not a file',
          });
          return z.NEVER;
        }
        if (file.size > 5 * 1024 * 1024) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: 'Max file size allowed is 5MB',
          });
        }
        if (
          !['image/jpeg', 'image/png', 'image/webp', 'image/jpg'].includes(
            file.type
          )
        ) {
          ctx.addIssue({
            code: z.ZodIssueCode.custom,
            message: 'File must be an image (jpeg, jpg, png, webp)',
          });
        }
      })
  });

  type Schema = z.infer<typeof schema>;

  async function onSubmit (event: FormSubmitEvent<Schema>) {
    console.log(event.data);
  }
</script>

As aspected the following onChange function logs a FileList

function onChangeFile(event: Event) {
  console.log(event)
}

But when I log val in the transform function of Zod

.transform((val) => {
  console.log(val);
  if (val instanceof File) return val;
  if (val instanceof FileList) return val[0];
  return null;
})

I get a string like this:

C:\fakepath\Screenshot 2024-10-25 at 10.26.36.png

Can someone please help me out, i'm looking for a few days now for a solution.

noook commented 3 days ago

Try to use Zod's z.instanceof. z.custom seems to be okay for object literals or string template/literals.

I think in your situation you'd need to do something like this:

z.object({
    picture: z.union([z.instanceof(File), z.instanceof(FileList)])
      .transform((val) => {
        if (val instanceof File) return val;
        if (val instanceof FileList) return val[0];
        return null;
      })
})
pieterjanmaes commented 3 days ago

Hi @noook, thanks for your reply.

When I try it, my app won't build and i get an 500 error:

FileList is not defined

When I use File alone and I select an image, i get:

Input not instance of File

z.object({
    picture: z.instanceof(File)
      .transform((val) => {
        if (val instanceof File) return val;
        if (val instanceof FileList) return val[0];
        return null;
      })
})
pieterjanmaes commented 3 days ago

And when i make the component 'clientOnly' i get Invalid input as error

noook commented 2 days ago

Can you try to make a reproducible example so that I can play with it ?

pieterjanmaes commented 2 days ago

@noook, sure, here you go!

https://stackblitz.com/edit/nuxt-starter-mkx9qp?file=app.vue

noook commented 2 days ago

I tried with this example:

https://stackblitz.com/edit/nuxt-starter-adyxmi?file=components%2FMyForm.client.vue

Basically what I did:

  1. Move logic into .client.vue file. This allows mentioning the FileList class. I think you can bypass this by doing import.meta.client ? z.instanceof(FileList) : z.any(). This shouldn't try to reach for an undefined class server-side
  2. Remove instanceof tests, your type should always be a FileList
  3. Remove binding on the input, it is not bound in the component if the type is "file". Manually assign with the @change event

However, I can't figure out why the transform is not working, it still outputs me a FileList on the Zod's parse output.