ee8797c142
Co-authored-by: Copilot <copilot@github.com>
6.5 KiB
6.5 KiB
Select
A styled <select> dropdown. Supports a pre-selected value, optional label, and optional description text. HTMX attributes can be added.
HTML structure
div.flex.flex-col.gap-1.5
label[for={id}].text-sm.font-medium ← omitted when label is empty
{label}
select[id, name, class, $$HxAttrs$$]
option[value, $$Selected$$] ← one per option; selected="selected" when matched
{display}
p.text-sm.text-muted-foreground ← omitted when description is empty
{description}
CSS mechanics
| Class | Effect |
|---|---|
flex h-10 w-full rounded-md border border-input bg-background |
Full-width 40px select field |
px-3 py-2 text-sm |
Inner padding and text size |
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring |
Keyboard focus ring |
disabled:cursor-not-allowed disabled:opacity-50 |
Disabled state |
bg-background |
Ensures the dropdown matches the page background in dark mode |
Constructor signature
public Select(
string id,
IEnumerable<(string Value, string Display)> options,
string? selectedValue = null,
string name = "",
string label = "",
string description = "",
string extraClasses = "",
string hxAttrs = "")
| Parameter | Description |
|---|---|
id |
Element id and label for target |
options |
List of (Value, Display) tuples |
selectedValue |
Pre-selected option value; null = no pre-selection (first option shown) |
name |
Form field name |
label |
Optional visible label |
description |
Optional helper text below the field |
extraClasses |
Additional Tailwind classes on the <select> element |
hxAttrs |
Verbatim HTMX / data attributes |
Usage examples
Country selector
new Select(
id: "country",
name: "country",
label: "Country",
options: new[]
{
("us", "United States"),
("gb", "United Kingdom"),
("ca", "Canada"),
("au", "Australia"),
},
selectedValue: "us")
Dynamic options from data
var options = categories.Select(c => (c.Slug, c.Name));
new Select(
id: "category",
name: "category",
label: "Category",
options: options,
selectedValue: existingCategory)
HTMX on-change reload
new Select(
id: "region",
name: "region",
label: "Region",
options: regions,
hxAttrs: """hx-get="/cities" hx-target="#city-select" hx-trigger="change" hx-include="[name='region']"""")
Reading in a form handler
public record Command([property: FromForm] string Country);
// command.Country == "us" | "gb" | "ca" | "au"
Placeholder option (no pre-selection)
new Select(
id: "role",
name: "role",
label: "Role",
options: new[]
{
("", "— Select a role —"),
("admin", "Administrator"),
("user", "Regular user"),
},
selectedValue: "")
Tips and tricks
- Pass an empty-value placeholder as the first option (
("", "Select…")) to force the user to make an explicit selection. selectedValuecomparison is exact — make sure the value you pass matches one of theValuestrings inoptions.hxAttrsis verbatim — you can addmultiple,size,disabled,autocomplete, or any other native attribute here.- To conditionally disable individual options, build the raw
<select>HTML manually or subclass the component. - To conditionally disable individual options, build the raw
<select>HTML manually or subclass the component.
Complete page example
Templates/FilterProductsPage.htmx
<div class="max-w-4xl mx-auto py-10">
<h1 class="text-2xl font-bold mb-6">Products</h1>
<form class="flex gap-4 mb-8 items-end"
hx-get="/products/filter"
hx-target="#product-list"
hx-trigger="change">
$$CategorySelect$$
$$SortSelect$$
</form>
<div id="product-list">
$$ProductTable$$
</div>
</div>
Templates/FilterProductsPage.htmx.cs
namespace Htmx.ApiDemo.Templates;
public sealed class FilterProductsPage : FilterProductsPageBase
{
private readonly IHtmxComponent _category;
private readonly IHtmxComponent _sort;
private readonly IHtmxComponent _table;
public FilterProductsPage(
IEnumerable<Product> products,
string selectedCategory = "",
string selectedSort = "name-asc")
{
_category = new Components.Select(
id: "category",
name: "category",
label: "Category",
options: new[]
{
("", "All categories"),
("electronics","Electronics"),
("clothing", "Clothing"),
("books", "Books"),
},
selectedValue: selectedCategory);
_sort = new Components.Select(
id: "sort",
name: "sort",
label: "Sort by",
options: new[]
{
("name-asc", "Name A–Z"),
("name-desc", "Name Z–A"),
("price-asc", "Price: low to high"),
("price-desc", "Price: high to low"),
},
selectedValue: selectedSort);
_table = new Components.Table(
headers: new[] { "Name", "Category", "Price" },
rows: products.Select(p => new[]
{
System.Net.WebUtility.HtmlEncode(p.Name),
System.Net.WebUtility.HtmlEncode(p.Category),
$"${p.Price:F2}",
}));
}
protected override void RenderCategorySelect(HtmxRenderContext ctx) => _category.Render(ctx.Next());
protected override void RenderSortSelect(HtmxRenderContext ctx) => _sort.Render(ctx.Next());
protected override void RenderProductTable(HtmxRenderContext ctx) => _table.Render(ctx.Next());
}
GET handler (full page + HTMX partial)
[Handler]
[MapGet("/products")]
public static partial class GetProductsHandler
{
public record Query(
[property: FromQuery] string Category = "",
[property: FromQuery] string Sort = "name-asc");
private static async Task<IResult> HandleAsync(
Query q, HttpContext ctx, ProductService products, CancellationToken ct)
{
var items = await products.FilterAsync(q.Category, q.Sort, ct);
return await ctx.WriteHtmxPage(
new FilterProductsPage(items, q.Category, q.Sort), title: "Products");
}
}