f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
6.7 KiB
6.7 KiB
Select
A styled dropdown that lets the user pick one option from a list. Use it for things like country selection, category filters, or anything where the user chooses from a fixed set of values.
Quick example
new Select(
id: "country",
name: "country",
label: "Country",
options: new[]
{
("us", "United States"),
("gb", "United Kingdom"),
("ca", "Canada"),
},
selectedValue: "us")
All the options
public Select(
string id,
IEnumerable<(string Value, string Display)> options,
string? selectedValue = null,
string name = "",
string label = "",
string description = "",
string extraClasses = "",
string hxAttrs = "")
| Parameter | What it does |
|---|---|
id |
The element id. Also used by the <label for="...">. |
options |
The list of choices. Each is a (Value, Display) tuple. |
selectedValue |
The Value of the option to pre-select. Leave null to show the first option. |
name |
Form field name — required if you want the value submitted. |
label |
Visible text label above the dropdown. |
description |
Small hint text below the field. |
extraClasses |
Additional Tailwind classes on the <select> element. |
hxAttrs |
Extra HTML attributes appended verbatim — use for HTMX and data-*. |
Real-world examples
Category filter that reloads the list on change
new Select(
id: "category",
name: "category",
label: "Filter by category",
options: categories.Select(c => (c.Slug, c.Name)),
selectedValue: currentCategory,
hxAttrs: """hx-get="/products" hx-target="#product-list" hx-trigger="change"""")
Dynamic options from the database (with current value pre-selected)
var options = roles.Select(r => (r.Id.ToString(), r.Name));
new Select(
id: "role",
name: "roleId",
label: "Role",
options: options,
selectedValue: user.RoleId.ToString())
Reading on the server:
public record Command([property: FromForm] string RoleId);
Simple yes/no choice
new Select(
id: "active",
name: "isActive",
label: "Status",
options: new[]
{
("true", "Active"),
("false", "Inactive"),
},
selectedValue: user.IsActive ? "true" : "false")
How it works
Select renders a standard <select> element — no custom dropdown JavaScript. The browser's native dropdown is used, which is the most accessible and reliable approach. The selected option is matched by Value and has selected="selected" set on render.
hxAttrs: """hx-get="/cities" hx-target="#city-select" hx-trigger="change" hx-include="[name='region']"""")
### Reading in a form handler
```csharp
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");
}
}