Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DropdownList: Filter text input in the menu #5915

Open
wants to merge 5 commits into
base: next-2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions Source/Extensions/Blazorise.Components/DropdownList.razor
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@typeparam TItem
@typeparam TValue
<Dropdown @ref="@dropdownRef" ElementId="@ElementId" Class="@Class" Style="@Style" EndAligned="@EndAligned" Disabled="@Disabled" Direction="@Direction" Attributes="@Attributes">
<DropdownToggle @ref="@dropdownToggleRef" Color="@Color" Size="@DropdownToggleSize" TabIndex="@TabIndex">@ChildContent</DropdownToggle>
<DropdownMenu MaxMenuHeight="@MaxMenuHeight">
@if ( Data != null )
<Dropdown @ref="@dropdownRef" ElementId="@ElementId" Class="@Class" Style="@Style" EndAligned="@EndAligned"
Disabled="@Disabled" Direction="@Direction" Attributes="@Attributes">
<DropdownToggle @ref="@dropdownToggleRef" Color="@Color" Size="@DropdownToggleSize"
TabIndex="@TabIndex">@ChildContent</DropdownToggle>
<DropdownMenu MaxMenuHeight="@MaxMenuHeight" Padding="@(FilterEnabled ? Padding.Is0.FromTop : Padding.IsAuto)">
@if (FilterEnabled)
{
@if ( Virtualize && Data is ICollection<TItem> collectionableData )
<TextEdit Value="@FilterText" ValueChanged="@OnFilterTextChangedHandler"
Position="Position.Sticky.Top.Is0"/>
}
@if (FilteredData != null)
{
@if ( Virtualize && FilteredData is ICollection<TItem> collectionableData )
{
<Virtualize TItem="TItem" Context="item" Items="@collectionableData">
@itemFragment( item )
</Virtualize>
}
else
{
@foreach ( var item in Data ?? Enumerable.Empty<TItem>() )
@foreach ( var item in FilteredData ?? Enumerable.Empty<TItem>() )
{
@itemFragment( item )
}
}
}
</DropdownMenu>
</Dropdown>

@code {
protected RenderFragment<TItem> itemFragment => item => __builder =>
{
protected RenderFragment<TItem> itemFragment => item => __builder => {
var text = GetItemText( item );
var value = GetItemValue( item );
var disabled = GetItemDisabled( item );

<DropdownItem @key="@item" Clicked="@HandleDropdownItemClicked" Value="@value" Disabled="@disabled"
ShowCheckbox="@(SelectionMode == DropdownListSelectionMode.Checkbox)"
Checked="IsSelected(value)"
CheckedChanged="@((isChecked) => HandleDropdownItemChecked(isChecked, value))">@text</DropdownItem>
CheckedChanged="@((isChecked) => HandleDropdownItemChecked( isChecked, value ))">@text</DropdownItem>
};
}
51 changes: 50 additions & 1 deletion Source/Extensions/Blazorise.Components/DropdownList.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ public partial class DropdownList<TItem, TValue> : ComponentBase
/// Reference to the DropdownToggle component.
/// </summary>
protected DropdownToggle dropdownToggleRef;

private List<TValue> selectedValues;

private IEnumerable<TItem> filteredData;

#endregion

Expand Down Expand Up @@ -103,6 +105,34 @@ private bool GetItemDisabled( TItem item )
return DisabledItem.Invoke( item );
}

private void FilterData( IQueryable<TItem> query )
{
dirtyFilter = false;
if ( !FilterEnabled || string.IsNullOrEmpty( FilterText ) )
{
filteredData = Data;
return;
}

if ( query == null )
{
filteredData = Enumerable.Empty<TItem>();
return;
}

if ( TextField == null )
return;

filteredData = Data.Where( x => TextField.Invoke( x ).Contains( FilterText, StringComparison.OrdinalIgnoreCase ) );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to store filtered data? Or can we just return it as

return Data.Where( x => TextField.Invoke( x ).Contains( FilterText, StringComparison.OrdinalIgnoreCase ) );

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in this the filteredData field isn't needed. That implementation can go without it.

I took it form the Autocomplete, where it is an IList and the data aren't enumerated on every get. But made it IEnumberable, because the Data are also IEnumerable.

Well - it is a question now. Should the DropdownList.FilteredData also be an IList instead of IEnumerable?

  • IEnumerable will do the filtering on every enumeration. Possibly on every re-render. This could have significant perf implications.
  • If converted to List, the filtering will be done only on text change, but it needs to fit into memory.... This is also a concern for current implementation of Autocomplete...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, performance can be a problem. Dropdown, from a UX perspective is meant to be used only for small menus. With this new search features we are essentially breaking that "behavior" since we are allowing for very large menus. I guess that is fine if our users would expect that. But now we have a dilemma. Should we optimize or leave it as it is.

I think we can leave it as is, and optimize with caching later if it becomes a problem.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, the current state is final. filteredData is "needed" to avoid repeating all the checks unnecessarily, even when the filter isn’t dirty.

Another option is to keep everything (Data and FilteredData) as List -> that would signal "small" dataset and avoid unnecessary filtering on every get...

}

private Task OnFilterTextChangedHandler( string filteredText )
{
FilterText = filteredText;
dirtyFilter = true;
return Task.CompletedTask;
}

#endregion

#region Properties
Expand Down Expand Up @@ -148,6 +178,20 @@ protected bool IsSelected( TValue value )
/// </summary>
[Parameter] public IEnumerable<TItem> Data { get; set; }

private IEnumerable<TItem> FilteredData
{
get
{
if ( dirtyFilter )
FilterData( Data?.AsQueryable() );
return filteredData;
}
}

private bool dirtyFilter = true;

private string FilterText { get; set; }

/// <summary>
/// Method used to get the display field from the supplied data source.
/// </summary>
Expand All @@ -168,6 +212,11 @@ protected bool IsSelected( TValue value )
/// </summary>
[Parameter] public EventCallback<TValue> SelectedValueChanged { get; set; }

/// <summary>
/// Enebles filter text input on the top of the items list.
/// </summary>
[Parameter] public bool FilterEnabled { get; set; }
tesar-tech marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Custom classname for dropdown element.
/// </summary>
Expand Down
Loading