Rewrote all the docs - more noob friendly now.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-05 23:55:26 +05:00
parent c1e1f74557
commit b530bb8c97
35 changed files with 2159 additions and 2341 deletions
+55 -97
View File
@@ -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 ← SunSat 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 JanDec 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.