nuxt / ui

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

Forms does not handle Zod array #1898

Open Barbapapazes opened 3 months ago

Barbapapazes commented 3 months ago

Environment


Version

"@nuxt/ui": "^2.17.0"

Reproduction

https://stackblitz.com/edit/github-gcnkca?file=app.vue

You can start the project, click on the file input, select a file, click on the submit button and check the console.

Description

If you upload a file larger than 200kb or that is not image/png and then submit the form, no error will be displayed. However, if you take a look at the console, you'll see an error related to the file input.

[
  {
    "path": "email",
    "message": "Required",
    "id": "nK7dDJpdOWE_57"
  },
  {
    "path": "password",
    "message": "Required",
    "id": "nK7dDJpdOWE_58"
  },
  {
    "path": "files.2",
    "message": "Max 200kb"
  }
]

Why is this happening?

The problem comes from this line: https://github.com/nuxt/ui/blob/dev/src/runtime/components/forms/Form.vue#L107

You are looking for this:

['files'].includes('files.0')

which is false because the array validate every index.

I expect this to be true in order to show an error message to the user.

Additional context

No response

Logs

No response

romhml commented 3 months ago

We rely on zod's path attribute to map each errors to a FormGroup using it's name attribute. When validating arrays, zod appends the index to the path of the element, in your case, files.0, which does not match the FormGroup's name.

This is needed in some cases, for instance using objects where you can map a FormGroup to a nested attributes, so we can't really remove it without introducing another bug.

A solution here is to tweak your schema to validate the entire array instead, here's an example: https://stackblitz.com/edit/github-gcnkca-cda6oa?file=app.vue

romhml commented 3 months ago

Or we might able to add a prop to the FormGroup with a pattern instead of a string to match errors, I'll explore this solution!

brentreilly commented 2 months ago

Thank you for that tip on Zod appending the index to the path, @romhml !

For anyone else looking to use UForm and Zod on an array of objects in your reactive state, see below. Note the :name="" attribute on the UFormGroup.

const menu = reactive({
  selections: [{ name: '', price: null }],
});
const menuSchema = z.object({
  selections: z
    .array(
      z.object({
        name: z.string().min(5, {
          message: 'Selection name must be at least 5 characters long.',
        }),
        price: z.number({
          message: 'Selection price is required.',
        }),
      })
    )
    .nonempty({
      message: 'Selections are required.',
    }),
});
<div
  v-for="(selection, index) in menu.selections"
  :key="index"
  class="flex items-center gap-2"
>
  <UFormGroup :name="`selections.${index}.name`" class="flex-1">
    <UInput
      v-model="selection.name"
      placeholder="Selection name"
      class="flex-1"
    />
  </UFormGroup>
</div>