Corex.DatePicker (Corex v0.1.0)

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.

LayerFormat
value, events, HTMLISO strings (comma-joined for multiple/range)
field={@form[:date]}Accepts %Date{} or string; displayed as ISO
Ecto:date, {:array, :date}
Flash / logsformat_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))}
end

Controller

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
end
defmodule 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
end

API

Requires a stable id on <.date_picker>.

FunctionActionReturns
set_value/2Set selected date(s) (client)%Phoenix.LiveView.JS{}
set_value/3Set selected date(s) (server)socket
format_value/2Format value for flash/logsISO string
cast_params/2Normalize params for Ecto castmap

Events

Pick an event name and pass it to on_* on <.date_picker>.

Server events

EventWhenPayload
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))}
end

Event value is always an ISO string (comma-joined for multiple/range). Use cast_params/2 before Ecto cast/3.

Client events

EventWhenevent.detail
on_value_change_client="date-changed"Selection changesid, value
on_open_change_client="open-changed"Popover opens/closesid, open
on_focus_change_client="focus-changed"Focus movesid, value
on_view_change_client="view-changed"Visible month changesid, ...
on_visible_range_change_client="range-changed"Visible range changesid, ...

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

ModifierClasses
Defaultdate-picker
Accentdate-picker date-picker--accent
Branddate-picker date-picker--brand
Alertdate-picker date-picker--alert
Infodate-picker date-picker--info
Successdate-picker date-picker--success

Size

ModifierClasses
SMdate-picker date-picker--sm
MDdate-picker date-picker--md
LGdate-picker date-picker--lg
XLdate-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

date_picker(assigns)

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 to nil.
  • controlled (:boolean) - Whether the date picker is controlled. Only in LiveView, the on_value_change event is required. Defaults to false.
  • locale (:string) - The locale for date formatting. Defaults to nil.
  • time_zone (:string) - The time zone for date operations. Defaults to nil.
  • dir (:string) - The direction of the date picker. When nil, derived from document (html lang + config :rtl_locales). Defaults to nil. Must be one of nil, "ltr", or "rtl".
  • on_value_change (:string) - The server event name when the value changes. Defaults to nil.
  • on_focus_change (:string) - The server event name when focus changes. Defaults to nil.
  • on_view_change (:string) - The server event name when the view changes. Defaults to nil.
  • name (:string) - The name attribute of the input element. Defaults to nil.
  • disabled (:boolean) - Whether the calendar is disabled. Defaults to false.
  • read_only (:boolean) - Whether the calendar is read-only. Defaults to false.
  • required (:boolean) - Whether the date picker is required. Defaults to false.
  • invalid (:boolean) - Whether the date picker is invalid. Defaults to false.
  • outside_day_selectable (:boolean) - Whether day outside the visible range can be selected. Defaults to false.
  • close_on_select (:boolean) - If true, close the popover when selection is complete. For selection_mode :multiple or :range, the default false keeps the panel open until dismissed unless you set this to true. Defaults to false.
  • min (:string) - The minimum date that can be selected (ISO date string). Defaults to nil.
  • max (:string) - The maximum date that can be selected (ISO date string). Defaults to nil.
  • focused_value (:string) - The initial focused date when the calendar opens (ISO date string). Used as default in the picker. Defaults to nil.
  • start_of_week (:integer) - The first day of the week (0=Sunday, 1=Monday, etc.). Defaults to 0.
  • fixed_weeks (:boolean) - Whether the calendar should have a fixed number of weeks (6 weeks). Defaults to true.
  • 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 to nil.
  • translation (:any) - Partial Corex.DatePicker.Translation struct; omitted fields use gettext defaults (see localization section). Defaults to nil.
  • max_selected_dates (:integer) - When selection_mode is "multiple", limits how many days can be selected. Omit for no cap. Defaults to nil.
  • 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 to nil.
  • on_open_change (:string) - The server event name when the calendar opens or closes. Defaults to nil.
  • 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 to nil.
  • 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 to nil.
  • on_focus_change_client (:string) - The client event name when focus changes. Defaults to nil.
  • on_view_change_client (:string) - The client event name when the view changes. Defaults to nil.
  • on_visible_range_change_client (:string) - The client event name when the visible range changes. Defaults to nil.
  • 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_value(date_picker_id, value)

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_value(socket, date_picker_id, value)

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

cast_params(mode, params)

@spec cast_params(String.t(), map()) :: map()

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.

format_value(mode, date)

@spec format_value(String.t(), term()) :: String.t()

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.