Rewrote all the docs - more noob friendly now.
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,68 +1,20 @@
|
||||
# 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.
|
||||
A date-range picker. The user clicks once to set a start date and clicks again to set an end date. While hovering, the range between start and the cursor is shaded as a preview. Great for booking forms, report filters, or anything that needs a "from / to" date pair.
|
||||
|
||||
---
|
||||
|
||||
## HTML structure
|
||||
## Quick example
|
||||
|
||||
```
|
||||
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]
|
||||
```csharp
|
||||
new CalendarRange(id: "vacation", name: "vacation")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
This renders an empty picker. The user clicks two dates to form a range. Both dates are submitted with the form as `vacation-start` and `vacation-end`.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
## All the options
|
||||
|
||||
```csharp
|
||||
public CalendarRange(
|
||||
@@ -72,46 +24,32 @@ public CalendarRange(
|
||||
DateOnly? selectedEnd = null)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| Parameter | What it does |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `id` | A unique identifier. The root element gets `id="calr-{id}"`. |
|
||||
| `name` | Base form field name. The two hidden inputs become `{name}-start` and `{name}-end`. |
|
||||
| `selectedStart` | Pre-selected start date. |
|
||||
| `selectedEnd` | Pre-selected end date. |
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
## Real-world 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
|
||||
### Vacation request form
|
||||
|
||||
```html
|
||||
<!-- Templates/VacationForm.htmx -->
|
||||
<form method="post" action="/vacation">
|
||||
$$AntiforgeryToken$$
|
||||
<label class="text-sm font-medium">Select vacation dates</label>
|
||||
$$RangePicker$$
|
||||
<button type="submit">Request</button>
|
||||
<form method="post" action="/vacation" class="space-y-6">
|
||||
$$Token$$
|
||||
<div>
|
||||
<label class="text-sm font-medium">Select vacation dates</label>
|
||||
$$RangePicker$$
|
||||
</div>
|
||||
<button type="submit">Submit request</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
**Reading the submitted values:**
|
||||
**Reading the submitted values on the server:**
|
||||
|
||||
```csharp
|
||||
public record Command(
|
||||
@@ -119,34 +57,48 @@ public record Command(
|
||||
[property: FromForm] string VacationEnd // "yyyy-MM-dd"
|
||||
);
|
||||
|
||||
// Validate they are not empty before parsing
|
||||
if (string.IsNullOrEmpty(command.VacationStart) || string.IsNullOrEmpty(command.VacationEnd))
|
||||
return; // user did not complete the selection
|
||||
|
||||
var start = DateOnly.ParseExact(command.VacationStart, "yyyy-MM-dd");
|
||||
var end = DateOnly.ParseExact(command.VacationEnd, "yyyy-MM-dd");
|
||||
```
|
||||
|
||||
### Listening for range changes client-side
|
||||
### Pre-selected range (e.g. editing an existing request)
|
||||
|
||||
```csharp
|
||||
new CalendarRange(
|
||||
id: "vacation",
|
||||
name: "vacation",
|
||||
selectedStart: existingRequest.StartDate,
|
||||
selectedEnd: existingRequest.EndDate)
|
||||
```
|
||||
|
||||
### Reacting to selection changes in JavaScript
|
||||
|
||||
```js
|
||||
document.getElementById('calr-vacation').addEventListener('rangeChange', e => {
|
||||
console.log(e.detail.start, e.detail.end);
|
||||
// e.g. "2026-07-01", "2026-07-14"
|
||||
// Update a price estimate, nights count, etc.
|
||||
});
|
||||
```
|
||||
|
||||
### 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.
|
||||
The `.calr-label` element inside the calendar automatically updates to show `start → end` (or `start → pick end date` while mid-selection). You do not need custom JS for the label.
|
||||
|
||||
---
|
||||
|
||||
## Tips and tricks
|
||||
## How it works
|
||||
|
||||
- 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.
|
||||
The click logic follows three states:
|
||||
|
||||
---
|
||||
1. **Nothing selected** — first click sets the start date, clears the end
|
||||
2. **Start selected, no end** — next click after start sets the end and fires `rangeChange`; clicking before start moves the start; clicking on start again clears both
|
||||
3. **Both selected** — any click resets and starts again from step 1
|
||||
|
||||
The hover preview does not rebuild the grid. It only toggles CSS classes on the day buttons, so it is fast even for long ranges.
|
||||
|
||||
All state is stored in `data-start` and `data-end` attributes on the root element, not in closures, so HTMX-swapped calendars re-initialise correctly.
|
||||
|
||||
## Complete page example
|
||||
|
||||
|
||||
Reference in New Issue
Block a user