Date range field
A first and a last day, picked from one calendar.
Installation
npx shadcn@latest add @formdef/date-range-fieldRegister it under widgets in lib/formdef/index.tsx, as widgets: { dateRange: DateRangeField }.
Usage
The value is { from?: string; to?: string }, each an ISO date such as 2027-03-01. The first
click
selects a range of one day, from and to alike; the second click moves one end.
export const = createForm({
: .({
:
.({ : ..().(), : ..().() })
.(({ , }) => !== && !== && < , {
: 'Pick a departure after your arrival',
}),
}),
: { : {} },
fields: {
stay: { : 'dateRange', : { : 'Stay', : '2027-01-01' } },
},
})The range is one field. A refine on it, here demanding a departure after the arrival, reports on
the field itself, so the widget shows its message. Leave out path: a message on stay.to would
belong to no widget.
| Prop | Meaning |
|---|---|
format | Turns each ISO date into the text on the button. |
separator | Stands between the two dates on the button. Defaults to –. |
numberOfMonths | How many months the calendar shows side by side. Defaults to 2. |
min, max | The first and last day that can be picked, as ISO dates. |
The calendar props and the time-zone behaviour are those of the date field.
Props
Prop
Type
Source
import { type FieldOf, fieldParts } from '@formdef/core'
import { type CalendarAttrs, CalendarPopover } from '@/components/formdef/calendar-popover'
import { FieldFrame } from '@/components/formdef/field-frame'
import { Calendar } from '@/components/ui/calendar'
import { fromIsoDate, outsideBounds, toIsoDate } from '@/lib/formdef/date'
import { type FieldLabels, labelIdFor } from '@/lib/formdef/field-labels'
type DateRange = {
from?: string | undefined
to?: string | undefined
}
type DateRangeFieldProps = FieldOf<DateRange> &
FieldLabels &
CalendarAttrs & {
placeholder?: string | undefined
format?: ((iso: string) => string) | undefined
separator?: string | undefined
numberOfMonths?: number | undefined
className?: string | undefined
}
const PICK_A_RANGE = 'Pick a date range'
const EN_DASH = ' – '
const TWO_MONTHS = 2
const asIs = (iso: string): string => iso
const isoOf = (date: Date | undefined): string | undefined =>
date === undefined ? undefined : toIsoDate(date)
export const DateRangeField = ({
field,
label,
labelAside,
description,
placeholder = PICK_A_RANGE,
format = asIs,
separator = EN_DASH,
numberOfMonths = TWO_MONTHS,
disabled,
required,
min,
max,
className,
...calendar
}: DateRangeFieldProps) => {
const parts = fieldParts(field, { description })
const { from, to } = field.state.value ?? {}
const isEmpty = from === undefined && to === undefined
const selected = isEmpty
? undefined
: {
from: from === undefined ? undefined : fromIsoDate(from),
to: to === undefined ? undefined : fromIsoDate(to),
}
const text = isEmpty
? undefined
: [from, to].flatMap((end) => (end === undefined ? [] : [format(end)])).join(separator)
return (
<FieldFrame
parts={parts}
label={label}
labelAside={labelAside}
description={description}
disabled={disabled}
>
<CalendarPopover
control={parts.control}
labelId={label === undefined ? undefined : labelIdFor(parts.control.id)}
text={text}
placeholder={placeholder}
disabled={disabled}
required={required}
className={className}
>
<Calendar
{...calendar}
mode="range"
numberOfMonths={numberOfMonths}
selected={selected}
{...(selected?.from === undefined ? {} : { defaultMonth: selected.from })}
disabled={outsideBounds(min, max, field.name)}
onSelect={(range) =>
field.handleChange({ from: isoOf(range?.from), to: isoOf(range?.to) })
}
/>
</CalendarPopover>
</FieldFrame>
)
}