Handle datetime-local for Future API #1135
|
Hi team! I've followed the discussion related to the same topic with conform v1: #738 I'm wondering what could be the best practice for use with the future API. ContextI would like to use a simple native input
Reproductionimport { coerceFormValue, formatResult } from "@conform-to/zod/v4/future";
import {
FieldName,
FormProvider,
isDirty,
parseSubmission,
report,
useControl,
useField,
useForm,
useFormData,
} from "@conform-to/react/future";
import { z } from "zod/v4";
import { data, Form } from "react-router";
const schema = coerceFormValue(
z.object({
createdAt: z.date(),
updatedAt: z.date(),
}),
);
export async function action({ request }) {
const formData = await request.formData();
const submission = parseSubmission(formData);
const result = schema.safeParse(submission.payload);
if (!result.success) {
return report(submission, {
error: formatResult(result),
});
}
console.log("update server side", result.data);
return data(null);
}
export async function loader() {
const infos = {
updatedAt: new Date("2026-01-01T12:00:00"),
createdAt: new Date("2026-01-01T06:00:00"),
};
return { infos };
}
// Custom field component approach
function DateTimeField({ name }: { name: FieldName<Date> }) {
const field = useField(name);
const control = useControl({
defaultValue: field.defaultValue,
});
return (
<>
<input type="text" ref={control.register} name={field.name} />
<input
type="datetime-local"
value={control.value?.substring(0, 16)}
onChange={(e) => {
control.change(e.target.valueAsDate?.toISOString() ?? "");
}}
/>
</>
);
}
export default function Component({ loaderData, actionData }) {
const { infos } = loaderData;
// Data preparation approach
const prepared = {
createdAt: infos.createdAt,
updatedAt: infos.updatedAt.toISOString().substring(0, 16),
};
const { form, fields } = useForm(schema, {
id: "form-zod",
defaultValue: prepared,
lastResult: actionData,
});
const dirty = useFormData(form.id, (formData) => {
return isDirty(formData, { defaultValue: prepared });
});
return (
<FormProvider context={form.context}>
<div>Dirty {dirty ? "yes" : "no"}</div>
<Form method="post" {...form.props}>
{/* Using custom component */}
<DateTimeField name={fields.createdAt.name} />
{/* Using prepared data */}
<input
type="datetime-local"
defaultValue={fields.updatedAt.defaultValue}
name={fields.updatedAt.name}
/>
<div>errors: {fields.updatedAt.errors}</div>
<div>
<button>Save</button>
</div>
</Form>
</FormProvider>
);
}QuestionI think I'm missing something but I can't find what. I'm wondering why I need to prepare a date for the input (string without the "Z" suffix) but I don't need to prepare a checkbox ("on" string) if I wanted to use a checkbox. Expected behavior: Being able to pass a Current behavior: I need to either:
Is this the intended way to work with date inputs, or is there a more idiomatic approach I should be using? Thanks for your help! |
Replies: 4 comments 8 replies
|
Thanks for bringing this up. Stripping the Z suffix does mean we lose explicit timezone information, but it probably makes sense as a default to treat everything as UTC. To support a different timezone, people would need a custom serialize function. The only thing I am still not fully sure about is how best to support cases where the datetime needs to be coerced based on another field value (for example, when the timezone is provided by the user in the same form), but overall I am on board with the suggestion. |
|
Hi, I've been working on handling The schema uses const baseSchema = z.object({
updatedAt: z.date(),
});
const clientSchema = coerceFormValue(baseSchema);
export function parseDate(unknown: Date | string | number) {
return typeof unknown === "string"
? new Date(unknown)
: typeof unknown === "number"
? new Date(unknown * 1000)
: unknown;
}
export function formatDateToDatetimeLocal(unknown: Date | number | string): string {
const date = parseDate(unknown);
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
export default function Component({ loaderData, actionData }: Route.ComponentProps) {
const { user } = loaderData;
const defaultValue = {
updatedAt: user.updatedAt,
};
const { form, fields } = useForm(clientSchema, {
id: "form-zod",
defaultValue,
lastResult: actionData,
});
const dirty = useFormData(form.id, (formData) => {
return isDirty(formData, {
defaultValue,
serialize(value, defaultSerialize) {
if (value instanceof Date) {
return formatDateToDatetimeLocal(value);
}
return defaultSerialize(value);
},
});
});
return (
<div>
<div>Dirty {dirty ? "Yes" : "No"}</div>
<Form method="post" {...form.props} className="flex flex-col gap-2">
<div>
updatedAt
<input
className="border"
type="datetime-local"
defaultValue={formatDateToDatetimeLocal(fields.updatedAt.defaultValue)}
name={fields.updatedAt.name}
/>
</div>
<Button>Save</Button>
</Form>
</div>
);
} |
|
Hi @edmundhung, Thank you for your work on dates. It's a bit awkward to say, but I'm troubled by the changes you made. I ran several tests and find myself in an even trickier situation than before: if you pass a UTC date without the const dateStr = "2026-03-14T12:00:00.000Z"
const dateForInputDateTimeLocal = "2026-03-14T12:00:00.000";
// visible to the user: the UTC value 2026-03-14T12:00
// it would never happen that the user actually wants to display
// the UTC value.
<input id="date" type="datetime-local" defaultValue={dateForInputDateTimeLocal} />
const $input = document.getElementById("date")
// value sent from the browser
// "2026-03-14T12:00"
const dateFromInput = $input.value
// value interpreted according to the server's timezone
"2026-03-14T11:00:00.000Z"I therefore think the discussion I had opened was perhaps not meant to drive a change, and that ultimately, using a UTC datetime string as-is in a new Date("2026-03-14T12:00:00.000")
// are definitely different from
new Date("2026-03-14T12:00:00.000Z")I look forward to reading your reply. Have a great day! |
Thanks for all the details!
I should have mentioned this changes in #1174 is only meant to make the default date serilaizing logic usable for people who are fine with UTC date time string, which I would expect to be quite common especially for back office applications. If you need to display and handle non UTC date time string, what we discussed hasn't really changed.
But I agree issue 2 and 4 shouldn't happen. I will work on a fix.