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

235 lines
6.5 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.
# 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
```csharp
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
```csharp
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
```csharp
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
```csharp
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
```csharp
public record Command([property: FromForm] string Country);
// command.Country == "us" | "gb" | "ca" | "au"
```
### Placeholder option (no pre-selection)
```csharp
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.
- `selectedValue` comparison is exact — make sure the value you pass matches one of the `Value` strings in `options`.
- `hxAttrs` is verbatim — you can add `multiple`, `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`**
```html
<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`**
```csharp
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 AZ"),
("name-desc", "Name ZA"),
("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)**
```csharp
[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");
}
}
```