Corex. DatePicker
(Corex v0.1.1)
View Source
Phoenix implementation of Zag.js Date Picker.
Anatomy
Basic Usage
<.date_picker>
<:label>Select a date</:label>
<:trigger>
<.heroicon name="hero-calendar" />
</:trigger>
<:prev_trigger>
<.heroicon name="hero-chevron-left" class="icon" />
</:prev_trigger>
<:next_trigger>
<.heroicon name="hero-chevron-right" class="icon" />
</:next_trigger>
</.date_picker>Form
When using with Phoenix forms, set the form id in to_form/2 (for example to_form(changeset, as: :name, id: "my-form")) and use <.form for={@form}>.
For cross-cutting invalid styling and error presentation, see the Forms guide. Pass invalid={Corex.FormField.invalid?(@form[:date])} when you want alert borders after validation.
Wire format vs domain types
The component and LiveView events use ISO-8601 strings (value, on_value_change, hidden inputs). Ecto schemas should use :date and {:array, :date}. After cast, work with %Date{} in application code.
| Layer | Format |
|---|---|
value, events, HTML | ISO strings (comma-joined for multiple/range) |
field={@form[:date]} | Accepts %Date{} or string; displayed as ISO |
| Ecto | :date, {:array, :date} |
| Flash / logs | format_value/2, not inspect/1 |
Use cast_params/2 in handle_event to normalize on_value_change or form params before cast/3:
def handle_event("date_changed", %{"value" => value}, socket) do
params = Corex.DatePicker.cast_params("single", %{"value" => value})
changeset =
MyApp.Form.DateForm.changeset_validate(%MyApp.Form.DateForm{}, params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, :form, to_form(changeset, action: :validate))}
endController
Build the form from an Ecto changeset:
def form_page(conn, _params) do
form =
%MyApp.Form.DateForm{}
|> MyApp.Form.DateForm.changeset(%{})
|> Phoenix.Component.to_form(as: :date_form, id: "date-form")
render(conn, :form_page, form: form)
end<.form :let={f} for={@form} action={@action} method="post">
<.date_picker
field={f[:date]}
class="date-picker"
translation={%Corex.DatePicker.Translation{
open_calendar: "Select date",
close_calendar: "Select date",
input: "Select date"
}}
>
<:label>Date</:label>
<:trigger>
<.heroicon name="hero-calendar" class="icon" />
</:trigger>
<:prev_trigger>
<.heroicon name="hero-chevron-left" class="icon" />
</:prev_trigger>
<:next_trigger>
<.heroicon name="hero-chevron-right" class="icon" />
</:next_trigger>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.date_picker>
<button type="submit">Submit</button>
</.form>Live View
Prefer building the form from an Ecto changeset (see "With Ecto changeset" below). Use phx-change on the form so params stay in sync after validation.
With Ecto changeset
Use field={@form[:birth_date]} inside <.form for={@form}>. The component resyncs from the server value on patch without controlled.
First create your schema and changeset:
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :birth_date, :date
timestamps(type: :utc_datetime)
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :birth_date])
|> validate_required([:name, :birth_date])
end
enddefmodule MyAppWeb.UserLive do
use MyAppWeb, :live_view
alias MyApp.Accounts.User
def mount(_params, _session, socket) do
{:ok, assign(socket, :form, to_form(User.changeset(%User{}, %{})))}
end
def handle_event("validate", %{"user" => user_params}, socket) do
changeset = User.changeset(%User{}, user_params)
{:noreply, assign(socket, form: to_form(changeset, action: :validate))}
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate">
<.date_picker field={@form[:birth_date]} class="date-picker">
<:label>Birth date</:label>
<:trigger>
<.heroicon name="hero-calendar" class="icon" />
</:trigger>
<:prev_trigger>
<.heroicon name="hero-chevron-left" class="icon" />
</:prev_trigger>
<:next_trigger>
<.heroicon name="hero-chevron-right" class="icon" />
</:next_trigger>
<:error :let={msg}>
<.heroicon name="hero-exclamation-circle" class="icon" />
{msg}
</:error>
</.date_picker>
</.form>
"""
end
endAPI
Requires a stable id on <.date_picker>.
| Function | Action | Returns |
|---|---|---|
set_value/2 | Set selected date(s) (client) | %Phoenix.LiveView.JS{} |
set_value/3 | Set selected date(s) (server) | socket |
format_value/2 | Format value for flash/logs | ISO string |
cast_params/2 | Normalize params for Ecto cast | map |
Events
Pick an event name and pass it to on_* on <.date_picker>.
Server events
| Event | When | Payload |
|---|---|---|
on_value_change="date_changed" | Selection changes | %{"id" => id, "value" => iso} |
on_open_change="open_changed" | Popover opens/closes | %{"id" => id, "open" => boolean} |
on_focus_change="focus_changed" | Focus moves | %{"id" => id, "value" => value} |
on_view_change="view_changed" | Visible month changes | %{"id" => id, ...} |
on_value_change
<.date_picker
class="date-picker"
controlled
value={@date_value}
on_value_change="date_changed"
>
<:label>Select a date</:label>
<:trigger><.heroicon name="hero-calendar" class="icon" /></:trigger>
<:prev_trigger><.heroicon name="hero-chevron-left" class="icon" /></:prev_trigger>
<:next_trigger><.heroicon name="hero-chevron-right" class="icon" /></:next_trigger>
</.date_picker>def handle_event("date_changed", %{"value" => value}, socket) do
params = Corex.DatePicker.cast_params("single", %{"value" => value})
changeset = MyApp.Form.DateForm.changeset_validate(%MyApp.Form.DateForm{}, params)
{:noreply, assign(socket, :form, to_form(changeset, action: :validate))}
endEvent value is always an ISO string (comma-joined for multiple/range). Use cast_params/2 before Ecto cast/3.
Client events
| Event | When | event.detail |
|---|---|---|
on_value_change_client="date-changed" | Selection changes | id, value |
on_open_change_client="open-changed" | Popover opens/closes | id, open |
on_focus_change_client="focus-changed" | Focus moves | id, value |
on_view_change_client="view-changed" | Visible month changes | id, ... |
on_visible_range_change_client="range-changed" | Visible range changes | id, ... |
Patterns
Controlled
Set controlled, bind value, and handle on_value_change. Pass ISO-8601 strings or Date.to_iso8601/1 for a Date assign.
<.date_picker
class="date-picker"
controlled
value={@due && Date.to_iso8601(@due)}
on_value_change="date_changed"
>
<:label>Due</:label>
<:trigger><.heroicon name="hero-calendar" class="icon" /></:trigger>
<:prev_trigger><.heroicon name="hero-chevron-left" class="icon" /></:prev_trigger>
<:next_trigger><.heroicon name="hero-chevron-right" class="icon" /></:next_trigger>
</.date_picker>assign(socket, :due, ~D[2024-01-15])Style
Use data attributes to target elements:
[data-scope="date-picker"][data-part="root"] {}
[data-scope="date-picker"][data-part="label"] {}
[data-scope="date-picker"][data-part="control"] {}
[data-scope="date-picker"][data-part="input"] {}
[data-scope="date-picker"][data-part="trigger"] {}
[data-scope="date-picker"][data-part="positioner"] {}
[data-scope="date-picker"][data-part="content"] {}@import "../corex/main.css";
@import "../corex/tokens/themes/neo/light.css";
@import "../corex/components/date-picker.css";Stack modifiers on the host (class on <.date_picker>).
Color
| Modifier | Classes |
|---|---|
| Default | date-picker |
| Accent | date-picker date-picker--accent |
| Brand | date-picker date-picker--brand |
| Alert | date-picker date-picker--alert |
| Info | date-picker date-picker--info |
| Success | date-picker date-picker--success |
Size
| Modifier | Classes |
|---|---|
| SM | date-picker date-picker--sm |
| MD | date-picker date-picker--md |
| LG | date-picker date-picker--lg |
| XL | date-picker date-picker--xl |
In selection_mode "range", the control shows two fields with side labels from translation (From / To by default, via gettext). In "multiple", a single field shows a comma‑separated list of the formatted selected dates. Use max_selected_dates to cap how many days can be selected in multiple mode; omit for no cap.
Localization and translation
Pass translation={%Corex.DatePicker.Translation{}} to override any string. Omitted fields use gettext defaults (see Corex.DatePicker.Translation).
Summary
Components
Renders a date picker component.
API
Set the selected date from a control (phx-click). Pass an ISO-8601 date string or Date.
Set the selected date from handle_event. Accepts ISO strings or Date (same as set_value/2).
Functions
Normalizes form or on_value_change params for Ecto cast/3.
Formats a date picker value for flash messages, toasts, and logs.
Components
Renders a date picker component.
Attributes
id(:string) - The unique identifier for the date picker. Required; use field={} to derive from field.id.value(:string) - The initial value or the controlled value (ISO date string). Defaults tonil.controlled(:boolean) - Whether the date picker is controlled. Only in LiveView, the on_value_change event is required. Defaults tofalse.locale(:string) - The locale for date formatting. Defaults tonil.time_zone(:string) - The time zone for date operations. Defaults tonil.dir(:string) - The direction of the date picker. When nil, derived from document (html lang + config :rtl_locales). Defaults tonil. Must be one ofnil,"ltr", or"rtl".on_value_change(:string) - The server event name when the value changes. Defaults tonil.on_focus_change(:string) - The server event name when focus changes. Defaults tonil.on_view_change(:string) - The server event name when the view changes. Defaults tonil.name(:string) - The name attribute of the input element. Defaults tonil.disabled(:boolean) - Whether the calendar is disabled. Defaults tofalse.read_only(:boolean) - Whether the calendar is read-only. Defaults tofalse.required(:boolean) - Whether the date picker is required. Defaults tofalse.invalid(:boolean) - Whether the date picker is invalid. Defaults tofalse.outside_day_selectable(:boolean) - Whether day outside the visible range can be selected. Defaults tofalse.close_on_select(:boolean) - If true, close the popover when selection is complete. Forselection_mode:multiple or :range, the default false keeps the panel open until dismissed unless you set this to true. Defaults tofalse.min(:string) - The minimum date that can be selected (ISO date string). Defaults tonil.max(:string) - The maximum date that can be selected (ISO date string). Defaults tonil.focused_value(:string) - The initial focused date when the calendar opens (ISO date string). Used as default in the picker. Defaults tonil.start_of_week(:integer) - The first day of the week (0=Sunday, 1=Monday, etc.). Defaults to0.fixed_weeks(:boolean) - Whether the calendar should have a fixed number of weeks (6 weeks). Defaults totrue.selection_mode(:string) - The selection mode of the calendar. Defaults to"single". Must be one of"single","multiple", or"range".placeholder(:string) - The placeholder text to display in the input. Defaults tonil.translation(:any) - PartialCorex.DatePicker.Translationstruct; omitted fields use gettext defaults (see localization section). Defaults tonil.max_selected_dates(:integer) - Whenselection_modeis "multiple", limits how many days can be selected. Omit for no cap. Defaults tonil.view(:string) - The initial view of the calendar (day, month, or year); passed to the client as the default view. Defaults to"day". Must be one of"day","month", or"year".min_view(:string) - The minimum view of the calendar. Defaults to"day". Must be one of"day","month", or"year".max_view(:string) - The maximum view of the calendar. Defaults to"year". Must be one of"day","month", or"year".positioning(Corex.Positioning) - Positioning options for the date picker content. Defaults to%Corex.Positioning{hide_when_detached: true, strategy: "fixed", placement: "bottom", gutter: 8, shift: 0, overflow_padding: 0, arrow_padding: 4, flip: true, slide: true, overlap: false, same_width: false, fit_viewport: true, offset: nil}.on_visible_range_change(:string) - The server event name when the visible range changes. Defaults tonil.on_open_change(:string) - The server event name when the calendar opens or closes. Defaults tonil.on_value_change_client(:string) - Fires a window-bubbling CustomEvent with this name when the value changes (optional; use with on_value_change for both). Defaults tonil.on_open_change_client(:string) - Fires a window-bubbling CustomEvent with this name when the calendar opens or closes (optional; use with on_open_change for both). Defaults tonil.on_focus_change_client(:string) - The client event name when focus changes. Defaults tonil.on_view_change_client(:string) - The client event name when the view changes. Defaults tonil.on_visible_range_change_client(:string) - The client event name when the visible range changes. Defaults tonil.errors(:list) - List of error messages to display. Defaults to[].field(Phoenix.HTML.FormField) - A form field struct from the form, e.g. @form[:birth_date]. Sets id, name, value, and errors from the field for form submission and LiveView resync.- Global attributes are accepted.
Slots
label- Accepts attributes:class(:string)
error- Accepts attributes:class(:string)
trigger- Accepts attributes:class(:string)
prev_trigger- Accepts attributes:class(:string)
next_trigger- Accepts attributes:class(:string)
API
Set the selected date from a control (phx-click). Pass an ISO-8601 date string or Date.
<.action phx-click={Corex.DatePicker.set_value("my-date-picker", "2024-06-01")}>June</.action>
<.date_picker id="my-date-picker" class="date-picker" value="2024-01-15">
<:label>Date</:label>
<:trigger><.heroicon name="hero-calendar" class="icon" /></:trigger>
</.date_picker>document.getElementById("my-date-picker")?.dispatchEvent(
new CustomEvent("corex:date-picker:set-value", {
bubbles: false,
detail: { value: "2024-06-01" },
})
);
Set the selected date from handle_event. Accepts ISO strings or Date (same as set_value/2).
<.action phx-click="pick_june" phx-value-value="2024-06-01">June</.action>
<.date_picker id="my-date-picker" class="date-picker" value="2024-01-15">
<:label>Date</:label>
<:trigger><.heroicon name="hero-calendar" class="icon" /></:trigger>
</.date_picker>def handle_event("pick_june", %{"value" => iso}, socket) do
{:noreply, Corex.DatePicker.set_value(socket, "my-date-picker", iso)}
end
Functions
Normalizes form or on_value_change params for Ecto cast/3.
Returns a map with the field key for the mode: "date", "dates", or "date_range".
Accepts ISO strings, comma-separated strings, lists, or %Date{} values.
Formats a date picker value for flash messages, toasts, and logs.
mode is "single", "multiple", or "range". Accepts %Date{}, ISO strings,
lists of dates, or comma-separated ISO strings. Never produces ~D[...] output.