b530bb8c97
Co-authored-by: Copilot <copilot@github.com>
201 lines
6.9 KiB
Markdown
201 lines
6.9 KiB
Markdown
# TimePicker
|
|
|
|
A styled time selector with separate dropdowns for hours and minutes (and optionally AM/PM). The selected time is always stored in a hidden input as `HH:MM` in 24-hour format, regardless of whether you show the 12-hour display mode.
|
|
|
|
---
|
|
|
|
## Quick example
|
|
|
|
```csharp
|
|
new TimePicker(name: "startTime", label: "Start time")
|
|
```
|
|
|
|
---
|
|
|
|
## All the options
|
|
|
|
```csharp
|
|
public TimePicker(
|
|
string name,
|
|
string? selected = null,
|
|
string label = "",
|
|
string description = "",
|
|
bool use12h = false)
|
|
```
|
|
|
|
| Parameter | What it does |
|
|
|---|---|
|
|
| `name` | Form field name. The hidden input gets this name and always holds a `HH:MM` value. The visible selects get `{name}-h`, `{name}-m`, `{name}-ampm`. |
|
|
| `selected` | Pre-selected time as `"HH:MM"` in 24-hour format. Defaults to the current time. |
|
|
| `label` | Visible text label above the picker. |
|
|
| `description` | Small hint text below the picker. |
|
|
| `use12h` | Show 12-hour mode with an AM/PM dropdown. The hidden input still stores 24h format. |
|
|
|
|
---
|
|
|
|
## Real-world examples
|
|
|
|
### Appointment booking with start and end times
|
|
|
|
```html
|
|
<!-- ScheduleForm.htmx -->
|
|
<form method="post" action="/schedule" class="space-y-4">
|
|
$$Token$$
|
|
$$StartTime$$
|
|
$$EndTime$$
|
|
<button type="submit">Save</button>
|
|
</form>
|
|
```
|
|
|
|
```csharp
|
|
// ScheduleForm.htmx.cs
|
|
_startTime = new TimePicker(name: "startTime", label: "Start", selected: "09:00");
|
|
_endTime = new TimePicker(name: "endTime", label: "End", selected: "17:00");
|
|
```
|
|
|
|
Reading on the server:
|
|
|
|
```csharp
|
|
public record Command(
|
|
[property: FromForm] string StartTime, // "HH:MM"
|
|
[property: FromForm] string EndTime
|
|
);
|
|
|
|
var start = TimeOnly.ParseExact(command.StartTime, "HH:mm");
|
|
var end = TimeOnly.ParseExact(command.EndTime, "HH:mm");
|
|
```
|
|
|
|
### 12-hour display mode with a pre-selected time
|
|
|
|
```csharp
|
|
new TimePicker(
|
|
name: "alarmTime",
|
|
selected: "07:30",
|
|
label: "Alarm time",
|
|
use12h: true)
|
|
```
|
|
|
|
The user sees `7:30 AM` in the dropdowns, but `07:30` is what gets submitted.
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
TimePicker renders three `<select>` elements (hours, minutes, and optionally AM/PM) styled to look like a single field, plus a hidden `<input>` that holds the combined value. JavaScript in `components.js` listens for changes on any of the three selects and writes the correctly formatted `HH:MM` value to the hidden input, converting from 12h to 24h when needed.
|
|
public record Command(
|
|
[property: FromForm] string StartTime, // "09:00"
|
|
[property: FromForm] string EndTime // "17:00"
|
|
);
|
|
|
|
var start = TimeOnly.ParseExact(command.StartTime, "HH:mm");
|
|
var end = TimeOnly.ParseExact(command.EndTime, "HH:mm");
|
|
```
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- The hidden input always stores 24h `HH:MM` regardless of `use12h` — parse with `"HH:mm"` format on the server.
|
|
- `selected` defaults to the current server time if not specified — pass `"00:00"` if you want the picker to start at midnight.
|
|
- The visible hour/minute selects are independent form fields (`{name}-h`, `{name}-m`) — only the hidden input with `name` is needed in your command record. Ignore the `-h`, `-m`, and `-ampm` fields server-side.
|
|
- For a date+time combination, pair `Calendar` (for date) with `TimePicker` (for time) and combine their values in the handler.
|
|
- For a date+time combination, pair `Calendar` (for date) with `TimePicker` (for time) and combine their values in the handler.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
**`Templates/ScheduleMeetingPage.htmx`**
|
|
```html
|
|
<div class="max-w-lg mx-auto py-10">
|
|
<h1 class="text-2xl font-bold mb-2">Schedule a meeting</h1>
|
|
<p class="text-sm text-muted-foreground mb-8">Pick a date and time that works for you.</p>
|
|
<form method="post" action="/meetings/new">
|
|
$$AntiforgeryToken$$
|
|
<div class="grid grid-cols-2 gap-6 mb-8">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Date</label>
|
|
$$DatePicker$$
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-2">Time</label>
|
|
$$TimePicker$$
|
|
</div>
|
|
</div>
|
|
$$TitleInput$$
|
|
<div class="mt-6">
|
|
$$SubmitBtn$$
|
|
</div>
|
|
</form>
|
|
$$SuccessAlert$$
|
|
</div>
|
|
```
|
|
|
|
**`Templates/ScheduleMeetingPage.htmx.cs`**
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class ScheduleMeetingPage : ScheduleMeetingPageBase
|
|
{
|
|
private readonly IHtmxComponent _calendar;
|
|
private readonly IHtmxComponent _timePicker;
|
|
private readonly IHtmxComponent _title;
|
|
private readonly IHtmxComponent _submit;
|
|
private readonly IHtmxComponent _success;
|
|
private readonly byte[] _afToken;
|
|
|
|
public ScheduleMeetingPage(
|
|
IAntiforgery af,
|
|
HttpContext ctx,
|
|
DateOnly? selectedDate = null,
|
|
string selectedTime = "",
|
|
bool booked = false)
|
|
{
|
|
var tokens = af.GetAndStoreTokens(ctx);
|
|
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
|
|
|
|
_calendar = new Components.Calendar(name: "date", selectedDate: selectedDate);
|
|
_timePicker = new Components.TimePicker(name: "time", value: selectedTime, placeholder: "09:00");
|
|
_title = new Components.Input(id: "title", name: "title", label: "Meeting title", placeholder: "Sync call");
|
|
_submit = new Components.Button("Book meeting", type: "submit");
|
|
_success = booked
|
|
? new Components.Alert(title: "Meeting booked!", description: $"Scheduled for {selectedDate:d} at {selectedTime}.")
|
|
: HtmxEmpty.Instance;
|
|
}
|
|
|
|
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
|
|
protected override void RenderDatePicker(HtmxRenderContext ctx) => _calendar.Render(ctx.Next());
|
|
protected override void RenderTimePicker(HtmxRenderContext ctx) => _timePicker.Render(ctx.Next());
|
|
protected override void RenderTitleInput(HtmxRenderContext ctx) => _title.Render(ctx.Next());
|
|
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submit.Render(ctx.Next());
|
|
protected override void RenderSuccessAlert(HtmxRenderContext ctx) => _success.Render(ctx.Next());
|
|
}
|
|
```
|
|
|
|
**POST handler**
|
|
```csharp
|
|
[Handler]
|
|
[MapPost("/meetings/new")]
|
|
public static partial class PostScheduleMeetingHandler
|
|
{
|
|
public record Command(
|
|
[property: FromForm] DateOnly Date,
|
|
[property: FromForm] string Time,
|
|
[property: FromForm] string Title);
|
|
|
|
private static Task<IResult> HandleAsync(
|
|
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
|
|
{
|
|
// Persist meeting…
|
|
return ctx.WriteHtmxPage(
|
|
new ScheduleMeetingPage(af, ctx, cmd.Date, cmd.Time, booked: true),
|
|
title: "Schedule meeting");
|
|
}
|
|
}
|
|
```
|
|
|
|
**`AppJsonSerializerContext.cs`**
|
|
```csharp
|
|
[JsonSerializable(typeof(PostScheduleMeetingHandler.Command), TypeInfoPropertyName = "ScheduleMeetingCommand")]
|
|
```
|