nuxt / ui

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

How to ensure UForm and UFormGroup auto assigns errors for nested dynamic form. #2674

Closed adharshmk96 closed 1 week ago

adharshmk96 commented 1 week ago

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

v2.x

Description

I have a form like the following example:

<template>
  <div>
    <h1>Training Form</h1>
  </div>
  <UForm :state="state" :schema="schema" @submit="handleSubmit">
    <UFormGroup label="Name" name="name">
      <UInput v-model="state.name" />
    </UFormGroup>
    <UFormGroup label="Training Week" name="trainingWeek">
      <div v-for="(day, index) in state.trainingWeek" :key="index">
        <UFormGroup label="Day" name="trainingWeek[' + index + '].day">
          <UInput v-model="day.day" />
        </UFormGroup>
        <UFormGroup label="Exercises" name="trainingWeek[' + index + '].exercises">
          <div v-for="(exercise, exerciseIndex) in day.exercises" :key="exerciseIndex">
            <UInput v-model="exercise.exerciseName" />
          </div>
        </UFormGroup>
        <div class="mt-4 flex justify-end">
          <UButton @click="addExercise(index)">Add Exercise</UButton>
        </div>
      </div>
    </UFormGroup>
    <div class="mt-4 flex justify-end">
      <UButton @click="addDay">Add Day</UButton>
      <UButton type="submit">Submit</UButton>
    </div>
  </UForm>
</template>

<script setup lang="ts">
import { z } from "zod";
import type { FormSubmitEvent } from "#ui/types";
const state = reactive({
  name: "",
  trainingWeek: [
    {
      day: "",
      exercises: [
        {
          exerciseName: "",
        },
      ],
    },
  ],
});

const schema = z.object({
  name: z.string().min(1, "Required field"),
  trainingWeek: z.array(
    z.object({
      day: z.string().min(1, "Required field"),
      exercises: z.array(
        z.object({
          exerciseName: z.string().min(1, "Required field"),
        }),
      ),
    }),
  ),
});

type Schema = z.infer<typeof schema>;

const handleSubmit = (event: FormSubmitEvent<Schema>) => {
  console.log(JSON.stringify(event.data, null, 2));
};

const addExercise = (index: number) => {
  state.trainingWeek[index].exercises.push({ exerciseName: "" });
};

const addDay = () => {
  state.trainingWeek.push({ day: "", exercises: [{ exerciseName: "" }] });
};
</script>

I render the form based on state and dynamically add fields.

The form automatically sets up errors for name ( or any first level fields ) but doesn't set up errors for nested ones like day or exerciseName in the above example

What's the proper way to create dynamic forms and avail error setting ?

I'm able to get the following:

profile.vue:68 ZodError: [
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "Required field",
    "path": [
      "trainingWeek",
      0,
      "day"
    ]
  },
  {
    "code": "too_small",
    "minimum": 1,
    "type": "string",
    "inclusive": true,
    "exact": false,
    "message": "Required field",
    "path": [
      "trainingWeek",
      0,
      "exercises",
      0,
      "exerciseName"
    ]
  }
]

output by parsing schema

watchEffect(() => {
  try {
    schema.parse(state);
  } catch (error) {
    console.error(error);
  }
});
adharshmk96 commented 1 week ago

I should be using . instead of [ ]

eg:

:name="`trainingWeek.${index}.day`" 

instead of

:name="trainingWeek[' + index + '].day">
<template>
  <div>
    <h1>Training Form</h1>
  </div>
  <UForm :state="state" :schema="schema" @submit="handleSubmit">
    <UFormGroup label="Name" name="name">
      <UInput v-model="state.name" />
    </UFormGroup>
    <UFormGroup label="Training Week" name="trainingWeek">
      <div v-for="(day, index) in state.trainingWeek" :key="index">
        <UFormGroup label="Day" :name="`trainingWeek.${index}.day`">
          <UInput v-model="day.day" />
        </UFormGroup>
        <UFormGroup label="Exercises" :name="`trainingWeek.${index}.exercises`">
          <div v-for="(exercise, exerciseIndex) in day.exercises" :key="exerciseIndex">
            <UFormGroup :name="`trainingWeek.${index}.exercises.${exerciseIndex}.exerciseName`">
              <UInput v-model="exercise.exerciseName" />
            </UFormGroup>
          </div>
        </UFormGroup>
        <div class="mt-4 flex justify-end">
          <UButton @click="addExercise(index)">Add Exercise</UButton>
        </div>
      </div>
    </UFormGroup>
    <div class="mt-4 flex justify-end">
      <UButton @click="addDay">Add Day</UButton>
      <UButton type="submit">Submit</UButton>
    </div>
  </UForm>
</template>

<script setup lang="ts">
import { z } from "zod";
import type { FormSubmitEvent } from "#ui/types";
const state = reactive({
  name: "",
  trainingWeek: [
    {
      day: "",
      exercises: [
        {
          exerciseName: "",
        },
      ],
    },
  ],
});

const schema = z.object({
  name: z.string().min(1, "Required field"),
  trainingWeek: z.array(
    z.object({
      day: z.string().min(1, "Required field"),
      exercises: z.array(
        z.object({
          exerciseName: z.string().min(1, "Required field"),
        }),
      ),
    }),
  ),
});

type Schema = z.infer<typeof schema>;

watchEffect(() => {
  try {
    schema.parse(state);
  } catch (error) {
    console.error(error);
  }
});

const handleSubmit = (event: FormSubmitEvent<Schema>) => {
  console.log(JSON.stringify(event.data, null, 2));
};

const addExercise = (index: number) => {
  state.trainingWeek[index].exercises.push({ exerciseName: "" });
};

const addDay = () => {
  state.trainingWeek.push({ day: "", exercises: [{ exerciseName: "" }] });
};
</script>