# 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. --- ## HTML structure ``` 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 ``` --- ## 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 | --- ## 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 ` ``` ```csharp // Templates/BookingForm.htmx.cs public IHtmxComponent DatePicker { get; } public BookingForm(string? afToken = null) { DatePicker = new Calendar(id: "booking", name: "bookingDate"); _afTokenData = /* antiforgery hidden input */; } protected override void RenderDatePicker(HtmxRenderContext ctx) => DatePicker.Render(ctx.Next()); ``` **Reading the submitted value on the server:** ```csharp public record Command( [property: FromForm] string BookingDate // "yyyy-MM-dd" ); // Parse: var date = DateOnly.ParseExact(command.BookingDate, "yyyy-MM-dd"); ``` ### Listening for selection changes client-side ```js document.getElementById('cal-appointment').addEventListener('calendarChange', e => { console.log(e.detail.date); // "2026-09-15" // update other UI elements based on selection }); ``` --- ## Tips and tricks - 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. - 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. --- ## Complete page example **`Templates/AppointmentPage.htmx`** ```html

Book an appointment

$$AntiforgeryToken$$
$$DatePicker$$
$$SubmitBtn$$
$$Confirmation$$
``` **`Templates/AppointmentPage.htmx.cs`** ```csharp namespace Htmx.ApiDemo.Templates; public sealed class AppointmentPage : AppointmentPageBase { private readonly IHtmxComponent _datePicker; private readonly IHtmxComponent _submitBtn; private readonly IHtmxComponent _confirmation; private readonly byte[] _afToken; public AppointmentPage(IAntiforgery af, HttpContext ctx, string? confirmedDate = null) { var tokens = af.GetAndStoreTokens(ctx); _afToken = $"""""".ToUtf8Bytes(); _datePicker = new Components.Calendar(id: "appt", name: "appointmentDate"); _submitBtn = new Components.Button("Book", type: "submit"); _confirmation = confirmedDate is not null ? new Components.Alert(title: "Booked!", description: $"Your appointment is on {confirmedDate}.") : HtmxEmpty.Instance; } protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken); protected override void RenderDatePicker(HtmxRenderContext ctx) => _datePicker.Render(ctx.Next()); protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submitBtn.Render(ctx.Next()); protected override void RenderConfirmation(HtmxRenderContext ctx) => _confirmation.Render(ctx.Next()); } ``` **GET + POST handlers** ```csharp [Handler] [MapGet("/appointments/new")] public static partial class GetAppointmentHandler { public record Query(); private static Task HandleAsync( Query _, HttpContext ctx, IAntiforgery af, CancellationToken ct) => ctx.WriteHtmxPage(new AppointmentPage(af, ctx), title: "Book appointment"); } [Handler] [MapPost("/appointments")] public static partial class PostAppointmentHandler { public record Command([property: FromForm] string AppointmentDate); private static Task HandleAsync( [AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct) { var date = DateOnly.ParseExact(cmd.AppointmentDate, "yyyy-MM-dd"); var page = new AppointmentPage(af, ctx, confirmedDate: date.ToLongDateString()); return ctx.WriteHtmxPage(page, title: "Book appointment"); } } ``` **`AppJsonSerializerContext.cs` — add the new command** ```csharp [JsonSerializable(typeof(PostAppointmentHandler.Command), TypeInfoPropertyName = "AppointmentCommand")] ```