defmodule Corex.DatePicker do @moduledoc ~S''' Phoenix implementation of [Zag.js Date Picker](https://zagjs.com/components/react/date-picker). ## Examples ### Basic Usage This example assumes the import of `.icon` from `Core Components` ```heex <.date_picker id="my-date-picker"> <:label>Select a date <:trigger> <.icon name="hero-calendar" /> <:prev_trigger> <.icon name="hero-chevron-left" class="icon" /> <:next_trigger> <.icon name="hero-chevron-right" class="icon" /> ``` ### Controlled Mode This example assumes the import of `.icon` from `Core Components` ```heex <.date_picker id="my-date-picker" controlled value={@date_value} on_value_change="date_changed"> <:label>Select a date <:trigger> <.icon name="hero-calendar" /> <:prev_trigger> <.icon name="hero-chevron-left" class="icon" /> <:next_trigger> <.icon name="hero-chevron-right" class="icon" /> ``` ```elixir def handle_event("date_changed", %{"value" => value}, socket) do {:noreply, assign(socket, :date_value, value)} end ``` ## Phoenix Form Integration When using with Phoenix forms, you must add an id to the form using the `Corex.Form.get_form_id/1` function. ### Controller ```elixir defmodule MyAppWeb.PageController do use MyAppWeb, :controller def home(conn, params) do form = Phoenix.Component.to_form(Map.get(params, "user", %{}), as: :user) render(conn, :home, form: form) end end ``` ```heex <.form :let={f} as={:user} for={@form} id={get_form_id(@form)} method="get"> <.date_picker field={f[:birth_date]} class="date-picker"> <:label>Birth date <:trigger> <.icon name="hero-calendar" class="icon" /> <:prev_trigger> <.icon name="hero-chevron-left" class="icon" /> <:next_trigger> <.icon name="hero-chevron-right" class="icon" /> <:error :let={msg}> <.icon name="hero-exclamation-circle" class="icon" /> {msg} ``` ### Live View When using Phoenix form in a Live view you must also add controlled mode. This allows the Live view to be the source of truth and the component to be in sync accordingly. ```elixir defmodule MyAppWeb.DatePickerLive do use MyAppWeb, :live_view def mount(_params, _session, socket) do form = to_form(%{"birth_date" => nil}, as: :user) {:ok, assign(socket, :form, form)} end def render(assigns) do ~H""" <.form as={:user} for={@form} id={get_form_id(@form)}> <.date_picker field={@form[:birth_date]} class="date-picker" controlled> <:label>Birth date <:trigger> <.icon name="hero-calendar" class="icon" /> <:prev_trigger> <.icon name="hero-chevron-left" class="icon" /> <:next_trigger> <.icon name="hero-chevron-right" class="icon" /> <:error :let={msg}> <.icon name="hero-exclamation-circle" class="icon" /> {msg} """ end end ``` ### With Ecto changeset When using Ecto changeset for validation and inside a Live view you must enable the controlled mode. This allows the Live View to be the source of truth and the component to be in sync accordingly. First create your schema and changeset: ```elixir 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 ``` ```elixir 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} id={get_form_id(@form)} phx-change="validate"> <.date_picker field={@form[:birth_date]} class="date-picker" controlled> <:label>Birth date <:trigger> <.icon name="hero-calendar" class="icon" /> <:prev_trigger> <.icon name="hero-chevron-left" class="icon" /> <:next_trigger> <.icon name="hero-chevron-right" class="icon" /> <:error :let={msg}> <.icon name="hero-exclamation-circle" class="icon" /> {msg} """ end end ``` ## API Control In order to use the API, you must use an id on the component ***Client-side*** ```heex ``` ***Server-side*** ```elixir def handle_event("set_date", _, socket) do {:noreply, Corex.DatePicker.set_value(socket, "my-date-picker", "2024-01-15")} end ``` ## Styling Use data attributes to target elements: ```css [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"] {} ``` If you wish to use the default Corex styling, you can use the class `date-picker` on the component. This requires to install `Mix.Tasks.Corex.Design` first and import the component css file. ```css @import "../corex/main.css"; @import "../corex/tokens/themes/neo/light.css"; @import "../corex/components/date-picker.css"; ``` You can then use modifiers ```heex <.date_picker class="date-picker date-picker--accent date-picker--lg" id="my-date-picker"> ``` Learn more about modifiers and [Corex Design](https://corex-ui.com/components/date-picker#modifiers) ''' use Phoenix.Component alias Corex.DatePicker.{Anatomy, Connect} @doc """ Renders a date picker component. """ attr(:id, :string, default: nil, doc: "The unique identifier for the date picker. Set automatically when using the field attr." ) attr(:value, :string, default: nil, doc: "The initial value or the controlled value (ISO date string)" ) attr(:controlled, :boolean, default: false, doc: "Whether the date picker is controlled. Only in LiveView, the on_value_change event is required" ) attr(:locale, :string, default: nil, doc: "The locale for date formatting" ) attr(:time_zone, :string, default: nil, doc: "The time zone for date operations" ) attr(:dir, :string, default: nil, values: [nil, "ltr", "rtl"], doc: "The direction of the date picker. When nil, derived from document (html lang + config :rtl_locales)" ) attr(:on_value_change, :string, default: nil, doc: "The server event name when the value changes" ) attr(:on_focus_change, :string, default: nil, doc: "The server event name when focus changes" ) attr(:on_view_change, :string, default: nil, doc: "The server event name when the view changes" ) attr(:name, :string, default: nil, doc: "The name attribute of the input element" ) attr(:disabled, :boolean, default: false, doc: "Whether the calendar is disabled" ) attr(:read_only, :boolean, default: false, doc: "Whether the calendar is read-only" ) attr(:required, :boolean, default: false, doc: "Whether the date picker is required" ) attr(:invalid, :boolean, default: false, doc: "Whether the date picker is invalid" ) attr(:outside_day_selectable, :boolean, default: false, doc: "Whether day outside the visible range can be selected" ) attr(:close_on_select, :boolean, default: true, doc: "Whether the calendar should close after the date selection is complete" ) attr(:min, :string, default: nil, doc: "The minimum date that can be selected (ISO date string)" ) attr(:max, :string, default: nil, doc: "The maximum date that can be selected (ISO date string)" ) attr(:focused_value, :string, default: nil, doc: "The initial focused date when the calendar opens (ISO date string). Used as default in the picker." ) attr(:num_of_months, :integer, default: 1, doc: "The number of months to display" ) attr(:start_of_week, :integer, default: 0, doc: "The first day of the week (0=Sunday, 1=Monday, etc.)" ) attr(:fixed_weeks, :boolean, default: true, doc: "Whether the calendar should have a fixed number of weeks (6 weeks)" ) attr(:selection_mode, :string, default: "single", values: ["single", "multiple", "range"], doc: "The selection mode of the calendar" ) attr(:placeholder, :string, default: nil, doc: "The placeholder text to display in the input" ) attr(:trigger_aria_label, :string, default: nil, doc: "Accessible name for the trigger button when it contains only an icon (e.g. \"Select date\")" ) attr(:input_aria_label, :string, default: nil, doc: "Accessible name for the input when it's not associated with a visible label (e.g. \"Select date\")" ) attr(:default_view, :string, default: "day", values: ["day", "month", "year"], doc: "The initial view of the calendar (day, month, or year)" ) attr(:min_view, :string, default: "day", values: ["day", "month", "year"], doc: "The minimum view of the calendar" ) attr(:max_view, :string, default: "year", values: ["day", "month", "year"], doc: "The maximum view of the calendar" ) attr(:positioning, Corex.Positioning, default: %Corex.Positioning{}, doc: "Positioning options for the date picker content" ) attr(:on_visible_range_change, :string, default: nil, doc: "The server event name when the visible range changes" ) attr(:on_open_change, :string, default: nil, doc: "The server event name when the calendar opens or closes" ) attr(:errors, :list, default: [], doc: "List of error messages to display" ) attr(:field, Phoenix.HTML.FormField, doc: "A form field struct from the form, e.g. @form[:birth_date]. Sets id, name, value, and errors from the field; enables controlled mode for LiveView." ) attr(:rest, :global) slot :label, required: false do attr(:class, :string, required: false) end slot :error, required: false do attr(:class, :string, required: false) end slot :trigger, required: false do attr(:class, :string, required: false) end slot :prev_trigger, required: false do attr(:class, :string, required: false) end slot :next_trigger, required: false do attr(:class, :string, required: false) end def date_picker(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] assigns |> assign(field: nil) |> assign(:errors, Enum.map(errors, &Corex.Gettext.translate_error(&1))) |> assign(:id, field.id) |> assign(:name, field.name) |> assign(:value, normalize_date_value(field.value)) |> date_picker() end def date_picker(assigns) do assigns = assigns |> assign(:id, assigns[:id] || "date-picker-#{System.unique_integer([:positive])}") ~H"""