f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
196 lines
7.0 KiB
Markdown
196 lines
7.0 KiB
Markdown
# Calendar
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new Calendar(id: "dob", name: "dateOfBirth")
|
|
```
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public Calendar(
|
|
string id,
|
|
string name = "date",
|
|
DateOnly? selected = null)
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `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. |
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### Appointment booking form
|
|
|
|
```html
|
|
<!-- Templates/BookingForm.htmx -->
|
|
<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 sealed class BookingForm : BookingFormBase
|
|
{
|
|
private readonly IHtmxComponent _datePicker;
|
|
private readonly byte[] _tokenData;
|
|
|
|
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 date on the server:**
|
|
|
|
```csharp
|
|
public record Command(
|
|
[property: FromForm] string BookingDate // arrives as "yyyy-MM-dd"
|
|
);
|
|
|
|
var date = DateOnly.ParseExact(command.BookingDate, "yyyy-MM-dd");
|
|
```
|
|
|
|
### 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-booking').addEventListener('calendarChange', e => {
|
|
console.log(e.detail.date); // "2026-09-15"
|
|
// update price estimates, availability, etc.
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
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.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/AppointmentPage.htmx`**
|
|
```html
|
|
<div class="max-w-sm mx-auto py-10">
|
|
<h1 class="text-2xl font-bold mb-6">Book an appointment</h1>
|
|
<form method="post" action="/appointments">
|
|
$$AntiforgeryToken$$
|
|
<div class="mb-4">
|
|
<label class="text-sm font-medium block mb-2">Select a date</label>
|
|
$$DatePicker$$
|
|
</div>
|
|
$$SubmitBtn$$
|
|
</form>
|
|
$$Confirmation$$
|
|
</div>
|
|
```
|
|
|
|
**`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 = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".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<IResult> 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<IResult> 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")]
|
|
```
|