# CalendarRange A date-range picker. The user selects a start date and then an end date. Hover preview shades the range before the second click commits it. Fires a `rangeChange` CustomEvent on every selection change. --- ## HTML structure ``` div.calr-root[id=calr-{id}, data-year, data-month, data-start, data-end, data-view="days"] div.mb-3.flex.items-center.justify-between ← navigation row button.calr-prev button.calr-month-label button.calr-next div.cal-dow-row.grid.grid-cols-7 ← day-of-week headings div.calr-grid.grid.grid-cols-7 ← day cells, rebuilt by JS on each interaction span.calr-label ← "start → end" or "start → pick end date" input.calr-hidden-start[type=hidden, name={name}-start] input.calr-hidden-end[type=hidden, name={name}-end] ``` --- ## CSS mechanics | Class | Effect | |---|---| | `calr-day-start` | Filled primary circle on start date | | `calr-day-end` | Filled primary circle on end date | | `calr-day-mid` | Lighter primary tint for dates between start and end | | `calr-day-plain` | Default un-selected day style | Hover preview is applied by `updateHoverClasses` by toggling the same CSS classes without rebuilding the DOM. --- ## JavaScript (`initCalendarRange` in `components.js`) ### State Stored in `data-start` and `data-end` attributes on the root (empty string = not selected). ### Click logic (`grid.onclick`) 1. **Nothing or both selected** → set `start = clicked`, clear `end` 2. **Only start selected:** - Click after start → set `end`, fire `rangeChange` - Click before start → move `start` to clicked, clear `end` - Click on start → clear both (toggle off) 3. Writes values to hidden inputs, fires `rangeChange` CustomEvent: `{ start: "yyyy-MM-dd", end: "yyyy-MM-dd" }` 4. Calls `renderRange` to rebuild grid and `updateLabel` to update the text summary ### Hover preview (`updateHoverClasses`) - Runs on `grid.onmouseover` without rebuilding the grid — only toggles CSS classes - Shades the tentative range from `start` to the hovered date before a click commits it - Cleared on `grid.onmouseleave` ### View navigation Same as Calendar: Prev/Next, month-label click drills days → months → years. `renderRange` rebuilds the grid on each navigation. --- ## Constructor signature ```csharp public CalendarRange( string id, string name = "date", DateOnly? selectedStart = null, DateOnly? selectedEnd = null) ``` | Parameter | Description | |---|---| | `id` | Logical id; element gets `id="calr-{id}"` | | `name` | Base form field name; hidden inputs are `{name}-start` and `{name}-end` | | `selectedStart` | Pre-selected start date | | `selectedEnd` | Pre-selected end date | --- ## Usage examples ### Empty picker ```csharp new CalendarRange(id: "vacation", name: "vacation") ``` ### Pre-selected range ```csharp new CalendarRange( id: "vacation", name: "vacation", selectedStart: new DateOnly(2026, 7, 1), selectedEnd: new DateOnly(2026, 7, 14)) ``` ### Inside a form ```html
``` **Reading the submitted values:** ```csharp public record Command( [property: FromForm] string VacationStart, // "yyyy-MM-dd" [property: FromForm] string VacationEnd // "yyyy-MM-dd" ); var start = DateOnly.ParseExact(command.VacationStart, "yyyy-MM-dd"); var end = DateOnly.ParseExact(command.VacationEnd, "yyyy-MM-dd"); ``` ### Listening for range changes client-side ```js document.getElementById('calr-vacation').addEventListener('rangeChange', e => { console.log(e.detail.start, e.detail.end); // e.g. "2026-07-01", "2026-07-14" }); ``` ### Showing a summary label elsewhere on the page The `.calr-label` span inside the component automatically updates to show `start → end` or `start → pick end date`. You don't need custom JS for this. --- ## Tips and tricks - Both hidden inputs are always submitted with the form. An empty string means the date was not selected — validate server-side before parsing. - The user can clear the selection by clicking the start date again after both are set. - To enforce a minimum range length (e.g. at least 2 nights), use the `rangeChange` event to validate client-side and show an error message. - The calendar always shows a single month. For a two-month range view, render two `CalendarRange` components side by side and sync their state via the `rangeChange` event. - The calendar always shows a single month. For a two-month range view, render two `CalendarRange` components side by side and sync their state via the `rangeChange` event. --- ## Complete page example **`Templates/ReportRangePage.htmx`** ```html