FilterForm
An interactive page: C# runs over a Blazor Server connection.
Usage
<FilterForm Label="Filter customers">
<FilterSearch Placeholder="Search customers" />
<FilterSelect Name="status" Label="Status" EnumType="typeof(CustomerStatus)" />
</FilterForm>
{
// The page reads the filters from the address and filters its own data.
[SupplyParameterFromQuery(Name = "q")] private string? Search { get; set; }
[SupplyParameterFromQuery(Name = "status")] private string? Status { get; set; }
}A filter form is a GET form: its values go in the address, so a filtered view can be bookmarked, shared and reloaded, and it works with JavaScript off. It doesn't filter anything itself; the page reads the values with [SupplyParameterFromQuery] and filters its own data. To filter a table by its columns instead, use the Table's own column filters.
Search, select and checkbox
Use FilterSearch for a search box (?q= unless you give it a Name), FilterSelect for a choice with "All" first, and FilterCheckbox for a filter that's on or off (?mine=true).
Preview
6 of 6 issues
- Export to CSV drops the last row Bug
- Dark mode for the billing page Feature
- Document the webhook retry policy Docs
- Search ignores accented letters Bug
- Keyboard shortcut for new project Feature
- Invite email shows the wrong workspace Bug
Code
@* The page reads the filters from the URL and filters its own list. *@
<Stack Gap="4" MaxWidth="34.Rem" Grow>
<FilterForm Label="Filter issues">
<FilterSearch Placeholder="Search issues" />
<FilterSelect Name="label" Label="Label" Items="Labels" />
<FilterCheckbox Name="mine" Label="Assigned to me" />
</FilterForm>
@* A live region: screen readers hear the new count after each filter. *@
<TextBlock Size="TextSize.Sm" Color="Color.Muted" aria-live="polite">Matches.Count() of All.Length issues</TextBlock>
if (Matches.Any())
{
<List Plain Gap="3">
foreach (var issue in Matches)
{
<ListItem>
<Row Gap="2" Align="Align.Center">
<TextBlock As="span">issue.Title</TextBlock>
<Badge Size="BadgeSize.Sm">issue.Label</Badge>
</Row>
</ListItem>
}
</List>
}
else
{
<TextBlock Color="Color.Muted">No issues match.</TextBlock>
}
</Stack>
{
[SupplyParameterFromQuery(Name = "q")] private string? Search { get; set; }
[SupplyParameterFromQuery(Name = "label")] private string? LabelFilter { get; set; }
[SupplyParameterFromQuery(Name = "mine")] private bool Mine { get; set; }
private IEnumerable<Issue> Matches => All
.Where(i => string.IsNullOrWhiteSpace(Search) || i.Title.Contains(Search, StringComparison.OrdinalIgnoreCase))
.Where(i => string.IsNullOrEmpty(LabelFilter) || i.Label == LabelFilter)
.Where(i => !Mine || i.Mine);
}Live: The same, through Blazor's navigation: the page's query parameters update.
With the script, the search applies after a pause in typing and the others as they change, without a reload, and focus stays where it was. A FilterSelect's options come from Items or an enum (EnumType), and the URL holds the value, so use ItemText when people should see something else.
Filtering on demand
Use Live="false" to keep the Apply button with the script too, so nothing applies until it's pressed or Enter is, and ApplyText to rename it.
Preview
5 of 5 people
- Ada Lovelace, Engineering
- Grace Hopper, Engineering
- Alan Turing, Research
- Katherine Johnson, Research
- Susan Kare, Design
Code
<Stack Gap="4" MaxWidth="34.Rem" Grow>
<FilterForm Label="Find people" Live="false" ApplyText="Search">
<FilterSearch Name="name" Label="Name" Placeholder="Search by name" />
<FilterSelect Name="team" Label="Team" Items="Teams" />
</FilterForm>
<TextBlock Size="TextSize.Sm" Color="Color.Muted" aria-live="polite">Found.Count() of People.Length people</TextBlock>
<List Plain Gap="2">
foreach (var person in Found)
{
<ListItem>person.Name, person.Team</ListItem>
}
</List>
</Stack>
{
[SupplyParameterFromQuery(Name = "name")] private string? Query { get; set; }
[SupplyParameterFromQuery(Name = "team")] private string? Team { get; set; }
private IEnumerable<Person> Found => People
.Where(p => string.IsNullOrWhiteSpace(Query) || p.Name.Contains(Query, StringComparison.OrdinalIgnoreCase))
.Where(p => string.IsNullOrEmpty(Team) || p.Team == Team);
}Choose this when each filter is slow to apply, such as a search against a large database, or when people set several filters before looking at the results.
With a table
Put the form above a Table and give the table the filtered rows: the form keeps the rest of the address, such as the table's sort, and drops page numbers, so filtering goes back to page 1.
Preview
Code
@* The filters and the table share the address: filtering keeps the sort and goes back to page 1. *@
<Stack Gap="4" Grow>
<FilterForm Label="Filter books">
<FilterSearch Name="title" Label="Title" Placeholder="Search titles" />
<FilterSelect Name="genre" Label="Genre" Items="Genres" />
</FilterForm>
<Table Items="Found" PageSize="4" InitialSort="title" Caption="Books" QueryPrefix="books-"
EmptyText="No books match these filters" Grow>
<PropertyColumn Property="b => b.Title" Sortable />
<PropertyColumn Property="b => b.Author" Sortable />
<PropertyColumn Property="b => b.Year" Sortable />
</Table>
</Stack>
{
[SupplyParameterFromQuery(Name = "title")] private string? Title { get; set; }
[SupplyParameterFromQuery(Name = "genre")] private string? Genre { get; set; }
// The page filters its own data; the table sorts and pages what it's given.
private IEnumerable<Book> Found => All
.Where(b => string.IsNullOrWhiteSpace(Title) || b.Title.Contains(Title, StringComparison.OrdinalIgnoreCase))
.Where(b => string.IsNullOrEmpty(Genre) || b.Genre == Genre);
}Live: The same, through Blazor's navigation; the table follows the address.
Page numbers are page or a key ending in -page, _page or .page, as a table with a QueryPrefix writes (books-page). The order list demo does the same for a whole page.
Your own inputs
Put any input with a name in the form, and list the names in Fields so the form doesn't also carry their old values over from the address.
<FilterForm Fields="(["from"])">
<FilterSearch Placeholder="Search invoices" />
<input type="date" name="from" value="From" aria-label="From" />
</FilterForm>
{
[SupplyParameterFromQuery(Name = "from")] private string? From { get; set; }
}With the script, a text, search, email, phone or URL input of your own applies after a pause in typing, like FilterSearch; a select or checkbox applies when it changes; and any other input, such as a date or a number, applies a moment after it changes, so typing a date part by part applies once. Give each one a label, here an aria-label, since the form doesn't add one.
Saying how many match
New results aren't announced, so show the count in a live region (aria-live="polite") next to the list, as the examples do: screen readers read it each time it changes.
Sizes and its own words
Use Size on each filter field to match a compact toolbar (the buttons stay their own size), Label for the search landmark's name ("Filters"), and ApplyText and ClearText for the buttons. A search box's and a select's Label is hidden but names it (a select requires one; the search defaults to "Search"), a FilterCheckbox's is its required visible text, and a FilterSelect's first option reads "Label: All" unless you set AllText.
Parts
Written inside FilterForm. The API page lists their parameters.
FilterSearch: a search box for a FilterForm.FilterSelect: a select for a FilterForm, with an "all" option first.FilterCheckbox: a checkbox for a FilterForm: when checked the URL has name=true.
Mode support
| Feature | Off | Static | Live |
|---|---|---|---|
| Filters in the address, applied with Apply or Enter; Clear removes them | Yes | Yes | Yes |
| Keeps the rest of the address and goes back to page 1 | Yes | Yes | Yes |
| Applies as you type and on each change, without a reload | No | Yes | Yes |
Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).
Accessibility
- It renders a GET
formwithrole="search": a search landmark named "Filters" (Label). FilterSearchandFilterSelecthave aLabelthat's hidden but names them. The search defaults to "Search"; a select requires one.FilterCheckboxis a native checkbox with itsLabel(required) shown beside it.- With script, filters apply as you type or change a value, without a reload, and focus stays where it was; Enter applies at once.
- The new results aren't announced, so say how many there are in a live region (a
Table's range under it is read when it changes). - Without script, or with
Livefalse, an Apply button submits; "Clear" is a link.
Parameters
| Prop | Type | Default |
|---|---|---|
ChildContentThe filter fields: FilterSearch, FilterSelect, FilterCheckbox, or your own inputs (list their names in Fields). | RenderFragment? | |
LiveApplies as you type and on every change, with JavaScript. | bool | true |
FieldsThe names of inputs you add yourself, so the form doesn't also carry them over from the URL. | IReadOnlyList<string>? | |
LabelThe search landmark's name. | string? | "Filters" |
ApplyTextThe submit button's text, shown when JavaScript is off or Live is false. | string? | "Apply" |
ClearTextThe text of the link that removes every filter. | string? | "Clear" |
Plus 16 item props and 4 common props.
Full APISee also
- Table: sort, filter, page and select, with state in the URL.
- Pagination: page links that keep the rest of the URL.
- TextField: a labeled text input with add-ons and a password reveal.
- Select: a native select, themed where the browser allows.
- Demos: Filtered order list.