ee8797c142
Co-authored-by: Copilot <copilot@github.com>
235 lines
6.5 KiB
Markdown
235 lines
6.5 KiB
Markdown
# 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 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");
|
||
}
|
||
}
|
||
```
|