Phoenix LiveView wrapper for @keenmate/web-daterangepicker — a themeable date & date-range picker web component with single/range/multiple selection, a time picker, month grids, disabled-date handling, special-date badges, and full keyboard navigation.
One package covers both plain HEEx and LiveView. The upstream JS + CSS are bundled, so no npm install is required.
Majors to 2.0 to track the upstream component's 2.x line. Bundles upstream @keenmate/web-daterangepicker v2.0.0-rc06, spanning two upstream releases: the rc05 "core adoption" (the element rebuilt on @keenmate/web-components-core / BlissElement) and the rc06 native form model. The headline change is form integration — read the first bullet before upgrading.
- Forms are now native — no hook needed, and the wrapper's hidden input is gone.
<web-daterangepicker>is now a form-associated custom element: the wrapper putsnameon the element, which renders its own hidden<input>inside the<form>and keeps it in sync with every pick — acrossinline/floating/modal. So a plain<.web_daterangepicker field={@form[:stay]} />works withphx-change/phx-submit/form.reset()withouthook={true}(previously forms required the hook to sync a wrapper-rendered hidden input, and inline pickers submitted nothing). Behavior change: the submitted value is now stable ISO-8601 — a range is"2026-06-15/2026-06-20"(a/interval, not the old" - "join); a server parsing form params must expect this. Newvalue_formatattr ("iso"|"json"|"array") picks the shape. - Upstream core rework (rc05), no wrapper API change. The element's hand-rolled custom-element plumbing moved to the shared core (attribute parsing, reactivity, reflection, pre-upgrade property lifting); positioning routes through core (so
@floating-ui/domis no longer a separate concern), and logging moved onto core's category loggers. All internal — the bundleddaterangepicker.{js,css,d.ts}were re-cut from the rc06 dist andupstream_version/0now reports"2.0.0-rc06". The dist bundles core + floating-ui inline, so the single-file import stays self-contained (no new npm dependency). - Fixes you get for free. The selection summary is now text-selectable (copy day counts / prices / booking refs written via
show_summary/3); the rolling month/year selector no longer bleeds over the sticky header; badge tooltips no longer render off-screen; and a dual-path input set as both attribute and property (e.g.disabled_dates/special_datesseeded declaratively then pushed) now resolves property-wins on upgrade. - New upstream escape hatches (optional). The element gains managed
onDateSelect/onChange/onCustomActionhandler properties (each receives theCustomEvent), anel.pickergetter exposing the live engine, andel.form/event.target.formresolution. The wrapper's hook keeps usingaddEventListener, so these are purely additive — reach for them only in your own client-sidephx-hook.
First release candidate. Bundles upstream @keenmate/web-daterangepicker v2.0.0-rc03 — no npm install.
- Component —
<.web_daterangepicker>covers the full upstream API as a pure render —Keenmate.WebDaterangepicker.Components.web_daterangepicker/1declares a typedattr/3for every documented<web-daterangepicker>attribute (booleans,values:-whitelisted enums, integers, date strings), mapping snake_case in HEEx to kebab-case on the element (selection_mode→selection-mode). Booleans render as explicit"true"/"false"because several upstream booleans default totrueand need a real opt-out, not HTML presence. The initialvalueis emitted on the element, so a standalone<.web_daterangepicker value="…" />shows its selection. No GenServer, no state — the same call works identically in a dead view and a LiveView. - One-command installer —
mix keen_web_daterangepicker.install— Wires a standard esbuild Phoenix app for you: imports the bundleddaterangepicker.js+ hook intoassets/js/app.js, registersKeenWebDaterangepickerHookon yourLiveSocket(merging into the stockhooks: {...}object), and importsdaterangepicker.cssintoassets/css/app.css. Idempotent and conservative — anything it can't confidently patch is left untouched and printed as a manual step.--dry-runpreviews. - LiveView events + form integration — opt in with
hook={true}— The hook forwards the component'sdate-select/change/custom-actionevents to the server as"web_daterangepicker:select"/":change"(payload{id, value}) and":custom_action"(payload{id, data}— the clicked button'sdata-*map), sohandle_event/3matches by id.valueis the calendar-date string (never a timezone-shifting ISO timestamp); inline pickers, which have no text input, derive it from the event's typed date fields. For forms, passfield={@form[:stay]}— the picker lives in Shadow DOM so isn't natively form-associated, so the wrapper renders a hidden input the hook mirrors each pick into (dispatching a nativeinputevent), andphx-change/phx-submitsee the value like any input. - Server-driven updates + feedback —
push_update/3,show_message/4,show_summary/3,show_loader/3— Push a new value, restrictions, or special dates across thephx-update="ignore"boundary withpush_update(socket, "checkout", min_date: checkin)(snake_case keys → camelCase JS properties; attribute-only inputs likelocale/placeholderfall back tosetAttribute). And drive the picker's message / summary / loader blocks fromhandle_event/handle_infowith no client JS (plushide_*/toggle_*and the low-levelpush_call/4) — e.g.show_loader(socket, "cal", :summary)thenshow_summary(socket, "cal", price_html)when an async lookup returns. - Read-only lock —
lock/3,unlock/3,toggle_lock/3, and a declarativereadonlyattribute — Freeze user interaction while keeping the value readable (unlikedisabled, which greys the input and, for an inline picker, does nothing). The motivating case is locking a confirmed booking so the range can't be changed. Lock everything, or compose aspects —lock(socket, "stay", [:selection, :actions])freezes the range and buttons while leaving</>month browsing live (aspects::selection,:navigation,:actions,:open). For a static full lock at render time, addreadonly. The programmatic API (push_update/3,clearSelection) keeps working while locked. - Declarative localization, no hook needed —
custom_strings/month_names/weekday_names—month_names={~w(Leden Únor …)}andweekday_names={~w(Ne Po …)}render as native pipe-delimitedmonth-names/weekday-namesattributes straight on the element, so a purely declarative picker localizes its calendar labels withouthook={true}.weekday_namesis indexed from Sunday (index 0 = Sunday);week_start_dayonly rotates the display.custom_strings(a map of UI-label overrides) is property-only upstream, so it ships as JSON and needs the hook. All three can also be pushed at runtime viapush_update/3for a server-side locale switcher. - Docs — a Theming guide + an LLM
ai/knowledge base ship in the package — The picker is styled entirely through CSS custom properties, so the Theming guide explains the two-tier--drp-*/--base-*cascade and the three integration paths (pure-admin, own base layer, standalone), plus dark-mode signals,--drp-remsizing, and the unlayered-reset footgun. Alongside it, a flat-textai/knowledge base (ai/INDEX.txt+ topic files + a 13-recipeai/cookbook.txt), fronted on hexdocs by a Using with AI agents guide, teaches a coding agent the component, hook,push_update/3, restrictions, special dates, i18n, forms, and theming. - Package — the live examples site is linked from Hex —
mix.exscarries ahomepage_urland an "Examples site" link pointing at keen-web-daterangepicker.keenmate.dev, a 1:1 mirror of the upstream example pages.
def deps do
[
{:keen_web_daterangepicker, "~> 2.0"}
]
endCurrently a release candidate. The current release is
2.0.0-rc.*, and Mix skips pre-releases for a plain~> 2.0constraint. Until2.0.0is final, opt in by requiring the pre-release explicitly:{:keen_web_daterangepicker, "~> 2.0.0-rc"}
The bundled JS and CSS live in this library's priv/static/ directory.
On a standard esbuild Phoenix app, let the installer wire everything for you:
mix keen_web_daterangepicker.installIt edits assets/js/app.js (imports + LiveSocket hook registration) and
assets/css/app.css (stylesheet import), idempotently — re-running is safe. Pass
--dry-run to preview. Anything it can't confidently patch is left untouched and
printed as a manual step. To wire it by hand, use one of the two paths below.
In assets/js/app.js:
import KeenWebDaterangepickerHook from "../../deps/keen_web_daterangepicker/priv/static/keen_web_daterangepicker_hook.js";
import "../../deps/keen_web_daterangepicker/priv/static/daterangepicker.js";
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { KeenWebDaterangepickerHook },
params: { _csrf_token: csrfToken }
});In assets/css/app.css:
@import "../../deps/keen_web_daterangepicker/priv/static/daterangepicker.css";The web component injects its own styles into its Shadow DOM, so the CSS import is only strictly required for
positioning_mode="inline"(which renders in light DOM) and for overriding the theme variables at document level. Importing it is harmless and recommended.
In your endpoint, add another Plug.Static:
plug Plug.Static,
at: "/keen_web_daterangepicker",
from: {:keen_web_daterangepicker, "priv/static"},
gzip: false,
only: ~w(daterangepicker.js daterangepicker.css keen_web_daterangepicker_hook.js)Then reference /keen_web_daterangepicker/daterangepicker.js from your layout
<script type="module"> tag and the CSS from a <link>.
Import the component in the module where you render templates (a LiveView, a LiveComponent, or your MyAppWeb.html_helpers/0):
import Keenmate.WebDaterangepicker.Components<.web_daterangepicker
id="checkin"
placeholder="Pick a date"
min_date="2026-01-01"
max_date="2026-12-31"
/><.web_daterangepicker
id="stay"
selection_mode="range"
visible_months_count={2}
disabled_weekdays={[0, 6]}
disabled_dates_handling="block"
/>Set hook={true} and the hook forwards the upstream date-select, change, and custom-action events to your LiveView (pass a string instead to name a custom hook):
<.web_daterangepicker
id="stay"
hook={true}
selection_mode="range"
/>def handle_event("web_daterangepicker:change", %{"id" => "stay", "value" => value}, socket) do
# value is a calendar-date string, e.g. "2026-04-30 - 2026-05-04"
{:noreply, assign(socket, :stay, value)}
endDates are strings, not timestamps.
<web-daterangepicker>is a calendar-date picker. Thevaluethe hook forwards is the upstreamformattedValue— a"YYYY-MM-DD"string. Keep dates as strings end-to-end; converting to an ISO timestamp (toISOString()) shifts them a day for non-UTC users.
Because the element renders phx-update="ignore", LiveView's DOM patcher won't push
new values or restrictions to it. Use Keenmate.WebDaterangepicker.push_update/3
(the sanctioned channel — it sends the event KeenWebDaterangepickerHook listens for):
# Cascading pickers — check-in changed, raise the check-out minimum
def handle_event("web_daterangepicker:change", %{"id" => "checkin", "value" => v}, socket) do
{:noreply, Keenmate.WebDaterangepicker.push_update(socket, "checkout", min_date: v)}
end
# Server-authoritative availability — push freshly-booked dates as disabled
Keenmate.WebDaterangepicker.push_update(socket, "stay", disabled_dates: booked_dates(listing))Snake_case keys become camelCase JS properties, so any documented property works
(value, min_date, max_date, disabled_dates, special_dates, selection_mode,
disabled, …). Only the keys you pass are sent. The target needs hook={true} and a
matching id.
Upstream v2.0.0 gave the picker's message, summary, and loader blocks an
imperative API. The wrapper exposes it as LiveView helpers, so you can drive the
picker's feedback UI from the server — a spinner while an async lookup runs, then the
result — with no client JavaScript. Like push_update/3, these need hook={true}:
# Show a spinner in the summary block, look the price up, then pin it
def handle_event("web_daterangepicker:change", %{"id" => "stay", "value" => _v}, socket) do
send(self(), :price_it)
{:noreply, Keenmate.WebDaterangepicker.show_loader(socket, "stay", :summary)}
end
def handle_info(:price_it, socket) do
{:noreply,
socket
|> Keenmate.WebDaterangepicker.hide_loader("stay", :summary)
|> Keenmate.WebDaterangepicker.show_summary("stay", "<strong>€474</strong> / 6 nights")
|> Keenmate.WebDaterangepicker.show_message("stay", "Available!", type: "success", auto_hide: 3000)}
endHelpers: show_message/4 · hide_message/2 · toggle_message/4 · show_summary/3 ·
hide_summary/2 · refresh_summary/2 · show_loader/3 · hide_loader/3 ·
toggle_loader/3 (loader target is :calendar | :message | :summary). All are
thin wrappers over push_call/4, which invokes any element method by name.
Decorate specific days with badges, tooltips, and CSS classes. special_dates is a
property-only input upstream, so the wrapper ships it as JSON and the hook applies it —
it requires hook={true}:
<.web_daterangepicker
id="calendar"
hook={true}
special_dates={[
%{date: "2026-12-25", badge_text: "🎄", day_tooltip: "Christmas", day_class: "holiday"},
%{date: "2026-07-04", badge_text: "🎆", day_tooltip: "Independence Day", day_class: "holiday"}
]}
/>The map keys follow the *_member attributes (date_member, badge_text_member,
day_class_member, …); the defaults above match upstream's canonical shape.
Pass a Phoenix.HTML.FormField and the component fills in id, name, and the initial value. The picker is a form-associated custom element (upstream v2.0.0-rc06): given a name it renders its own hidden <input> inside the <form> and keeps it in sync with every pick, so form integration works with no hook — including inline pickers:
<.simple_form for={@form} phx-change="validate">
<.web_daterangepicker field={@form[:stay]} selection_mode="range" />
</.simple_form>phx-change and phx-submit then see the selected value in params[form_name]["stay"]
just like a native input. The submitted value is stable ISO-8601, independent of the
display masks — a single date is "2026-06-15", a range is "2026-06-15/2026-06-20", and
multiple dates are joined by ,. Set value_format="json" for a JSON.stringifyd
selection, or value_format="array" to submit multiple name[] inputs (a range flattens to
start, end). Add hook={true} only when you also want server-side handle_event/3,
push_update/3, or the property-only custom_strings / special_dates.
Every documented attribute from the upstream component is exposed as a typed attr/3. See Keenmate.WebDaterangepicker.Components.web_daterangepicker/1 for the full list, or the upstream usage docs for what each one does.
Snake_case in HEEx maps to kebab-case on the rendered element: selection_mode → selection-mode, visible_months_count → visible-months-count, etc.
The picker is styled entirely through CSS custom properties, in a two-tier
cascade: component tokens (--drp-*) each fall back to a shared design token
(--base-*), so it works out of the box, inherits a design system when one is
present, and is overridable per-instance from HEEx via class / style.
See the Theming guide for the three integration paths:
- With pure-admin — the picker inherits the
--base-*tokens pure-admin provides, matching palette and dark mode with zero configuration. - With other KeenMate components (no pure-admin) — define the
--base-*layer yourself once as a single source of truth; every component reads it. - Standalone — built-in
light-dark()fallbacks give a working light/dark theme; override--drp-*to restyle just the picker.
keen_web_daterangepicker versions are independent of @keenmate/web-daterangepicker. The bundled upstream version is reported by:
Keenmate.WebDaterangepicker.upstream_version()
#=> "2.0.0-rc06"A flat-text knowledge base for coding agents ships in the package under the ai/
folder — modelled on the sibling keen_web_multiselect layout but written for
this wrapper. Browse it in the
repository, or
read it from deps/keen_web_daterangepicker/ai/ in a consuming app: start at
ai/INDEX.txt (keyword index + common questions) or ai/cookbook.txt
(copy-paste recipes).
On hexdocs, see the Using with AI agents page. ex_doc also publishes a
machine-readable llms.txt for the package (the llms.txt
convention), so agents that fetch hexdocs.pm/keen_web_daterangepicker/llms.txt get a
structured index of the docs.
MIT.