keen_web_daterangepicker
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.
What's New in v1.0.0-rc.1
First release candidate. Bundles upstream @keenmate/web-daterangepickerv2.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.
Install
def deps do
[
{:keen_web_daterangepicker, "~> 1.0"}
]
end
Currently a release candidate. Only
1.0.0-rc.*is published so far, and Mix skips pre-releases for a plain~> 1.0constraint. Until1.0.0is final, opt in by requiring the pre-release explicitly:{:keen_web_daterangepicker, "~> 1.0.0-rc"}
Wire up the assets
The bundled JS and CSS live in this library's priv/static/ directory.
Quick start — the installer
On a standard esbuild Phoenix app, let the installer wire everything for you:
mix keen_web_daterangepicker.install
It 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.
Path A — import from deps/ (esbuild, the Phoenix default)
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.
Path B — serve directly from the dep's priv/static
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>.
Use the component
Import the component in the module where you render templates (a LiveView, a LiveComponent, or your MyAppWeb.html_helpers/0):
import Keenmate.WebDaterangepicker.Components
Declarative — no JavaScript needed
<.web_daterangepicker
id="checkin"
placeholder="Pick a date"
min_date="2026-01-01"
max_date="2026-12-31"
/>
Date range with two months and a summary
<.web_daterangepicker
id="stay"
selection_mode="range"
visible_months_count={2}
disabled_weekdays={[0, 6]}
disabled_dates_handling="block"
/>
LiveView events
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)}
end
Dates 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.
Driving the component from the server
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.
Server-driven feedback (loader / summary / message)
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)}
end
Helpers: 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.
Special dates (holidays, events, prices)
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.
Form integration
Pass a Phoenix.HTML.FormField and the component fills in id, name, and the initial value. Because the picker isn't form-associated, the wrapper renders a hidden input and the hook keeps it in sync — so form integration needs hook={true}:
<.simple_form for={@form} phx-change="validate">
<.web_daterangepicker field={@form[:stay]} hook={true} 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.
Attributes
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.
Theming
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.
Versioning
keen_web_daterangepicker versions are independent of @keenmate/web-daterangepicker. The bundled upstream version is reported by:
Keenmate.WebDaterangepicker.upstream_version()
#=> "2.0.0-rc03"
For LLMs and coding agents
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.
License
MIT.