How to filter generic list parameter in a blazor component without knowing its property name and types

Viewed 2134

Below is an unordered list with a search input that filters the list as you type. Everything works perfectly:

@page "/todolist"

<input @bind-value="SearchTerm" @bind-value:event="oninput" />

<span class="text-muted ml-5">
    Showing @FilteredToDos.Count out of @ToDoItems.Count
</span>

<h4 class="mt-4">To Do's</h4>
<ul>
    @foreach (var toDo in FilteredToDos)
    {
        <li>@toDo.Name</li>
    }
</ul>

@code {
    // Initialize SearchTerm to "" to prevent null's
    string SearchTerm { get; set; } = "";

    // Data
    class ToDoItem
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    List<ToDoItem> ToDoItems => new List<ToDoItem>
    {
        new ToDoItem {Id = 1, Name = "Garbage" },
        new ToDoItem {Id = 2, Name = "Dishes" },
        new ToDoItem {Id = 3, Name = "Wash clothes" },
        new ToDoItem {Id = 4, Name = "Water flowers" }
    };
    //filter
    List<ToDoItem> FilteredToDos => ToDoItems.Where(i => i.Name.ToLower().Contains(SearchTerm.ToLower())).ToList();
}

I seperated the list into its own component (TodoComponent):

@typeparam TItem

<input @bind-value="SearchTerm" @bind-value:event="oninput" />

<h4 class="mt-4">To Do's</h4>
<ul>
    @foreach (var toDo in Todos)
    {
        @UnorderedList(toDo)
    }
</ul>

@code {
    [Parameter]
    public List<TItem> Todos { get; set; }
    [Parameter]
    public RenderFragment<TItem> UnorderedList { get; set; }

    string SearchTerm { get; set; } = "";

    List<TItem> FilteredToDos => Todos.Where(i => i.Name.ToLower().Contains(SearchTerm.ToLower())).ToList();
}

I'm calling the blazor component here:

@page "/todo"

    <TodoComponent Todos="@ToDoItems">
        <UnorderedList>
            <li>ID: @context.Id Name: @context.Name </li>
        </UnorderedList>
    </TodoComponent>

@code {
    private List<ToDoItem> ToDoItems => new List<ToDoItem>
        {
            new ToDoItem {Id = 1, Name = "Garbage" },
            new ToDoItem {Id = 2, Name = "Dishes" },
            new ToDoItem {Id = 3, Name = "Wash clothes" },
            new ToDoItem {Id = 4, Name = "Water flowers" }
        };

    public class ToDoItem
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

}

The list displays fine, however I cannot filter the list anymore. I don't want to put the filter function outside of the component. What would be the best way to filter based on Name or ID inside the component? Would it be better to use a different data structure instead of a list?

1 Answers

Add a parameter property to your component called GetFilterData that is a Func<TItem, string>

Then you can call GetFilterData on each item to decide if you should include it or not.

Change the FetchData <table> markup in a standard template Blazor app to the following:

    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            <MyList Items=@forecasts GetFilterableText=@(item => $"{item.Date} {item.Summary} {item.TemperatureC} {item.TemperatureF}" )>
                <tr>
                    <td>@context.Date.ToShortDateString()</td>
                    <td>@context.TemperatureC</td>
                    <td>@context.TemperatureF</td>
                    <td>@context.Summary</td>
                </tr>
            </MyList>
        </tbody>
    </table>

Then add the following component to your app

@typeparam TItem

<input @bind=Filter @bind:event="oninput" />
@foreach (TItem item in GetFilteredItems())
{
    if (ChildContent == null)
    {
        <li>@item?.ToString()</li>
    }
    else
    {
        @ChildContent(item);
    }
}

@code {
        [Parameter]
        public IEnumerable<TItem> Items { get; set; }

        [Parameter]
        public Func<TItem, string> GetFilterableText { get; set; }

        [Parameter]
        public RenderFragment<TItem> ChildContent { get; set; }

    private string Filter;
    private static readonly Func<TItem, string> DefaultGetFilterableText =
        item => (item?.ToString() ?? "");


    private IEnumerable<TItem> GetFilteredItems()
    {
        Func<TItem, string> filterFunc = GetFilterableText ?? DefaultGetFilterableText;
        IEnumerable<TItem> result = (Items ?? Array.Empty<TItem>());
        if (!string.IsNullOrEmpty(Filter))
        {
            result = result
                .Where(x =>
                    (GetFilterableText(x) ?? "")
                    .Contains(Filter, StringComparison.InvariantCultureIgnoreCase));
        }
        return result;
    }

}

You should be able to adapt this to your needs.

Related