Rewrote all the docs - more noob friendly now.
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
+55
-97
@@ -1,68 +1,20 @@
|
||||
# Calendar
|
||||
|
||||
A single-date picker rendered server-side with full client-side interaction. The selected date is stored in a hidden input and submitted as part of a form. Supports three drill-down views: days → months → years.
|
||||
A date picker that lets the user click to select a single date. The selected date is stored in a hidden form input and submitted with the form. Think of it as a fancy `<input type="date">` that looks the same on every browser.
|
||||
|
||||
---
|
||||
|
||||
## HTML structure
|
||||
## Quick example
|
||||
|
||||
```
|
||||
div.calendar-root[id=cal-{id}, data-year, data-month, data-sel-day,
|
||||
data-sel-month, data-sel-year, data-view="days"]
|
||||
div.mb-3.flex.items-center.justify-between ← navigation row
|
||||
button.cal-prev ← previous month/year/decade
|
||||
button.cal-month-label ← shows "Month YYYY" / "YYYY" / decade range
|
||||
button.cal-next ← next
|
||||
div.cal-dow-row.grid.grid-cols-7 ← Sun–Sat headings (hidden in month/year views)
|
||||
div.cal-grid.grid.grid-cols-7 ← day/month/year cells, built by JS
|
||||
input.cal-hidden-input[type=hidden, name] ← holds selected date as yyyy-MM-dd
|
||||
```csharp
|
||||
new Calendar(id: "dob", name: "dateOfBirth")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS mechanics
|
||||
|
||||
| Class | Effect |
|
||||
|---|---|
|
||||
| `cal-day` | Base day button style (text-center, rounded, hover highlight) |
|
||||
| `cal-day-selected` | Filled primary circle on the selected day |
|
||||
| `cal-view-btn` | Base style for month/year selection buttons |
|
||||
| `cal-view-btn-selected` | Highlighted active month or year |
|
||||
| Grid is 7-column for days, 3-column for months/years | Switched via `gridTemplateColumns` inline style |
|
||||
That renders a calendar starting at today's date. When the user clicks a day, the hidden input is updated and the date is included in the form submission.
|
||||
|
||||
---
|
||||
|
||||
## JavaScript (`initCalendar` in `components.js`)
|
||||
|
||||
State is stored entirely in `data-*` attributes on the root element. JS reads and writes these attributes — no hidden state in closures.
|
||||
|
||||
### `renderCalendar(root)` — three view modes
|
||||
|
||||
**Days view:**
|
||||
1. Reads `data-year` and `data-month` (0-based, JS-style)
|
||||
2. Calculates leading empty cells for the first weekday offset
|
||||
3. Renders numbered `<button>` elements; adds `cal-day-selected` to the matching date
|
||||
4. Each day button stores `yyyy-MM-dd` in `data-date`
|
||||
5. On click: updates `data-sel-*`, highlights the new selection, writes value to `.cal-hidden-input`, fires `calendarChange` CustomEvent
|
||||
|
||||
**Months view:**
|
||||
- Renders Jan–Dec abbreviated buttons in a 3-column grid
|
||||
- Click drills back to days view for that month
|
||||
|
||||
**Years view:**
|
||||
- Renders 12 consecutive years (decade rounded to nearest 12)
|
||||
- Click drills back to months view for that year
|
||||
|
||||
### Navigation buttons
|
||||
- Prev/Next adjust month ± 1 (wrapping year), year ± 1, or decade ± 12 depending on `data-view`
|
||||
- Month-label click drills down: days → months → years (no further drill from years)
|
||||
|
||||
### Re-initialization
|
||||
`initAll` re-queries `.calendar-root` after `htmx:afterSwap`, so HTMX-swapped calendars work correctly.
|
||||
|
||||
---
|
||||
|
||||
## Constructor signature
|
||||
## All the options
|
||||
|
||||
```csharp
|
||||
public Calendar(
|
||||
@@ -71,83 +23,89 @@ public Calendar(
|
||||
DateOnly? selected = null)
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| Parameter | What it does |
|
||||
|---|---|
|
||||
| `id` | Logical id; element gets `id="cal-{id}"` |
|
||||
| `name` | Form field name for the hidden input |
|
||||
| `selected` | Pre-selected date; defaults to today |
|
||||
| `id` | A unique identifier for this calendar. The root element gets `id="cal-{id}"`. |
|
||||
| `name` | The form field name for the hidden input. Use this name in your `Command` record on the server. |
|
||||
| `selected` | The date to pre-select on render. Defaults to today. |
|
||||
|
||||
---
|
||||
|
||||
## Usage examples
|
||||
## Real-world examples
|
||||
|
||||
### Basic date picker
|
||||
|
||||
```csharp
|
||||
new Calendar(id: "dob", name: "dateOfBirth")
|
||||
```
|
||||
|
||||
### Pre-selected date
|
||||
|
||||
```csharp
|
||||
new Calendar(
|
||||
id: "appointment",
|
||||
name: "appointmentDate",
|
||||
selected: new DateOnly(2026, 9, 15))
|
||||
```
|
||||
|
||||
### Inside a form
|
||||
### Appointment booking form
|
||||
|
||||
```html
|
||||
<!-- Templates/BookingForm.htmx -->
|
||||
<form method="post" action="/book">
|
||||
$$AntiforgeryToken$$
|
||||
<label class="text-sm font-medium">Pick a date</label>
|
||||
$$DatePicker$$
|
||||
<button type="submit">Book</button>
|
||||
<form method="post" action="/book" class="space-y-6">
|
||||
$$Token$$
|
||||
<div>
|
||||
<label class="text-sm font-medium">Pick a date</label>
|
||||
$$DatePicker$$
|
||||
</div>
|
||||
<button type="submit">Book appointment</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
```csharp
|
||||
// Templates/BookingForm.htmx.cs
|
||||
public IHtmxComponent DatePicker { get; }
|
||||
|
||||
public BookingForm(string? afToken = null)
|
||||
public sealed class BookingForm : BookingFormBase
|
||||
{
|
||||
DatePicker = new Calendar(id: "booking", name: "bookingDate");
|
||||
_afTokenData = /* antiforgery hidden input */;
|
||||
}
|
||||
private readonly IHtmxComponent _datePicker;
|
||||
private readonly byte[] _tokenData;
|
||||
|
||||
protected override void RenderDatePicker(HtmxRenderContext ctx)
|
||||
=> DatePicker.Render(ctx.Next());
|
||||
public BookingForm(string antiforgeryToken)
|
||||
{
|
||||
_tokenData = $"""<input type="hidden" name="__RequestVerificationToken" value="{System.Web.HttpUtility.HtmlAttributeEncode(antiforgeryToken)}" />""".ToUtf8Bytes();
|
||||
_datePicker = new Calendar(id: "booking", name: "bookingDate");
|
||||
}
|
||||
|
||||
protected override void RenderToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_tokenData);
|
||||
protected override void RenderDatePicker(HtmxRenderContext ctx) => _datePicker.Render(ctx.Next());
|
||||
}
|
||||
```
|
||||
|
||||
**Reading the submitted value on the server:**
|
||||
**Reading the submitted date on the server:**
|
||||
|
||||
```csharp
|
||||
public record Command(
|
||||
[property: FromForm] string BookingDate // "yyyy-MM-dd"
|
||||
[property: FromForm] string BookingDate // arrives as "yyyy-MM-dd"
|
||||
);
|
||||
|
||||
// Parse:
|
||||
var date = DateOnly.ParseExact(command.BookingDate, "yyyy-MM-dd");
|
||||
```
|
||||
|
||||
### Listening for selection changes client-side
|
||||
### Pre-selected date (e.g. editing an existing booking)
|
||||
|
||||
```csharp
|
||||
new Calendar(
|
||||
id: "appointment",
|
||||
name: "appointmentDate",
|
||||
selected: existingBooking.Date)
|
||||
```
|
||||
|
||||
### Reacting to date selection in JavaScript
|
||||
|
||||
When the user picks a date, the calendar fires a `calendarChange` custom event:
|
||||
|
||||
```js
|
||||
document.getElementById('cal-appointment').addEventListener('calendarChange', e => {
|
||||
document.getElementById('cal-booking').addEventListener('calendarChange', e => {
|
||||
console.log(e.detail.date); // "2026-09-15"
|
||||
// update other UI elements based on selection
|
||||
// update price estimates, availability, etc.
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips and tricks
|
||||
## How it works
|
||||
|
||||
- The hidden input is always named with the `name` parameter — use this as the form field name when reading the submitted POST.
|
||||
- Months are 0-based in the JS `data-*` attributes (matching `Date` object convention) but the hidden input always stores `yyyy-MM-dd` with 1-based months.
|
||||
The calendar is rendered as static HTML by the server, with the current month's grid pre-built as `<button>` elements. JavaScript in `components.js` (`initCalendar`) takes over after the page loads:
|
||||
|
||||
- Clicking a day updates the hidden input and highlights the selected date.
|
||||
- Clicking the month/year label in the navigation row drills down: days → months → years. This lets the user jump to a different year quickly without clicking through months one at a time.
|
||||
- Prev/Next arrows move through the current view (month by month, year by year, or decade by decade).
|
||||
|
||||
All state is stored in `data-*` attributes on the root element — not in JavaScript closures. This means the calendar is fully re-initialised correctly when HTMX swaps it in.
|
||||
- If you need to clear the selection client-side, set `document.querySelector('#cal-myid .cal-hidden-input').value = ''` and remove `cal-day-selected` from any button.
|
||||
- To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the `calendarChange` event or override the generated grid with a custom `initCalendar` extension.
|
||||
- To restrict available dates (e.g. no past dates), post-process the rendered buttons in JS using the `calendarChange` event or override the generated grid with a custom `initCalendar` extension.
|
||||
|
||||
Reference in New Issue
Block a user