PetalComponents.DatePicker (petal_components v4.15.3)

Copy Markdown View Source

A text input with a <.calendar> in a panel underneath it. The input shows a formatted date you can type into; the form always posts ISO 8601.

Examples

<.date_picker name="due_on" label="Due date" value={~D[2026-03-14]} format="%d %b %Y" />

Wired to a form field, it takes its name, value, errors and label the same way <.field> does:

<.form for={@form} phx-change="validate" phx-submit="save">
  <.date_picker field={@form[:due_on]} label="Due date" min={Date.utc_today()} />
</.form>

Range mode posts from and to sub-fields, so params["stay"] comes back as %{"from" => "2026-03-10", "to" => "2026-03-17"}:

<.date_picker
  name="stay"
  mode="range"
  two_months
  min={Date.utc_today()}
  value={{~D[2026-03-10], ~D[2026-03-17]}}
/>

What gets posted

The visible input is display-only: it is not named, so it never posts. A hidden input carries the value in ISO 8601 (YYYY-MM-DD) whatever format you show, because a strftime string is for humans and a form value is for your changeset.

Parse on blur

You can type in the input. On blur (or Enter) the picker tries, in order:

  1. Date.from_iso8601/1, so 2026-03-14 always works
  2. a lenient parse of the configured format, so 14 Mar 2026 works when format is "%d %b %Y"

On success the hidden value and the calendar's month both move to the parsed date. On failure the input reverts to the last valid formatted value, so a half-typed date can never silently post as nothing. Enter commits the same way blur does, and when it lands a COMPLETE selection - both ends in range mode - it also closes the panel; a partial or failed parse keeps it open. Clicking a day keeps the same contract: the click that completes the selection (any single-mode click, a range's second end) closes the panel and hands focus back to the input; a range's anchor click keeps browsing. The calendar icon is a true toggle - it opens a closed panel and closes an open one.

While the panel is open, typing does not even wait for blur: the moment the text parses to a complete selection, the calendar pages to it and paints it, with the caret and your text left exactly where they are (14 Mar 2027 canonicalises to the configured format on blur or Enter, not mid-keystroke). Incomplete text does nothing - previews never revert. Server-owned pickers (on_select) keep the blur/Enter contract only, so your handler is never fed per-keystroke half-states.

One wiring requirement that bites: with on_month_change left nil, month navigation renders as patch links carrying month_param - which only pages if your LiveView actually handles that param in handle_params/3 and feeds it back through month. In a LiveView that ignores the param, prev/next do nothing and a typed far-away date cannot bring its month into view. Either handle the param, or pass on_month_change and assign the pushed month - the typed-date month jump works through whichever wiring you chose.

With on_select set the server owns the value, so a parsed date is pushed to you as that same event rather than written client-side - typing 12 Jun 1987 and clicking 12 June 1987 arrive at handle_event/3 identically. In range mode a typed range arrives as two events in reading order, exactly as two clicks would, so the handler you already have keeps working. Emptying the input pushes on_clear when you have wired one; without one there is nothing the picker can push, so the display reverts rather than sit there disagreeing with the value that will actually post.

LiveView and dead views

With on_select set, clicking a day pushes that event with the ISO date and your handle_event/3 owns the value. Without it, the PetalDatePicker hook writes the hidden input itself and dispatches input/change, which is enough for a plain form post or a phx-change form. The panel opens and closes with Phoenix.LiveView.JS either way: Escape closes it and returns focus to the input, and clicking outside dismisses it.

Summary

Functions

Renders a date input with a calendar panel. See the module docs above for what gets posted, the parse-on-blur contract, and the LiveView vs dead-view wirings.

Functions

date_picker(assigns)

Renders a date input with a calendar panel. See the module docs above for what gets posted, the parse-on-blur contract, and the LiveView vs dead-view wirings.

<.date_picker field={@form[:due_on]} label="Due date" format="%d %b %Y" />

Attributes

  • id (:string) - the picker id; autogenerated if not set.
  • field (Phoenix.HTML.FormField) - a form field; wires name, value, errors and label like <.field>. Defaults to nil.
  • name (:any) - the posted field name; derived from field when given. Defaults to nil.
  • value (:any) - a Date, an ISO string, or a {from, to} pair in range mode. Defaults to nil.
  • label (:string) - the field label; omitted when nil. Defaults to nil.
  • errors (:list) - error messages rendered under the input. Defaults to [].
  • help_text (:string) - context shown under the input. Defaults to nil.
  • mode (:string) - selection behaviour. Defaults to "single". Must be one of "single", or "range".
  • format (:string) - strftime format for the display input (Calendar.strftime/2); the posted value is always ISO 8601. Defaults to "%Y-%m-%d".
  • range_separator (:string) - what sits between the two dates in the display input in range mode. Defaults to " - ".
  • placeholder (:string) - placeholder for the display input. Defaults to nil.
  • clearable (:boolean) - show a clear button whenever the field holds text. Defaults to false.
  • two_months (:boolean) - render two month panes side by side. A range reads naturally across the pair, and single mode earns it too - the flight-search pattern, where seeing next month saves a page before the date you are really after. Defaults to false.
  • disabled (:boolean) - disable the input and the panel. Defaults to false.
  • required (:boolean) - mark the field required. Defaults to false.
  • month (:any) - the displayed month as any Date within it. Defaults to nil.
  • min (:any) - earliest selectable Date (inclusive). Defaults to nil.
  • max (:any) - latest selectable Date (inclusive). Defaults to nil.
  • disabled_dates (:any) - a list of Dates, or a 1-arity function Date -> boolean. Defaults to nil.
  • today (:any) - the date treated as today; defaults to Date.utc_today/0. Defaults to nil.
  • starts_on (:integer) - first day of the week, ISO day numbers (1 = Monday, 7 = Sunday). Defaults to 1. Must be one of 1, 2, 3, 4, 5, 6, or 7.
  • show_outside_days (:boolean) - render adjacent-month days in the grid. Defaults to true.
  • fixed_weeks (:boolean) - always render 6 week rows. Defaults to false.
  • day_names (:list) - 7 short day labels ordered Monday-first. Defaults to nil.
  • day_names_long (:list) - 7 full day labels ordered Monday-first. Defaults to nil.
  • month_names (:list) - 12 month labels. Defaults to nil.
  • on_select (:any) - event pushed when a day is clicked, with the ISO date. Defaults to nil.
  • on_month_change (:any) - event pushed by the prev/next month buttons. Defaults to nil.
  • on_clear (:any) - event pushed when the clear button is clicked. Defaults to nil.
  • target (:any) - phx-target for the picker's events. Defaults to nil.
  • open_label (:string) - accessible label for the toggle button. Defaults to "Choose date".
  • clear_label (:string) - accessible label for the clear button. Defaults to "Clear date".
  • class (:any) - extra classes for the wrapper. Defaults to nil.
  • panel_class (:any) - extra classes for the panel. Defaults to nil.
  • Global attributes are accepted.