Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

FilterForm

Search and filters kept in the URL.

An interactive page: C# runs over a Blazor Server connection.

Usage

razor
<FilterForm Label="Filter customers">
    <FilterSearch Placeholder="Search customers" />
    <FilterSelect Name="status" Label="Status" EnumType="typeof(CustomerStatus)" />
</FilterForm>

@code {
    // 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

Issues.razor
@* 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>

@code {
    [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

ApplyButton.razor
<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>

@code {
    [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

Books
Title Author Year
A Brief History of TimeStephen Hawking1988
BelovedToni Morrison1987
Guns, Germs, and SteelJared Diamond1997
One Hundred Years of SolitudeGabriel García Márquez1967

Code

Books.razor
@* 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>

@code {
    [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.

razor
<FilterForm Fields="@(["from"])">
    <FilterSearch Placeholder="Search invoices" />
    <input type="date" name="from" value="@From" aria-label="From" />
</FilterForm>

@code {
    [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

What works in each mode, FilterForm
FeatureOffStaticLive
Filters in the address, applied with Apply or Enter; Clear removes themYesYesYes
Keeps the rest of the address and goes back to page 1YesYesYes
Applies as you type and on each change, without a reloadNoYesYes

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 form with role="search": a search landmark named "Filters" (Label).
  • FilterSearch and FilterSelect have a Label that's hidden but names them. The search defaults to "Search"; a select requires one.
  • FilterCheckbox is a native checkbox with its Label (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 Live false, an Apply button submits; "Clear" is a link.

Parameters

FilterForm parameters
PropTypeDefault
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.booltrue
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"

See 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.