Documentations added

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-04 19:57:48 +05:00
parent 40a7d9018c
commit ee8797c142
35 changed files with 6655 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
# 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 ← 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
```
---
## 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 `<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
```csharp
public Calendar(
string id,
string name = "date",
DateOnly? selected = null)
```
| Parameter | Description |
|---|---|
| `id` | Logical id; element gets `id="cal-{id}"` |
| `name` | Form field name for the hidden input |
| `selected` | Pre-selected date; defaults to today |
---
## Usage 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
```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>
```
```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
<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")]
```