ee8797c142
Co-authored-by: Copilot <copilot@github.com>
7.6 KiB
7.6 KiB
TimePicker
A styled time picker. The user selects hours, minutes, and optionally AM/PM. The component always writes the selected time as HH:MM (24-hour) to the hidden input, regardless of whether 12-hour display mode is used. Optionally renders a visible label and description.
HTML structure
div.flex.flex-col.gap-1.5
label.text-sm.font-medium ← omitted when empty
{label}
div.flex.items-center.gap-1.rounded-md.border.border-input.bg-background.px-3.py-2
select.timepicker-h[name={name}-h] ← hour select (1–12 or 0–23)
span.text-muted-foreground :
select.timepicker-m[name={name}-m] ← minute select (00–59)
select.timepicker-ampm[name={name}-ampm] ← AM/PM (12h mode only)
input.sr-only[type=hidden, name={name}] ← hidden input holding HH:MM
p.text-sm.text-muted-foreground ← omitted when empty
{description}
CSS mechanics
| Class | Effect |
|---|---|
rounded-md border border-input bg-background |
Consistent styling with other form fields |
sr-only on hidden input |
Hidden visually but included in form submission |
appearance-none on <select> elements |
Removes native browser dropdown arrow for uniform style |
focus:outline-none on selects |
Focus ring deferred to the wrapper div |
JavaScript (syncTime in components.js)
Runs on DOMContentLoaded and htmx:afterSwap.
syncTime(wrapper)
- Finds
.timepicker-h,.timepicker-m,.timepicker-ampm, and the hiddeninput - On any
changeevent across the three visible selects:- Reads hour, minute, and AM/PM values
- Converts 12h → 24h if AM/PM select is present
- Writes
HH:MMto the hidden input
Constructor signature
public TimePicker(
string name,
string? selected = null,
string label = "",
string description = "",
bool use12h = false)
| Parameter | Description |
|---|---|
name |
Form field name; hidden input gets this name, visible selects get {name}-h, {name}-m, {name}-ampm |
selected |
Pre-selected time as "HH:MM" (24h format); defaults to current time |
label |
Optional visible label |
description |
Optional helper text |
use12h |
If true, shows AM/PM select and hour range 1–12 |
Usage examples
Basic time picker (24h)
new TimePicker(name: "startTime", label: "Start time")
12-hour mode
new TimePicker(
name: "meetingTime",
label: "Meeting time",
use12h: true)
Pre-selected time
new TimePicker(
name: "alarmTime",
selected: "07:30",
label: "Alarm",
use12h: true)
Inside a form
<!-- ScheduleForm.htmx -->
<form method="post" action="/schedule">
$$AntiforgeryToken$$
$$StartTime$$
$$EndTime$$
<button type="submit">Save</button>
</form>
public ScheduleForm()
{
StartTime = new TimePicker(name: "startTime", label: "Start", selected: "09:00");
EndTime = new TimePicker(name: "endTime", label: "End", selected: "17:00");
}
Reading the submitted values:
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:MMregardless ofuse12h— parse with"HH:mm"format on the server. selecteddefaults 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 withnameis needed in your command record. Ignore the-h,-m, and-ampmfields server-side. - For a date+time combination, pair
Calendar(for date) withTimePicker(for time) and combine their values in the handler. - For a date+time combination, pair
Calendar(for date) withTimePicker(for time) and combine their values in the handler.
Complete page example
Templates/ScheduleMeetingPage.htmx
<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
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
[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
[JsonSerializable(typeof(PostScheduleMeetingHandler.Command), TypeInfoPropertyName = "ScheduleMeetingCommand")]