edmundhung / conform

A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.
https://conform.guide
MIT License
1.8k stars 101 forks source link

Shadcn Calendar: Incorrect date value (-1) on select #679

Closed matt-d-webb closed 3 months ago

matt-d-webb commented 3 months ago

Describe the bug and the expected behavior

Using the Shadcn Calendarvia the Conform DatePicker , I am getting the incorrect date on select, this is consistently day -1 of the selected value.

Attached video.

Conform version

v1.1.4

Steps to Reproduce the Bug or Issue

Referencing the Conform Datepicker: https://github.com/edmundhung/conform/blob/main/examples/shadcn-ui/src/components/conform/DatePicker.tsx

My code:


"use client";

import { useFormState, useFormStatus } from "react-dom";
import { parseWithZod } from "@conform-to/zod";
import { useForm } from "@conform-to/react";

... 

export function WeightForm() {
  const [lastResult, action] = useFormState<any, FormData>(
    submitAction,
    undefined,
  );

  const [form, fields] = useForm({
    id: "weightForm",
    lastResult,
    onValidate({ formData }) {
      console.log("client validation: ", formData.get("date"));
      return parseWithZod(formData, { schema: formSchema });
    },
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
  });

  return (
    <Card>
      <CardHeader>
        <CardTitle>Add</CardTitle>
        <CardDescription>Update your current weight</CardDescription>
      </CardHeader>
      <CardContent>
        <form
          name="weight-form"
          id={form.id}
          onSubmit={form.onSubmit}
          action={action}
        >
          <div className="grid gap-4">
            <Field>
              <Label htmlFor={fields.weight.id}>Weight</Label>
              <InputConform
                id={fields.weight.id}
                meta={fields.weight}
                step="0.1"
                type="number"
              />
              {fields.weight.errors && (
                <FieldError>{fields.weight.errors}</FieldError>
              )}
            </Field>
            <Field>
              <Label>Date</Label>
              <DatePickerConform meta={fields.date} />
              {fields.date.errors && (
                <FieldError>{fields.date.errors}</FieldError>
              )}
            </Field>
            <SubmitButton>Submit</SubmitButton>
          </div>
        </form>
      </CardContent>
    </Card>
  );
}```

### What browsers are you seeing the problem on?

Chrome

### Screenshots or Videos

Uploading shadcn-datepicker-conform.mov…
matt-d-webb commented 3 months ago

Closing issue, this is due to the subtle difference between calling .toString() and .toISOString(), locale vs UTC, as such I have switched to the former in the ShadCn DatePicker.tsx conform implementation.

Screenshot 2024-06-15 at 9 34 21 AM

MDN ref note:

matt-d-webb commented 2 months ago

This is what I needed to do for fixing the timezone issue:

import { format, parseISO } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import {
  FieldMetadata,
  unstable_useControl as useControl,
} from "@conform-to/react";

export function DatePickerConform({ meta }: { meta: FieldMetadata<Date> }) {
  const triggerRef = React.useRef<HTMLButtonElement>(null);
  const control = useControl(meta);

  // Function to convert date to ISO string while preserving local time
  const dateToLocalISOString = (date: Date) => {
    const offset = date.getTimezoneOffset();
    const localDate = new Date(date.getTime() - offset * 60 * 1000);
    return localDate.toISOString().split("T")[0];
  };

  // Function to parse ISO string to local date
  const parseLocalISOString = (isoString: string) => {
    const date = parseISO(isoString);
    return new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
  };

  return (
    <div>
      <input
        className="sr-only"
        aria-hidden
        tabIndex={-1}
        ref={control.register}
        name={meta.name}
        defaultValue={
          meta.initialValue
            ? dateToLocalISOString(new Date(meta.initialValue))
            : ""
        }
        onFocus={() => {
          triggerRef.current?.focus();
        }}
      />
      <Popover>
        <PopoverTrigger asChild>
          <Button
            ref={triggerRef}
            variant={"outline"}
            className={cn(
              "w-64 justify-start text-left font-normal focus:ring-2 focus:ring-stone-950 focus:ring-offset-2",
              !control.value && "text-muted-foreground",
            )}
          >
            <CalendarIcon className="mr-2 h-4 w-4" />
            {control.value ? (
              format(parseLocalISOString(control.value), "PPP")
            ) : (
              <span>Pick a date</span>
            )}
          </Button>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0">
          <Calendar
            mode="single"
            selected={
              control.value ? parseLocalISOString(control.value) : undefined
            }
            onSelect={(value) =>
              control.change(value ? dateToLocalISOString(value) : "")
            }
          />
        </PopoverContent>
      </Popover>
    </div>
  );
}
swalker326 commented 1 month ago

Thank you for coming back with the solution you got, saved me loads of time.