ui icon indicating copy to clipboard operation
ui copied to clipboard

problem with form onSubmit

Open cblberlin opened this issue 1 year ago • 0 comments

hi everyone, i'm writing a from which is multi-entries form, but after submit, it return an object not an array

the console.log show me the submit value as an object:

{
  {
    "school": "University A",
    "major": "Computer Science",
    "degree": "Bachelor's",
    "startdate": "2020-09-01",
    "enddate": "2024-06-01",
    "isCurrent": false
  },
  {
    "school": "University B",
    "major": "Graphic Design",
    "degree": "Master's",
    "startdate": "2025-09-01",
    "enddate": "2027-06-01",
    "isCurrent": false
  }
}

but it expect the output as an array otherwise it won't pass the validation:

[
  {
    "school": "University A",
    "major": "Computer Science",
    "degree": "Bachelor's",
    "startdate": "2020-09-01",
    "enddate": "2024-06-01",
    "isCurrent": false
  },
  {
    "school": "University B",
    "major": "Graphic Design",
    "degree": "Master's",
    "startdate": "2025-09-01",
    "enddate": "2027-06-01",
    "isCurrent": false
  }
]

here is my schema:

import * as z from "zod"

export const educationEntrySchema = z.object({
  school: z.string().min(2, {
    message: "please enter a valid school name.",
  }),
  major: z.string().min(2, {
    message: "please enter a valid major.",
  }),
  degree: z.string().min(2, {
    message: "please enter a valid degree.",
  }),
  startdate: z.coerce.date({
    errorMap: (issue, {defaultError}) => ({
      message: issue.code === "invalid_date" ? "please select a valid start day" : defaultError,
    }),
  }),
  isCurrent: z.boolean().optional(),
  enddate: z.coerce.date({
    errorMap: (issue, {defaultError}) => ({
      message: issue.code === "invalid_date" ? "please select a valid end day" : defaultError,
    }),
  }).optional(),
});

export type educationEntry = z.infer<typeof educationEntrySchema>;

export const educationschema = z.array(educationEntrySchema);

and here is the form:

"use client"

import React, { useState } from "react"

import { cn } from "@/lib/utils"
import { format } from "date-fns"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Input } from "@/components/ui/input"
import { useToast } from "@/components/ui/use-toast"

import { Calendar } from "@/components/ui/calendar"
import { Checkbox } from "@/components/ui/checkbox"

import { CalendarIcon } from "lucide-react"
import { educationschema, educationEntry } from "./validators/education-schema"

export function Education() {
  const { toast } = useToast();

  // set education
  const [educations, setEducations] = useState<educationEntry[]>(
    [
      {
        school: "",
        major: "",
        degree: "",
        startdate: new Date(),
        enddate: new Date(),
        isCurrent: false,
      },
    ]
  );

  const currentYear = new Date().getFullYear()

  // 1. Define your form.
  const form = useForm<z.infer<typeof educationschema>>({
    resolver: zodResolver(educationschema),
    defaultValues: [
      {
        school: "",
        major: "",
        degree: "",
        startdate: new Date(),
        enddate: new Date(),
        isCurrent: false,
      },
    ],
  })
 
  // 2. Define a submit handler.
  function onSubmit(values: z.infer<typeof educationschema>) {
    console.log("submited values: ", form.getValues());
    toast({
      title: "You submitted the following values:",
      description: (
        <p className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
          <code className="text-white">{JSON.stringify(values, null, 2)}</code>
        </p>
      ),
    })
    console.log(values)
  }

  // allow to add more education entries
  function addEducation() {
    setEducations([
      ...educations, 
      {
        school: "",
        major: "",
        degree: "",
        startdate: new Date(),
        enddate: new Date(),
        isCurrent: false,
      },
    ]);
  };

  // delete education entry, but keep at least one entry
  function deleteEducation(index: number) {
    if (educations.length > 1) {
      const newEducations = [...educations];
      newEducations.splice(index, 1);
      setEducations(newEducations);
    } else {
      alert("at least one entry");
    }
  };

  const handleCurrentChange = (index: number, isCurrent: boolean) => {
    const updatedEducation = [...educations];
    updatedEducation[index].isCurrent = isCurrent;
    setEducations(updatedEducation);
  };
  
  console.log("submited values: ", form.getValues());
  console.log("Form errors:", form.formState.errors);

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 my-4">
        {educations.map((edu, index) => (
          <div key={index}>
            <FormField
              control={form.control}
              name={`${index}.school`}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>school</FormLabel>
                  <FormControl>
                    <Input 
                      placeholder="Université xxx" 
                      {...field}
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name={`${index}.major`}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>major</FormLabel>
                  <FormControl>
                    <Input placeholder="ex: Design" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name={`${index}.degree`}
              render={({ field }) => (
                <FormItem>
                  <FormLabel>degree</FormLabel>
                  <FormControl>
                    <Input placeholder="ex: Master Design" {...field} />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />

            <FormField
              control={form.control}
              name={`${index}.startdate`}
              render={({ field }) => (
                <FormItem className="flex flex-col">
                  <FormLabel>start day</FormLabel>
                  <Popover>
                    <PopoverTrigger asChild>
                      <FormControl>
                        <Button
                          variant={"outline"}
                          className={cn(
                            "w-[240px] pl-3 text-left font-normal",
                            !field.value && "text-muted-foreground"
                          )}
                        >
                          {field.value ? (
                            format(field.value, "PPP")
                          ) : (
                            <span>please select start day</span>
                          )}
                          <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                        </Button>
                      </FormControl>
                    </PopoverTrigger>
                    <PopoverContent className="w-auto p-0" align="start">
                      <Calendar
                        mode="single"
                        selected={field.value}
                        onSelect={field.onChange}
                        disabled={(date) =>
                          date > new Date() || date < new Date("1900-01-01")
                        }
                        captionLayout="dropdown-buttons"
                        fromYear={1900}
                        toYear={currentYear}
                        initialFocus
                      />
                    </PopoverContent>
                  </Popover>
                  <FormMessage />
                </FormItem>
              )}
            />

            {/* a checkbox isCurrent: if not selected then show enddate, else show nothing */}
            <FormField
              control={form.control}
              name={`${index}.isCurrent`}
              render={({ field }) => (
                <FormItem className="flex flex-col">
                  <FormLabel>is current?</FormLabel>
                  <FormControl>
                    <Checkbox 
                      checked={field.value}
                      onCheckedChange={(isCurrent) => handleCurrentChange(index, Boolean(isCurrent))}
                    />
                  </FormControl>
                </FormItem>
              )}
            />

            {!edu.isCurrent && (
              <FormField
                control={form.control}
                name={`${index}.enddate`}
                render={({ field }) => (
                  <FormItem className="flex flex-col">
                    <FormLabel>end day</FormLabel>
                    <Popover>
                      <PopoverTrigger asChild>
                        <FormControl>
                          <Button
                            variant={"outline"}
                            className={cn(
                              "w-[240px] pl-3 text-left font-normal",
                              !field.value && "text-muted-foreground"
                            )}
                          >
                            {field.value ? (
                              format(field.value, "PPP")
                            ) : (
                              <span>please select end day</span>
                            )}
                            <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
                          </Button>
                        </FormControl>
                      </PopoverTrigger>
                      <PopoverContent className="w-auto p-0" align="start">
                        <Calendar
                          mode="single"
                          selected={field.value}
                          onSelect={field.onChange}
                          disabled={(date) =>
                            date > new Date() || date < new Date("1900-01-01")
                          }
                          captionLayout="dropdown-buttons"
                          fromYear={1900}
                          toYear={currentYear}
                          initialFocus
                        />
                      </PopoverContent>
                    </Popover>
                    <FormMessage />
                  </FormItem>
                )}
              />
            )}

            {index === educations.length - 1 && (
              <div className="flex-wrap-gap-2 mb-2">
                {index !== 0 && (
                  <Button
                    type="button"
                    variant="secondary"
                    onClick={() => deleteEducation(index)}
                  >
                    Delete
                  </Button>
                )}
                <Button type="button" onClick={addEducation}>
                  Add
                </Button>
              </div>
            )}
          </div>
        ))}
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

i don't know at which step the submit result turn into an object instead of an array

cblberlin avatar Jan 10 '24 18:01 cblberlin