PhiaUi.Components.Calendar (phia_ui v0.1.17)

Copy Markdown View Source

Server-rendered calendar component for date selection with keyboard navigation.

The calendar grid is built entirely server-side using Elixir's Date module. State (current month, selected date or range) lives in the parent LiveView. Day clicks and month navigation fire phx-click events back to the LiveView.

Supports two selection modes:

  • :single — selects a single Date.t() value
  • :range — highlights an interval between :range_start and :range_end

Keyboard navigation (Arrow keys, Enter, Home, End) is provided by the PhiaCalendar JS hook registered on the root element.

When to use

Use calendar/1 as the foundational date-picking primitive. Higher-level components compose it:

  • date_picker/1 — wraps calendar/1 in a popover with a trigger button
  • date_range_picker/1 — renders two calendar/1 instances side-by-side for range selection

Use calendar/1 directly when you want a permanently visible calendar (e.g. a booking widget, an event scheduler, a date navigator).

ARIA

The grid uses role="grid". Day-of-week headers use role="columnheader". Individual day cells use role="gridcell" with aria-selected and aria-disabled reflecting state. Disabled buttons have the disabled HTML attribute to prevent interaction and remove them from the tab order.

Single-date example

defmodule MyAppWeb.BookingLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    today = Date.utc_today()
    {:ok, assign(socket,
      current_month: Date.beginning_of_month(today),
      selected_date: nil
    )}
  end

  def handle_event("date-selected", %{"date" => iso}, socket) do
    date = Date.from_iso8601!(iso)
    {:noreply, assign(socket, selected_date: date)}
  end

  def handle_event("calendar-prev-month", %{"month" => iso}, socket) do
    {:noreply, assign(socket, current_month: Date.from_iso8601!(iso))}
  end

  def handle_event("calendar-next-month", %{"month" => iso}, socket) do
    {:noreply, assign(socket, current_month: Date.from_iso8601!(iso))}
  end
end

<%!-- Template --%>
<.calendar
  id="booking-calendar"
  current_month={@current_month}
  value={@selected_date}
  on_change="date-selected"
  min={Date.utc_today()}
/>
<p :if={@selected_date}>
  Selected: {Calendar.strftime(@selected_date, "%B %d, %Y")}
</p>

Range selection example

<.calendar
  id="vacation-range"
  current_month={@current_month}
  mode="range"
  range_start={@check_in}
  range_end={@check_out}
  on_change="date-selected"
  min={Date.utc_today()}
/>

Disabling specific dates

Pass disabled_dates to block holiday or unavailable dates:

<.calendar
  id="appointment-picker"
  value={@appointment_date}
  disabled_dates={@fully_booked_dates}
  min={Date.utc_today()}
  max={Date.add(Date.utc_today(), 90)}
  on_change="date-selected"
/>

Hook setup

# app.js
import PhiaCalendar from "./hooks/calendar"
let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { PhiaCalendar }
})

Summary

Functions

Renders a server-rendered, accessible calendar grid for date selection.

Functions

calendar(assigns)

Renders a server-rendered, accessible calendar grid for date selection.

All calendar geometry (weeks, leading/trailing days, cell states) is computed server-side in build_weeks/1 and compute_cell_states/4 before the template renders. This keeps the HEEx template declarative and free of logic.

The PhiaCalendar hook handles keyboard navigation: Arrow keys move focus between day cells, Enter selects the focused cell, Home/End jump to the first/last day of the displayed month.

Attributes

  • id (:string) - Unique calendar ID. Used as the hook anchor and as a prefix for the embedded calendar in date_picker/1. Must be unique per page.

    Defaults to "calendar".

  • value (:any) - Selected date (Date.t()) in :single mode, or nil for no selection. Defaults to nil.

  • current_month (:any) - Currently displayed month (Date.t()). When nil, defaults to the month containing value, or the current month if value is also nil. The LiveView must update this assign in response to prev/next month events.

    Defaults to nil.

  • mode (:string) - Selection mode:

    • "single" — highlights one selected date
    • "range" — highlights the interval between range_start and range_end

    Defaults to "single". Must be one of "single", or "range".

  • range_start (:any) - Range selection start date (Date.t()) — only used in mode="range". Defaults to nil.

  • range_end (:any) - Range selection end date (Date.t()) — only used in mode="range". Defaults to nil.

  • min (:any) - Minimum selectable date (Date.t()). Dates before this are disabled. Defaults to nil.

  • max (:any) - Maximum selectable date (Date.t()). Dates after this are disabled. Defaults to nil.

  • disabled_dates (:list) - Explicit list of Date.t() values that cannot be selected regardless of min/max. Use for holidays, fully-booked slots, or blackout dates.

    Defaults to [].

  • on_change (:string) - phx-click event name fired when a day button is clicked. The LiveView receives %{"date" => "YYYY-MM-DD"}.

    Defaults to "calendar-change".

  • class (:string) - Additional CSS classes merged onto the root element via cn/1. Defaults to nil.

  • Global attributes are accepted. HTML attributes forwarded to the root div element.