f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
239 lines
6.7 KiB
Markdown
239 lines
6.7 KiB
Markdown
# 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
|
||
|
||
```csharp
|
||
new Select(
|
||
id: "country",
|
||
name: "country",
|
||
label: "Country",
|
||
options: new[]
|
||
{
|
||
("us", "United States"),
|
||
("gb", "United Kingdom"),
|
||
("ca", "Canada"),
|
||
},
|
||
selectedValue: "us")
|
||
```
|
||
|
||
---
|
||
|
||
## All the options
|
||
|
||
```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 | 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
|
||
|
||
```csharp
|
||
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)
|
||
|
||
```csharp
|
||
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:
|
||
|
||
```csharp
|
||
public record Command([property: FromForm] string RoleId);
|
||
```
|
||
|
||
### Simple yes/no choice
|
||
|
||
```csharp
|
||
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)
|
||
|
||
```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 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)**
|
||
```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");
|
||
}
|
||
}
|
||
```
|