How to use shadcn/ui Range Date Picker inside Form?

1.8k Views Asked by At

I have a problem with using The Range Date Picker inside the Form component. To be exact, I need an object to store the {from, to} values of the range, but using an object as a Form field value makes error messages not work. I just get undefined as an error message.

The current fix I'm using, is that I just modified the Form Message Component to render the errors a bit differently, but I think it's more of a hack than the proper way.

The code:

const formSchema = z.object({
  title: z.string().min(2, {
    message: "Title must be at least 2 characters.",
  }),
  date: z.object({
    from: z.date(),
    to: z.date(),
  }),
});

function NewEventPage() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
      date: {},
    },
  });

  function onError(a) {
    console.log(a);
    /*
    title:
      message: "Title must be at least 2 characters."
      ref: {focus: ƒ, select: ƒ, setCustomValidity: ƒ, reportValidity: ƒ}
      type: "too_small"
    date:
      from: {message: 'Required', type: 'invalid_type', ref: undefined}
      to: {message: 'Required', type: 'invalid_type', ref: undefined}
    */
  }

  return (
    <Form {...form}>
      <form
        onSubmit={form.handleSubmit(onSubmit, onError)}
        className="mx-auto mt-12 grid max-w-screen-lg grid-cols-2 gap-x-12 gap-y-8"
      >
        <h1 className="col-span-2 scroll-m-20 text-center text-4xl font-extrabold tracking-tight lg:text-5xl">
          Create A New event
        </h1>

        <FormField
          control={form.control}
          name="title"
          render={({ field }) => {
            return (
              <FormItem>
                <FormLabel>Event Title</FormLabel>
                <FormControl>
                  <Input placeholder="The best title ever" {...field} />
                </FormControl>
                <FormDescription>Give a name for your event</FormDescription>
                <FormMessage />
              </FormItem>
            );
          }}
        />

        <FormField
      control={form.control}
      name="date"
      render={({ field }) => (
        <FormItem>
          <FormLabel>Date</FormLabel>
          <FormControl>
            <Popover>
              <PopoverTrigger asChild>
                <Button
                  variant={"outline"}
                  className={cn(
                    "w-full justify-start text-left font-normal",
                    !field.value && "text-muted-foreground",
                  )}
                >
                  <CalendarIcon className="mr-2 h-4 w-4" />
                  {field.value?.from ? (
                    field.value.to ? (
                      <>
                        {format(field.value.from, "LLL dd, y")} -{" "}
                        {format(field.value.to, "LLL dd, y")}
                      </>
                    ) : (
                      format(field.value.from, "LLL dd, y")
                    )
                  ) : (
                    <span>Pick a date</span>
                  )}
                </Button>
              </PopoverTrigger>
              <PopoverContent className="w-auto p-0" align="start">
                <Calendar
                  initialFocus
                  mode="range"
                  defaultMonth={field.value?.from}
                  selected={field.value}
                  onSelect={field.onChange}
                  numberOfMonths={2}
                />
              </PopoverContent>
            </Popover>
          </FormControl>
          <FormDescription>
            Select the date for when the event will take place
          </FormDescription>
          <FormMessage />
        </FormItem>
      )}
    />

        <Button type="submit" className="col-span-2">
          Submit
        </Button>
      </form>
    </Form>
  );
}

Title shows the error, but date shows undefined: Title shows the error, but date shows undefined

3

There are 3 best solutions below

0
On

You just have to give it a define Error message:

  const formSchema = z.object({
  title: z.string().min(2, {
    message: "Title must be at least 2 characters.",
  }),
  date: z.object({
    from: z.date(),
    to: z.date(),
  }).refine(
      (data) => data.from > addDays(new Date(), -1),
      "Start date must be in the future"
    ),
});

and remove date: {} from DefaultValues which is causing undefined.

Write like this:

function NewEventPage() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
    },
0
On

I managed to solve it the following way:

const DATE_REQUIRED_ERROR = "Date is required.";

const formSchema = z.object({
   date: z.object({
       from: z.date().optional(),
       to: z.date().optional(),
   }, {required_error: DATE_REQUIRED_ERROR}).refine((date) => {
       return !!date.from;
   }, DATE_REQUIRED_ERROR),
   comment: z.string().min(1, {message: "A comment is required."}),
});

This is definitely not pretty. But this is the only configuration where I did not run into the following issue:

  • When clicking on submit without having input a value in the range date picker, it showed undefined as an error, because it detected that date.from is undefined and therefore triggered the error for the entire date object. But the date object itself did not have an error, only the date.from inside of it. The error is apparent not propagated up to the date object. That is the reason why it showed the error undefined. That is why I put the date.from as optional, so it would never cause an error.
  • Then I added the refine((date) => {...}) to the date object itself. In there, it checks if date.from is undefined. If it is, the DATE_REQUIRED_ERROR text will be displayed in the UI. By doing it this way, you "lift" the error from date.from to the date object itself and it is displayed correctly.

So why then the {required_error: DATE_REQUIRED_ERROR} too?

This has the following reason:

  • When having inserted a value in the date range picker and then having removed it again, for some reason, it triggers the Required error on the date object. If the required_error is not set, then the UI will just show Required. I personally disliked the two different error messages depending if you never isnerted a value or if inserted it and removed it again. That's why I added this, so these two errors are the same. If you don't care about that, you can leave it out.

I personally added the default values in the useForm so they are all there. But for me it also worked when not putting them in.

const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
        date: {
            from: undefined,
            to: undefined,
        },
        comment: ''
    },
});

You also need to change selected field in the Calender tag. This is due to the date.from field being optional. The selected field needs it to be definitely set. That's why I assured that through the ! operator.

<Calendar
  initialFocus
  mode="range"
  defaultMonth={field.value.from}
  selected={{from: field.value.from!, to: field.value.to}}
  onSelect={field.onChange}
  numberOfMonths={1}
/>

This is my full code. Note, that the comment field is not implemented in the UI yet and has a dummy value in the defaults:

"use client"

import {Form, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {z} from "zod";
import {useForm} from "react-hook-form";
import {zodResolver} from "@hookform/resolvers/zod";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {Button} from "@/components/ui/button";
import {cn} from "@/lib/utils";
import {format} from "date-fns";
import {CalendarIcon} from "lucide-react";
import {Calendar} from "@/components/ui/calendar";
import * as React from "react";

const DATE_REQUIRED_ERROR = "Date is required.";

const formSchema = z.object({
    date: z.object({
        from: z.date().optional(),
        to: z.date().optional(),
    }, {required_error: DATE_REQUIRED_ERROR}).refine((date) => {
        return !!date.from;
    }, DATE_REQUIRED_ERROR),
    comment: z.string().min(1, {message: "A comment is required."}),
});

export default function CommentSection() {
const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
        date: {
            from: undefined,
            to: undefined,
        },
        // TODO: remove this dummy value
        comment: "asdf"
    },
});

function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
}

return (
    <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
            <FormField
                control={form.control}
                name='date'
                render={({field}) => (
                    <FormItem className="flex flex-col">
                        <FormLabel>Date</FormLabel>
                        <Popover>
                            <PopoverTrigger asChild>
                                <Button
                                    id="date"
                                    variant={"outline"}
                                    className={cn(
                                        "w-full justify-start text-left font-normal",
                                        !field.value.from && "text-muted-foreground"
                                    )}
                                >
                                    <CalendarIcon className="mr-2 h-4 w-4"/>
                                    {field.value.from ? (
                                        field.value.to ? (
                                            <>
                                                {format(field.value.from, "LLL dd, y")} -{" "}
                                                {format(field.value.to, "LLL dd, y")}
                                            </>
                                        ) : (
                                            format(field.value.from, "LLL dd, y")
                                        )
                                    ) : (
                                        <span>Pick a date</span>
                                    )}
                                </Button>
                            </PopoverTrigger>
                            <PopoverContent className="w-auto p-0" align="start">
                                <Calendar
                                    initialFocus
                                    mode="range"
                                    defaultMonth={field.value.from}
                                    selected={{from: field.value.from!, to: field.value.to}}
                                    onSelect={field.onChange}
                                    numberOfMonths={1}
                                />
                            </PopoverContent>
                        </Popover>
                        <FormDescription>
                            The date you want to add a comment for.
                        </FormDescription>
                        <FormMessage/>
                    </FormItem>
                )}
            />
            <Button type={"submit"}>Submit</Button>
        </form>
    </Form>
);
}
1
On

I managed to solve it the following way:

const formSchema = z.object({
  title: z.string().min(2, {
    message: "Title must be at least 2 characters.",
  }),
 date: z
      .object(
        {
          from: z.date(),
          to: z.date().optional(),
        },
        { required_error: "Date is required." },
      )
      .refine((date) => {
        return !!date.to
      }, "End Date is required."),
});

function NewEventPage() {
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      title: "",
    },
  });