Files
Htmx/docs/Components/TimePicker.md
T
2026-05-04 19:57:48 +05:00

237 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (112 or 023)
span.text-muted-foreground :
select.timepicker-m[name={name}-m] ← minute select (0059)
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)`
1. Finds `.timepicker-h`, `.timepicker-m`, `.timepicker-ampm`, and the hidden `input`
2. On any `change` event across the three visible selects:
- Reads hour, minute, and AM/PM values
- Converts 12h → 24h if AM/PM select is present
- Writes `HH:MM` to the hidden input
---
## Constructor signature
```csharp
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 112 |
---
## Usage examples
### Basic time picker (24h)
```csharp
new TimePicker(name: "startTime", label: "Start time")
```
### 12-hour mode
```csharp
new TimePicker(
name: "meetingTime",
label: "Meeting time",
use12h: true)
```
### Pre-selected time
```csharp
new TimePicker(
name: "alarmTime",
selected: "07:30",
label: "Alarm",
use12h: true)
```
### Inside a form
```html
<!-- ScheduleForm.htmx -->
<form method="post" action="/schedule">
$$AntiforgeryToken$$
$$StartTime$$
$$EndTime$$
<button type="submit">Save</button>
</form>
```
```csharp
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:**
```csharp
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")]
```