The SearchableList component is a generic list container designed to display and filter a collection of items that implement the ISearchableItem interface. It provides an integrated search box that allows users to search through the items by typing in a term. The component is highly customizable, supporting features such as custom "no results" messages, background search operations, and the addition of custom components before or after the search box. Use this component when you need a searchable list interface in your application.
Usage
Instantiate a SearchableList using the static helper methods from Tesserae.UI. It accepts an array or an ObservableList of items (each implementing ISearchableItem) and optional column widths for layout purposes. The search box filters items based on whether their IsMatch method returns true for each entered search term.
Below is a minimal sample demonstrating how to implement the ISearchableItem interface and create a SearchableList with a custom "no results" message:
searchable-list-sample.js
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using H5.Core;
using Tesserae;
using static H5.Core.dom;
using static Tesserae.UI;
namespace Tesserae.Tests
{
internal static class App
{
private static void Main()
{
var sampleComponent = new SampleUsage();
document.body.style.overflow = "hidden";
MountCenteredToBody(sampleComponent);
}
}
public class MySearchableItem : ISearchableItem
{
private readonly string _name;
public MySearchableItem(string name)
{
_name = name;
}
public bool IsMatch(string searchTerm) => _name.IndexOf(searchTerm, StringComparison.OrdinalIgnoreCase) >= 0;
public IComponent Render() => Card(TextBlock(_name));
}
public class SampleUsage : IComponent
{
public HTMLElement Render()
{
// Create an array of searchable items.
var items = new MySearchableItem[]
{
new MySearchableItem("Apple"),
new MySearchableItem("Banana"),
new MySearchableItem("Cherry")
};
// Instantiate SearchableList with a custom no results message.
var searchableList = SearchableList(items)
.WithNoResultsMessage(() => Card(TextBlock("No Results").Padding(16.px())));
return searchableList.Render();
}
}
}
Methods
• WithNoResultsMessage(Func emptyListMessageGenerator)
- Sets a custom no results message by specifying a delegate that returns an IComponent.
- Parameter: emptyListMessageGenerator – a function that generates an IComponent to display when no items match the search.
• SearchBox(Action sb)
- Allows customization of the inner SearchBox component.
- Parameter: sb – an action delegate to modify the SearchBox properties.
• CaptureSearchBox(out SearchBox sb)
- Captures and returns the underlying SearchBox instance for further customization or external manipulation.
- Parameter: sb – an output parameter that will hold the SearchBox instance.
• WithBackgroundSearch(Func<string, Task<T[]>> searcher)
- Configures a background search operation that is executed asynchronously based on the current search text.
- Parameter: searcher – a function that returns a Task with an array of items to include in the search result.
• HideSearchBoxIfLessThan(int items)
- Hides the search box when the number of items in the list is less than the specified threshold.
- Parameter: items – the minimum number of items required to show the search box.
• ShowNotMatching()
- Configures the list to optionally display items that do not match the current search criteria (styled differently).
• BeforeSearchBox(params IComponent[] beforeComponents)
- Inserts one or more custom components into the search box container before the default SearchBox.
- Parameter: beforeComponents – one or more IComponent instances to add before the SearchBox.
• AfterSearchBox(params IComponent[] afterComponents)
- Appends one or more custom components to the search box container after the SearchBox.
- Parameter: afterComponents – one or more IComponent instances to add after the SearchBox.
• Render()
- Renders the SearchableList component into its underlying HTMLElement.
- Returns: HTMLElement – the rendered DOM element.
Properties
• Items (ObservableList)
- Exposes the underlying collection of searchable items.
• ShowNotMatchingItems (bool)
- Determines whether items that do not match the search criteria should still be shown (with a distinct styling).
• StylingContainer (HTMLElement)
- (Read-only) Returns the container element used for styling purposes.
• PropagateToStackItemParent (bool)
- (Read-only) Indicates if the styling should propagate to the parent container of each list item.
Samples
Live Filtering with 50 Items
This sample produces 50 contact-style rows. Typing in the search box (try 7, Eve, or manager) filters the list as you type — when nothing matches, the custom "No Results" component is shown.
searchable-list-2-sample.js
using System;
using System.Linq;
using H5.Core;
using Tesserae;
using static H5.Core.dom;
using static Tesserae.UI;
namespace Tesserae.Tests
{
internal static class App
{
private static void Main()
{
document.body.style.overflow = "hidden";
MountCenteredToBody(new LiveFilterSample());
}
}
public class LiveFilterSample : IComponent
{
public HTMLElement Render()
{
var roles = new[] { "Engineer", "Designer", "Manager", "Analyst" };
var names = new[] { "Alice", "Bob", "Carol", "Dan", "Eve", "Frank", "Grace", "Hank" };
var items = Enumerable.Range(1, 50).Select(n =>
new Contact(
$"{names[n % names.Length]} #{n}",
roles[n % roles.Length],
$"user{n}@example.com")
).ToArray();
return SearchableList(items)
.WithNoResultsMessage(() =>
BackgroundArea(VStack().AlignItemsCenter().Children(
Icon(UIcons.SearchMinus).XLarge().Muted(),
TextBlock("No matching contacts").SemiBold().PT(8),
TextBlock("Try a different keyword").Small().Muted()
).Padding(16.px())).WS().HS().MinHeight(150.px()))
.Height(400.px())
.Render();
}
}
public class Contact : ISearchableItem
{
private readonly string _name, _role, _email;
public Contact(string name, string role, string email)
{ _name = name; _role = role; _email = email; }
public bool IsMatch(string term)
=> _name.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0
|| _role.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0
|| _email.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
public IComponent Render() => Card(HStack().AlignItemsCenter().Children(
Icon(UIcons.User).Primary(),
VStack().PL(12).Children(
TextBlock(_name).SemiBold(),
HStack().Children(
TextBlock(_role).Small().Muted(),
TextBlock(" · " + _email).Small().Muted()
)
)
).Padding(8.px()));
}
}
Searchable List with Background Search
WithBackgroundSearch lets you augment the local matches with results fetched asynchronously (for example from a backend). The delegate is invoked with the current search term after the user pauses typing, and its results are merged into the displayed list.
searchable-list-3-sample.js
using System;
using System.Linq;
using System.Threading.Tasks;
using H5.Core;
using Tesserae;
using static H5.Core.dom;
using static Tesserae.UI;
namespace Tesserae.Tests
{
internal static class App
{
private static void Main()
{
document.body.style.overflow = "hidden";
MountCenteredToBody(new BackgroundSearchSample());
}
}
public class BackgroundSearchSample : IComponent
{
public HTMLElement Render()
{
var localItems = new[]
{
new ColorItem("Crimson"),
new ColorItem("Coral"),
new ColorItem("Cerulean")
};
return SearchableList(localItems)
.BeforeSearchBox(Button("Filter").SetIcon(UIcons.Filter).Link())
.AfterSearchBox(Button("Add color").SetIcon(UIcons.Plus).Primary()
.OnClick((s, e) => Toast().Information("Add clicked")))
.WithBackgroundSearch(async term =>
{
await Task.Delay(500); // simulate remote lookup
if (string.IsNullOrWhiteSpace(term)) return new ColorItem[0];
return new[]
{
new ColorItem($"Remote – {term} light"),
new ColorItem($"Remote – {term} dark")
};
})
.WithNoResultsMessage(() =>
BackgroundArea(Card(TextBlock("No matching colors").Padding(16.px()))).WS().HS().MinHeight(100.px()))
.Height(320.px())
.Render();
}
}
public class ColorItem : ISearchableItem
{
private readonly string _name;
public ColorItem(string name) { _name = name; }
public bool IsMatch(string term)
=> _name.IndexOf(term, StringComparison.OrdinalIgnoreCase) >= 0;
public IComponent Render() => Card(TextBlock(_name).Padding(8.px()));
}
}