Components
Select
One option from a list, or several. The browser's own select, a listbox the kit draws for options that carry more than words, or a combobox that filters as it is typed into; the options are its children, typed by the select's value.
Drawn and behaving as Flux UI's select does, measured against its live docs.
Select
The browser's own select, in the input's box. For a list of up to five, radios or checkboxes may serve better.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectExample : Component
{
private string? _industry;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Placeholder("Choose industry...").Id("ui-select-native")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text.Data("testid", "ui-select-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
];
}Nothing chosen.
Small
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectSmallExample : Component
{
private string? _industry;
protected override Component? Render() =>
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Sm.Placeholder("Choose industry...").Class("max-w-sm")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}Option groups
Related options under a label, to make a long list easier to scan.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectGroupsExample : Component
{
private string? _industry;
protected override Component? Render() =>
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Placeholder("Choose an industry...").Class("max-w-sm")[
Ui.SelectGroup.Label("Creative")[
Ui.SelectOption.Value("photography")["Photography"],
Ui.SelectOption.Value("design")["Design services"]
],
Ui.SelectGroup.Label("Technology")[
Ui.SelectOption.Value("web-development")["Web development"],
Ui.SelectOption.Value("it-consulting")["IT consulting"]
],
Ui.SelectGroup.Label("Services")[
Ui.SelectOption.Value("accounting")["Accounting"],
Ui.SelectOption.Value("legal")["Legal services"],
Ui.SelectOption.Value("consulting")["Consulting"]
]
];
}Custom select
Listbox draws the list instead of handing it to the browser, for options with icons, pictures and more.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectListboxExample : Component
{
private string? _industry;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Placeholder("Choose industry...").Id("ui-select-listbox")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text.Data("testid", "ui-listbox-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
];
}Nothing chosen.
The button slot
Ui.SelectButton as a child sets what the select's own steps do not reach.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectButtonSlotExample : Component
{
private string? _industry;
protected override Component? Render() =>
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Class("max-w-sm")[
Ui.SelectButton.Class("rounded-full!").Placeholder("Choose industry..."),
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}Clearable
A button that puts the select back to nothing chosen, shown while there is an answer.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectClearableExample : Component
{
private string? Industry { get; set; } = "Photography";
protected override Component? Render() =>
Ui.Select.Bind(() => Industry).Listbox.Clearable().Placeholder("Choose industry...").Class("max-w-sm")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}With a prefix
Words that stay in the button before the chosen option.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectPrefixExample : Component
{
private string Comparison { get; set; } = "last-month";
protected override Component? Render() =>
Ui.Select.Bind(() => Comparison).Listbox.Prefix("Compare to").Class("max-w-sm")[
Ui.SelectOption.Value("previous-period")["Previous period"],
Ui.SelectOption.Value("last-month")["Last month"],
Ui.SelectOption.Value("last-quarter")["Last quarter"],
Ui.SelectOption.Value("last-year")["Last year"]
];
}With icons
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectIconsExample : Component
{
private string? _method;
protected override Component? Render() =>
Ui.Select.Value(_method).OnChange(value => { _method = value; }).Listbox.Placeholder("Choose method...").Class("max-w-sm")[
Ui.SelectOption.Value("card").Label("Credit card").Icon(Ui.IconName.CreditCard),
Ui.SelectOption.Value("paypal").Label("PayPal").Icon(Ui.IconName.Banknotes),
Ui.SelectOption.Value("bank").Label("Bank transfer").Icon(Ui.IconName.BuildingLibrary)
];
}With descriptions
A description is the list's only: the button does not repeat it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectDescriptionsExample : Component
{
private string? _plan;
protected override Component? Render() =>
Ui.Select.Value(_plan).OnChange(value => { _plan = value; }).Listbox.Placeholder("Choose plan...").Class("max-w-sm")[
Ui.SelectOption.Value("basic").Label("Basic").Description("For individuals getting started"),
Ui.SelectOption.Value("pro").Label("Pro").Description("For small teams that need more power"),
Ui.SelectOption.Value("enterprise").Label("Enterprise").Description("Advanced controls and support")
];
}With avatars
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectAvatarsExample : Component
{
private string? _person;
protected override Component? Render() =>
Ui.Select.Value(_person).OnChange(value => { _person = value; }).Listbox.Placeholder("Search people...").Class("max-w-sm")[
Ui.SelectOption.Value("maralind").Label("Mara Lind").Avatar("/img/avatars/amber.svg"),
Ui.SelectOption.Value("theobrandt").Label("Theo Brandt").Avatar("/img/avatars/sky.svg"),
Ui.SelectOption.Value("rosavidal").Label("Rosa Vidal").Avatar("/img/avatars/rose.svg")
];
}With custom content
An option's children are drawn as they are.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectCustomContentExample : Component
{
private string? _color;
protected override Component? Render() =>
Ui.Select.Value(_color).OnChange(value => { _color = value; }).Listbox.Placeholder("Select color...").Class("max-w-sm")[
Ui.SelectOption.Value("red")[
Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-red-500"), "Red"]
],
Ui.SelectOption.Value("orange")[
Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-orange-500"), "Orange"]
],
Ui.SelectOption.Value("amber")[
Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-amber-500"), "Amber"]
],
Ui.SelectOption.Value("yellow")[
Div.Class("flex items-center gap-2")[Div.Class("size-4 rounded-full bg-yellow-500"), "Yellow"]
]
];
}Customizing the dropdown width
The list is as wide as its button; OptionsClass widens it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectWidthExample : Component
{
private string? _visibility;
protected override Component? Render() =>
Ui.Select.Value(_visibility).OnChange(value => { _visibility = value; }).Listbox.Placeholder("Select visibility...").Class("max-w-32").OptionsClass("min-w-72")[
Ui.SelectOption.Value("public").Label("Public").Icon(Ui.IconName.GlobeAlt)
.Description("Shown on your calendar and eligible to be featured on our homepage."),
Ui.SelectOption.Value("private").Label("Private").Icon(Ui.IconName.LockClosed)
.Description("Unlisted. Only invited people and people with the link can register.")
];
}Searchable select
A search field over the options, for a long list.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectSearchableExample : Component
{
private string? _industry;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Searchable().Placeholder("Choose industries...").Id("ui-select-searchable")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text.Data("testid", "ui-select-search-state")[_industry is null ? "Nothing chosen." : $"Chosen: {_industry}."]
];
}Nothing chosen.
The search slot
Ui.SelectSearch as a child sets the search field's own steps.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectSearchSlotExample : Component
{
private string? _industry;
protected override Component? Render() =>
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Listbox.Searchable().Placeholder("Choose industry...").Class("max-w-sm")[
Ui.SelectSearch.Class("px-4").Placeholder("Search industries..."),
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}Search keywords
Search for "apple", "orange" or "pear": Fruit is found by words it never shows.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectKeywordsExample : Component
{
private string? _category;
protected override Component? Render() =>
Ui.Select.Value(_category).OnChange(value => { _category = value; }).Listbox.Searchable().Placeholder("Choose category...").Class("max-w-sm")[
Ui.SelectOption.Value("fruit").Keywords("apple orange pear")["Fruit"],
Ui.SelectOption.Value("vegetables").Keywords("carrot broccoli spinach")["Vegetables"],
Ui.SelectOption.Value("drinks").Keywords("coffee tea juice")["Drinks"]
];
}Multiple select
Opened on a collection, the listbox picks several and stays open while you do.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectMultipleExample : Component
{
private List<string> Industries { get; set; } = [];
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Bind(() => Industries).Live().Listbox.Multiple().Placeholder("Choose industries...").Id("ui-select-multiple")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text.Data("testid", "ui-multiselect-state")[
Industries.Count == 0 ? "Nothing chosen." : $"Chosen: {string.Join(", ", Industries)}."
]
];
}Nothing chosen.
Selected suffix
What follows the count once several are picked: "2 industries selected".
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectSelectedSuffixExample : Component
{
private List<string> Industries { get; set; } = ["Photography", "Accounting"];
protected override Component? Render() =>
Ui.Select.Bind(() => Industries).Listbox.SelectedSuffix("industries selected").Multiple().Class("max-w-sm")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}Clearing search
Clear(Ui.SelectClear.Close) keeps the search through each pick and empties it when the list closes.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectClearSearchExample : Component
{
private List<string> Industries { get; set; } = [];
protected override Component? Render() =>
Ui.Select.Bind(() => Industries).Listbox.Searchable().Multiple().Clear(Ui.SelectClear.Close)
.Placeholder("Choose industries...").Class("max-w-sm")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
];
}Combobox
A text input that filters the list under it as you type.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectComboboxExample : Component
{
private string? _industry;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Combobox.Placeholder("Choose industry...").Id("ui-select-combobox")[
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text.Data("testid", "ui-combobox-state")[$"Industry: {_industry ?? "none"}."]
];
}Industry: none.
The input slot
Ui.SelectInput as a child sets the input's own steps, and can hand its text to the page.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectInputSlotExample : Component
{
private string _search = "";
private string? _industry;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_industry).OnChange(value => { _industry = value; }).Combobox[
Ui.SelectInput.Value(_search).OnInput(text => { _search = text; }).Placeholder("Choose industry..."),
Ui.SelectOption["Photography"],
Ui.SelectOption["Design services"],
Ui.SelectOption["Web development"],
Ui.SelectOption["Accounting"],
Ui.SelectOption["Legal services"],
Ui.SelectOption["Consulting"],
Ui.SelectOption["Other"]
],
Ui.Text[$"The page holds the search: \"{_search}\"."]
];
}The page holds the search: "".
Backend search
Filter(false) leaves the rows to the page, and Debounce asks it once typing has paused: one query for a word, not one for a letter.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectBackendSearchExample : Component
{
// What a store would hold. The page answers each search with the rows that match.
private static readonly (int Id, string Name)[] Projects =
[(1, "Branding"), (2, "Analytics"), (3, "Infrastructure"), (4, "Documentation"), (5, "Security"), (6, "Performance")];
private string _search = "";
private int _searches;
private int? _projectId;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_projectId).OnChange(value => { _projectId = value; }).Combobox.Filter(false).Id("ui-select-backend")[
Ui.SelectInput.Placeholder("Start typing...").OnInput(Search).Debounce(250.Milliseconds),
Projects
.Where(project => project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name])
],
Ui.Text.Data("testid", "ui-select-backend-state")[$"Project: {NameOf(_projectId)}. Searches: {_searches}."]
];
private void Search(string text)
{
_searches++;
_search = text;
}
private static string NameOf(int? id) => id is { } found ? Array.Find(Projects, project => project.Id == found).Name : "none";
}Project: none. Searches: 0.
Backend search in a listbox
The same in a searchable listbox, through Ui.SelectSearch. The rows drawn stay until the answer is here, and a row picked while the pause runs is the row you pressed.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectListboxSearchExample : Component
{
// What a store would hold: more rows than a list shows, so the page answers a search with the first few.
private static readonly string[] Cities =
[
"Amsterdam", "Athens", "Berlin", "Bern", "Brussels", "Budapest", "Copenhagen", "Dublin", "Helsinki", "Lisbon",
"Ljubljana", "Madrid", "Oslo", "Paris", "Prague", "Riga", "Rome", "Stockholm", "Vienna", "Warsaw",
];
private string[] _cities = [.. Cities.Take(6)];
private int _queries;
private string? _city;
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Value(_city).OnChange(value => { _city = value; }).Listbox.Searchable().Filter(false).Placeholder("Choose a city...").Id("ui-select-store")[
Ui.SelectSearch.Placeholder("Search cities...").OnInput(Search).Debounce(250.Milliseconds),
_cities.Select(name => Ui.SelectOption.Key(name)[name])
],
Ui.Text.Data("testid", "ui-select-store-state")[$"City: {_city ?? "none"}. Queries: {_queries}."]
];
// The page's answer to the search: a query, here over a list.
private void Search(string text)
{
_queries++;
_cities = [.. Cities.Where(name => name.Contains(text, StringComparison.CurrentCultureIgnoreCase)).Take(6)];
}
}City: none. Queries: 0.
Create option
Type a name no project has: the create row offers to make it, from two characters on.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectCreateExample : Component
{
private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
private int? ProjectId { get; set; }
protected override Component? Render() =>
Div.Class("max-w-sm space-y-3")[
Ui.Select.Bind(() => ProjectId).Live().Combobox.Id("ui-select-create")[
Ui.SelectInput.Placeholder("Start typing..."),
_projects.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
],
Ui.Text.Data("testid", "ui-select-create-state")[
$"Project: {(ProjectId is { } id ? _projects.Find(project => project.Id == id).Name : "none")}."
]
];
// The page's answer to the create row: add the project, and pick it.
private void CreateProject(string name)
{
var id = _projects.Max(project => project.Id) + 1;
_projects.Add((id, name));
ProjectId = id;
}
}Project: none.
With backend search
When the page filters, keep the chosen project among the rows, so the select can go on naming it.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectCreateSearchExample : Component
{
private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
private string _search = "";
private int? ProjectId { get; set; }
protected override Component? Render() =>
Ui.Select.Bind(() => ProjectId).Combobox.Filter(false).Class("max-w-sm")[
Ui.SelectInput.Placeholder("Start typing...").OnInput(text => { _search = text.Trim(); }),
Matching().Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
];
// The rows that match the search, and the one picked even when it does not: the select goes on naming it.
private IEnumerable<(int Id, string Name)> Matching() =>
_projects.Where(project => project.Id == ProjectId || project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase));
private void CreateProject(string name)
{
var id = _projects.Max(project => project.Id) + 1;
_projects.Add((id, name.Trim()));
ProjectId = id;
}
}Loading message
What the list says while a slow search is on its way, and when it finds nothing.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectLoadingExample : Component
{
private static readonly (int Id, string Name)[] Projects =
[(1, "Branding"), (2, "Analytics"), (3, "Infrastructure"), (4, "Documentation")];
private string _search = "";
private int? _projectId;
protected override Component? Render() =>
Ui.Select.Value(_projectId).OnChange(value => { _projectId = value; }).Combobox.Filter(false).Class("max-w-sm")[
Ui.SelectInput.Placeholder("Start typing...").OnInput(Search).Debounce(250.Milliseconds),
Projects
.Where(project => project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
Ui.SelectOptionEmpty.WhenLoading("Loading projects...")["No projects found."]
];
// A slow store: long enough to read the loading message.
private async Task Search(string text)
{
await Task.Delay(600.Milliseconds);
_search = text;
}
}With validation
The page refuses a name longer than forty characters: the input is marked invalid until the next search.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectValidationExample : Component
{
private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
private string _search = "";
private string? _error;
private int? ProjectId { get; set; }
protected override Component? Render() =>
Ui.Field.Class("max-w-sm")[
Ui.Label["Project"],
Ui.Select.Bind(() => ProjectId).Combobox.Filter(false).ShowValidation(false)[
Ui.SelectInput.Placeholder("Start typing...").Invalid(_error is not null).OnInput(Search),
_projects
.Where(project => project.Id == ProjectId || project.Name.Contains(_search, StringComparison.CurrentCultureIgnoreCase))
.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
Ui.SelectOptionCreate.MinLength(2).OnClick(CreateProject)["Create new project"]
],
Ui.Error.Message(_error)
];
// A new search clears the last refusal, as Flux's example resets its error bag.
private void Search(string text)
{
_search = text.Trim();
_error = null;
}
private void CreateProject(string name)
{
name = name.Trim();
if (name.Length > 40)
{
_error = "A project's name is 40 characters at most.";
return;
}
var id = _projects.Max(project => project.Id) + 1;
_projects.Add((id, name));
ProjectId = id;
}
}With modal
A create row in a listbox opens a form of the page's own in a modal.
namespace Rask.Site.Features.UiDocs.Examples;
public sealed partial class SelectModalExample : Component
{
private readonly List<(int Id, string Name)> _projects = [(1, "Branding"), (2, "Analytics"), (3, "Infrastructure")];
private bool _creating;
private int? ProjectId { get; set; }
private string ProjectName { get; set; } = "";
protected override Component? Render() =>
Div.Class("max-w-sm")[
Ui.Select.Bind(() => ProjectId).Listbox.Placeholder("Choose project...")[
_projects.Select(project => Ui.SelectOption.Key(project.Id).Value(project.Id)[project.Name]),
Ui.SelectOptionCreate.OnClick(_ => { _creating = true; })["Create new"]
],
Ui.Modal.Open(_creating).OnClose(() => { _creating = false; }).Class("md:w-96")[
Div.Class("space-y-6")[
Div[
Ui.Heading.Level(2).Lg["Create new project"],
Ui.Text.Class("mt-2")["Enter the name of the new project."]
],
Ui.Input.Bind(() => ProjectName).Label("Name").Placeholder("e.g. 'UX Research'"),
Div.Class("flex")[
Ui.Spacer,
Ui.Button.Primary.OnClick(CreateProject)["Create"]
]
]
]
];
private void CreateProject()
{
if (string.IsNullOrWhiteSpace(ProjectName))
{
return;
}
var id = _projects.Max(project => project.Id) + 1;
_projects.Add((id, ProjectName.Trim()));
ProjectId = id;
ProjectName = "";
_creating = false;
}
}Behaviour
Options are children
Bind or Value stands where Flux has wire:model, and the options are the select's children, typed by its value: Ui.Select.Bind(() => m.Country)[countries.Select(c => Ui.SelectOption.Key(c.Code).Value(c.Code)[c.Name])]. An option with no Value stands for its own text. Options made from a list take a Key.
The native variant is the browser's <select>: it needs no runtime, renders complete on a prerendered page and gets a phone's own picker, and its options hold text and nothing else. A listbox's button keeps the chosen option's words, icon and avatar for as long as the value is that one, whatever rows are drawn, and SelectedLabel is what it shows when that should not be the option's own words. An Avatar wins over an Icon. Position and Align say which side the list opens on and which edge it lines up with.
Searching in the browser
Searchable() puts a search field at the top of the listbox's options. Opening focuses it; typing filters case- and accent-insensitively over each option's text and its Keywords, and puts the cursor on the first match. "No results found" shows when nothing matches, and Empty("…") says it in your own words. The filtering is the browser's, as Flux's is: a key typed sends the page nothing.
In a combobox a click or typing opens the list, Enter picks and the input shows the answer. Escape closes the list and a second Escape empties the text but keeps the answer; leaving the input with words that name nothing puts the answer's text back.
Searching on the server
Filter(false) hands the search to the page: the OnInput of Ui.SelectSearch or Ui.SelectInput hears what is typed, at every key or at the pause Debounce(250.Milliseconds) names, and the page renders the rows that match. The browser counts the pause, so nothing is sent while the reader types. The rows drawn stay until the new ones are here, and a row pressed (or Enter) during the pause picks from the list as it is drawn; the text typed since is dropped with it.
The page's search follows the field. When the select rewrites the field itself (the list shuts and the field is emptied, or a combobox's input says the answer), OnInput hears that text too, once, and only when the page thinks otherwise: after a search for "ber" and a pick it is called with "", so the page goes back to the rows it starts from.
Ui.SelectOptionCreate offers to make an option of what was typed once the search is MinLength long and names none that exists; its OnClick is handed the text, and the page adds the option and chooses it. In a listbox with no search it is always shown and handed an empty string: the case where the page opens a form of its own. Ui.SelectOptionEmpty says what the list says when there are no rows, and WhenLoading what it says while results are on their way.
Several answers
Open the select on a collection (Bind over a List<T>, a T[] or a HashSet<T>, or Values(tags) with OnChange) and .Listbox.Multiple() picks several. A pick switches its row and leaves the list open; the button names the one option picked, or counts them ("3 selected", or your SelectedSuffix). A bound list, array or set each gets its own kind back. Several answers are the listbox's, as on Flux: Multiple() over one value, or a collection on the native variant, throws and says so.
The keyboard
The keys are the runtime's (data-rask-list), recorded on fluxui.dev and matched, and an open list sends the page nothing until a row is picked.
Key | What happens |
|---|---|
| Click, Space, ArrowDown, ArrowUp | Open, with the cursor on the chosen option, or the first when there is none. |
| ArrowDown, ArrowUp | Move the cursor past a disabled option. They stop at either end; there is no wrap. |
| Home, End, PageUp, PageDown | Nothing, as on Flux. |
| Enter | Picks the lit row and closes; with Multiple() it switches the row and the list stays open. On the closed button it does nothing. |
| Escape, a click elsewhere | Close; the focus returns to the button. |
| Tab | Closes, picks nothing, and moves on. |
| A letter | On the closed button it picks the next option starting with it; on the open list it moves the cursor. Letters within half a second are one prefix. |
| The pointer | Lights the row it enters; a row it lit goes dark when it leaves the list, and one the keys lit stays. |
No script of the kit's
The drawn list is a native popover placed by CSS anchor positioning: as wide as its trigger, 5px under it, at most 20rem tall, and kept inside the viewport. The browser opens it on a click and closes it on Escape or a click elsewhere. While it is open the page behind it neither scrolls nor takes the pointer (data-rask-lock), and the closed button ignores Enter and its arrows do not scroll the page (data-rask-listbox-button), as Flux's script does.
The trigger carries role="combobox", aria-haspopup="listbox", aria-expanded, aria-controls and, while open, aria-activedescendant; the list is role="listbox" and each row role="option" with aria-selected. A drawn select posts nothing by itself, so one with a Name carries its answer in hidden inputs. A press on a row names its own row, and a bound select then writes its member with the next action, or at once with .Live().
Reference
Ui.Select
One option from a list, or several when it is opened on a collection. Its options are its children.
Step | Takes | What it does |
|---|---|---|
.Bind(…) | () => a T | The model's member it reads and writes: () => m.Country, or a list for several. Or Value with OnChange. |
.Value(…) | T, the options' type | The chosen option's value, for a page that holds it itself. Values(list) for several. |
.OnChange(…) | a handler given the new value | Runs with the new value when an option is picked. |
.AfterBind(…) | a handler given the new value | Runs after Bind has written the model. |
.Validate(…) | a validator | A rule the value is checked by when it is written. |
.Live() | bool | Writes the model as soon as an option is picked, not with the next action. |
.Variant(…) | Ui.SelectVariantDefault Listbox Combobox Each is a step of its own too, unless an HTML tag has its name. | The browser's own select, a drawn listbox, or a combobox. The browser's when unset. |
.Size(…) | Ui.SelectSizeBase Sm Xs Each is a step of its own too, unless an HTML tag has its name. | How tall it is. 40px when unset. |
.Placeholder(…) | string | Shown while nothing is picked. |
.Label(…) | string | Draws a field around it, with this label and the error. |
.Description(…) | string | Help text between the label and the select. |
.DescriptionTrailing(…) | string | Help text under the select. |
.Badge(…) | string | A word beside the label: "Optional". |
.Multiple() | bool | Picks several, over a collection. The listbox's. |
.Searchable() | bool | A search field over the listbox's options. |
.Filter() | bool | False leaves every option in the list whatever is typed: the page filters, from the search's OnInput. |
.Empty(…) | string | What a search that matches nothing says. "No results found" when unset. |
.Clearable() | bool | A button that clears the answer, shown while there is one. |
.Prefix(…) | string | Words that stay in the button before the chosen option: "Compare to". |
.SelectedSuffix(…) | string | What follows the count once several are picked. "selected" when unset. |
.Clear(…) | Ui.SelectClearSelect Close Each is a step of its own too, unless an HTML tag has its name. | When a search empties itself: at every pick, or when the list closes. |
.Position(…) | Ui.PositionDefault Top Right Bottom Left Each is a step of its own too, unless an HTML tag has its name. | Which side of the select the list opens on. Below when unset. |
.Align(…) | Ui.AlignDefault Start Center End Each is a step of its own too, unless an HTML tag has its name. | Which edge of the select the list lines up with. Its start when unset. |
.OptionsClass(…) | string | Classes for the open list: a width, such as min-w-72. |
.Disabled() | bool | A select that cannot be opened or changed. |
.Invalid() | bool | Error styling the form did not ask for. |
.Name(…) | string | What the answer is posted under in a form. |
.ShowValidation() | bool | False leaves the member's message to a Ui.Error placed elsewhere. |
Ui.SelectOption
One answer. Its children are its words, or content of your own.
Step | Takes | What it does |
|---|---|---|
.Value(…) | a value of the select's type | What the select holds while it is chosen. Its own words when unset. |
.Label(…) | string | Its words, in place of children. |
.SelectedLabel(…) | string | What the button says while it is chosen, when that is not its own words. |
.Keywords(…) | string | More words a search finds it by, never shown: "apple orange pear". |
.Icon(…) | Ui.IconName — one of 317 names | An icon at its start, in a listbox or a combobox. |
.IconVariant(…) | Ui.IconVariantOutline Solid Mini Micro Each is a step of its own too, unless an HTML tag has its name. | Which drawing of the icon. Mini when unset. |
.IconClass(…) | string | Classes for the icon. |
.Avatar(…) | string | A picture's address, drawn as an avatar at its start. It wins over an icon. |
.Description(…) | string | Words under its label in the list, not in the button. |
.Disabled() | bool | Shown but not pickable. |
Ui.SelectGroup
Options under a heading: an <optgroup> in the browser's select.
Step | Takes | What it does |
|---|---|---|
.Label(…) | string | The heading over its options. |
Ui.SelectButton
The listbox's button, as a child, for what the select's steps do not reach.
Step | Takes | What it does |
|---|---|---|
.Placeholder(…) | string | Shown while nothing is picked. |
.Invalid() | bool | Error styling. |
.Size(…) | Ui.SelectSizeBase Sm Xs Each is a step of its own too, unless an HTML tag has its name. | How tall it is. |
.Disabled() | bool | A button that cannot be pressed. |
.Clearable() | bool | A button that clears the answer. |
Ui.SelectInput
The combobox's input, as a child, and what is typed into it.
Step | Takes | What it does |
|---|---|---|
.Placeholder(…) | string | Shown while it is empty. |
.Invalid() | bool | Error styling: a refused search. |
.Size(…) | Ui.SelectSizeBase Sm Xs Each is a step of its own too, unless an HTML tag has its name. | How tall it is. |
.Value(…) | string | The text, for a page that holds the search itself. |
.OnInput(…) | a handler given the new value | Runs with the text as typed, or once typing has paused for Debounce. |
.Debounce(…) | a TimeSpan: 250.Milliseconds | How long typing pauses before OnInput runs. |
Ui.SelectSearch
The listbox's search field, as a child.
Step | Takes | What it does |
|---|---|---|
.Placeholder(…) | string | Shown while it is empty. "Search..." when unset. |
.Icon(…) | Ui.IconName — one of 317 names | The icon at its start. A magnifying glass when unset. |
.Clearable() | bool | The button that empties it. Shown unless false. |
.Value(…) | string | The text, for a page that holds the search itself. |
.OnInput(…) | a handler given the new value | Runs with the text as typed, or once typing has paused for Debounce. |
.Debounce(…) | a TimeSpan: 250.Milliseconds | How long typing pauses before OnInput runs. |
Ui.SelectOptionCreate
The row that offers to make an option of what was typed. Its children are its words.
Step | Takes | What it does |
|---|---|---|
.MinLength(…) | int | How long the search must be before it is offered. |
.OnClick(…) | a handler given the text | Runs with the search as typed when it is picked. |
Ui.SelectOptionEmpty
What the list says when a search finds nothing. Its children are its words.
Step | Takes | What it does |
|---|---|---|
.WhenLoading(…) | string | What it says while results are on their way. |