Packages
keen_web_daterangepicker
1.0.0-rc.1
Phoenix LiveView wrapper for the @keenmate/web-daterangepicker custom element (bundled JS+CSS, typed attrs for every documented option, optional LV hook).
Current section
Files
Jump to
Current section
Files
keen_web_daterangepicker
README.md
README.md
# keen_web_daterangepicker
Phoenix LiveView wrapper for [`@keenmate/web-daterangepicker`](https://github.com/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-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/1` declares a typed `attr/3` for 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 to `true` and need a real opt-out, not HTML presence. The initial `value` is 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 bundled `daterangepicker.js` + hook into `assets/js/app.js`, registers `KeenWebDaterangepickerHook` on your `LiveSocket` (merging into the stock `hooks: {...}` object), and imports `daterangepicker.css` into `assets/css/app.css`. Idempotent and conservative — anything it can't confidently patch is left untouched and printed as a manual step. `--dry-run` previews.
- **LiveView events + form integration — opt in with `hook={true}`** — The hook forwards the component's `date-select`/`change`/`custom-action` events to the server as `"web_daterangepicker:select"` / `":change"` (payload `{id, value}`) and `":custom_action"` (payload `{id, data}` — the clicked button's `data-*` map), so `handle_event/3` matches by id. `value` is 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, pass `field={@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 native `input` event), and `phx-change`/`phx-submit` see 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 the `phx-update="ignore"` boundary with `push_update(socket, "checkout", min_date: checkin)` (snake_case keys → camelCase JS properties; attribute-only inputs like `locale`/`placeholder` fall back to `setAttribute`). And drive the picker's message / summary / loader blocks from `handle_event`/`handle_info` with no client JS (plus `hide_*` / `toggle_*` and the low-level `push_call/4`) — e.g. `show_loader(socket, "cal", :summary)` then `show_summary(socket, "cal", price_html)` when an async lookup returns.
- **Read-only lock — `lock/3`, `unlock/3`, `toggle_lock/3`, and a declarative `readonly` attribute** — Freeze user interaction while keeping the value readable (unlike `disabled`, 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, add `readonly`. 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 …)}` and `weekday_names={~w(Ne Po …)}` render as native pipe-delimited `month-names` / `weekday-names` attributes straight on the element, so a purely declarative picker localizes its calendar labels without `hook={true}`. `weekday_names` is **indexed from Sunday** (index 0 = Sunday); `week_start_day` only 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 via `push_update/3` for 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](guides/theming.md) explains the two-tier `--drp-*` / `--base-*` cascade and the three integration paths (pure-admin, own base layer, standalone), plus dark-mode signals, `--drp-rem` sizing, and the unlayered-reset footgun. Alongside it, a flat-text `ai/` knowledge base (`ai/INDEX.txt` + topic files + a 13-recipe `ai/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.exs` carries a `homepage_url` and an "Examples site" link pointing at [keen-web-daterangepicker.keenmate.dev](https://keen-web-daterangepicker.keenmate.dev), a 1:1 mirror of the upstream example pages.
## Install
```elixir
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.0` constraint. Until `1.0.0` is final, opt in by
> requiring the pre-release explicitly:
>
> ```elixir
> {: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:
```sh
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`:
```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`:
```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`:
```elixir
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`):
```elixir
import Keenmate.WebDaterangepicker.Components
```
### Declarative — no JavaScript needed
```heex
<.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
```heex
<.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):
```heex
<.web_daterangepicker
id="stay"
hook={true}
selection_mode="range"
/>
```
```elixir
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. The `value` the hook forwards is the upstream `formattedValue` — 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):
```elixir
# 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}`:
```elixir
# 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}`**:
```heex
<.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}`**:
```heex
<.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](https://github.com/keenmate/web-daterangepicker/blob/main/docs/usage.md) 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](guides/theming.md)** 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:
```elixir
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](https://github.com/keenmate/keen-web-daterangepicker/tree/HEAD/ai), 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](https://llmstxt.org)
convention), so agents that fetch `hexdocs.pm/keen_web_daterangepicker/llms.txt` get a
structured index of the docs.
## License
MIT.