feat(module: table): add filters (#1178)

* feat: add table filter

* add filter buttons

* fix filter menu

* make filter work

* support multiple filters

* delete the old files

* support single select
This commit is contained in:
James Yeung 2021-02-27 09:55:48 -06:00 committed by GitHub
parent 08fec90cd3
commit 4065869806
14 changed files with 360 additions and 61 deletions

View File

@ -88,6 +88,7 @@ namespace AntDesign.Internal
* Current placement, would change by overlay in some cases(via ChangePlacementForShow function)
*/
private PlacementType _placement = PlacementType.BottomLeft;
[Parameter]
public PlacementType Placement
{
@ -127,6 +128,17 @@ namespace AntDesign.Internal
[Parameter]
public TriggerBoundaryAdjustMode BoundaryAdjustMode { get; set; } = TriggerBoundaryAdjustMode.InView;
[Parameter]
public ElementReference TriggerReference
{
get => _triggerReference;
set
{
_triggerReference = value;
RefBack.Set(value);
}
}
[Inject]
private DomEventService DomEventService { get; set; }
@ -134,6 +146,7 @@ namespace AntDesign.Internal
private bool _mouseInOverlay = false;
protected Overlay _overlay = null;
private ElementReference _triggerReference;
protected override void OnAfterRender(bool firstRender)
{
@ -158,14 +171,16 @@ namespace AntDesign.Internal
DomEventService.AddEventListener(Ref, "focusin", OnUnboundFocusIn, true);
DomEventService.AddEventListener(Ref, "focusout", OnUnboundFocusOut, true);
DomEventService.AddEventListener(Ref, "contextmenu", OnContextMenu, true, true);
}
return base.OnAfterRenderAsync(firstRender);
}
private void OnUnboundMouseEnter(JsonElement jsonElement) => OnTriggerMouseEnter();
private void OnUnboundMouseLeave(JsonElement jsonElement) => OnTriggerMouseLeave();
private void OnUnboundFocusIn(JsonElement jsonElement) => OnTriggerFocusIn();
private void OnUnboundFocusOut(JsonElement jsonElement) => OnTriggerFocusOut();
private async void OnUnboundClick(JsonElement jsonElement)
@ -175,6 +190,7 @@ namespace AntDesign.Internal
await OnClickDiv(eventArgs);
}
private async void OnContextMenu(JsonElement jsonElement)
{
var eventArgs = JsonSerializer.Deserialize<MouseEventArgs>(jsonElement.ToString(),
@ -361,8 +377,13 @@ namespace AntDesign.Internal
await OnOverlayHiding.InvokeAsync(visible);
}
protected virtual void OnOverlayShow() { }
protected virtual void OnOverlayHide() { }
protected virtual void OnOverlayShow()
{
}
protected virtual void OnOverlayHide()
{
}
internal void ChangePlacementForShow(PlacementType placement)
{

View File

@ -102,6 +102,7 @@ namespace AntDesign
set
{
_selectedKeys = value;
MenuItems.ForEach(x => x.UpdateStelected());
}
}
@ -140,7 +141,7 @@ namespace AntDesign
}
else if (menuitem.IsSelected || menuitem.FirstRun)
{
if (!menuitem.FirstRun)
if (!menuitem.FirstRun)
skipParentSelection = item.ParentMenu?.Key == menuitem.ParentMenu?.Key;
menuitem.Deselect(skipParentSelection);
}

View File

@ -87,6 +87,18 @@ namespace AntDesign
Select();
}
internal void UpdateStelected()
{
if (RootMenu.SelectedKeys.Contains(Key))
{
if (!IsSelected) Select();
}
else if (IsSelected)
{
Deselect();
}
}
public async Task HandleOnClick(MouseEventArgs args)
{
if (Disabled)

View File

@ -40,17 +40,10 @@ else if (IsHeader && HeaderColSpan != 0)
<th class="ant-table-cell ant-table-row-expand-icon-cell"></th>
}
<th class="@ClassMapper.Class" style="@FixedStyle @HeaderStyle" @onclick="HandelHeaderClick" colspan="@HeaderColSpan" title="@(Ellipsis?HeaderTitle:"")">
@if (Sortable)
<th class="@ClassMapper.Class" style="@FixedStyle @HeaderStyle" colspan="@HeaderColSpan" title="@(Ellipsis?HeaderTitle:"")">
@if (Sortable || Filters?.Any() == true)
{
if (ShowSorterTooltip)
{
@ToolTipSorter(this)
}
else
{
@SortHeader(this)
}
@FilterToolTipSorter(this)
}
else if (TitleTemplate != null)
{
@ -120,17 +113,16 @@ else if (IsBody && RowSpan != 0 && ColSpan != 0)
string HeaderTitle => Title ?? DisplayName ?? FieldName ?? DataIndex;
readonly RenderFragment<Column<TData>> SortHeader = col =>
@<div class="ant-table-column-sorters">
<span>
@if (col.TitleTemplate != null)@col.TitleTemplate else @col.HeaderTitle
</span>
@{
@{
bool hasDescendingSorter = SortDirection.Descending.IsIn(col.SortDirections);
bool hasAscendingSorter = SortDirection.Ascending.IsIn(col.SortDirections);
}
<span class="ant-table-column-sorter @((hasDescendingSorter && hasAscendingSorter)?"ant-table-column-sorter-full":"")">
<span class="ant-table-column-sorter @(hasDescendingSorter && hasAscendingSorter?"ant-table-column-sorter-full":"")">
<span class="ant-table-column-sorter-inner">
@if (hasAscendingSorter)
{
@ -144,13 +136,89 @@ else if (IsBody && RowSpan != 0 && ColSpan != 0)
</span>
</div>;
readonly RenderFragment<Column<TData>> ToolTipSorter = col =>
@<Tooltip Title="@col.SorterTooltip">
<Unbound>
<div class="ant-table-column-sorters-with-tooltip" @ref="context.Current">
@col.SortHeader(col)
</div>
</Unbound>
</Tooltip>;
readonly RenderFragment<Column<TData>> ToolTipSorter = col =>
@<Template>
@if (col.ShowSorterTooltip)
{
<Tooltip Title="@col.SorterTooltip">
<Unbound>
<div class="ant-table-column-sorters-with-tooltip" @ref="context.Current">
@col.SortHeader(col)
</div>
</Unbound>
</Tooltip>
}
else
{
@col.SortHeader(col)
}
</Template>;
}
readonly RenderFragment<Column<TData>> FilterToolTipSorter = col =>
@<Template>
@if (col.Filters?.Any() == true)
{
<div class="ant-table-filter-column">
<span class="ant-table-filter-column-title" @attributes="@(new Dictionary<string, object>() { ["onclick"]=_callbackFactory.Create(col, col.HandelHeaderClick )})">
@if (col.Sortable)
{
@col.ToolTipSorter(col)
}
</span>
<Dropdown Trigger="new[] { TriggerType.Click }" Visible="col._filterOpened" Placement="PlacementType.BottomRight" TriggerReference="col._filterTriggerRef">
<Unbound>
<span @ref="@col._filterTriggerRef" class="ant-table-filter-trigger-container @(col._filterOpened?" ant-table-filter-trigger-container-open":"")">
<span role="button" tabindex="-1" class="ant-dropdown-trigger ant-table-filter-trigger @(col._hasFilterSelected?"active":"")"
@attributes="@(new Dictionary<string, object>() { ["onclick"] = _callbackFactory.Create<MouseEventArgs>(col, () => col._filterOpened = !col._filterOpened )})">
<Icon Type="filter" Theme="fill" />
</span>
</span>
</Unbound>
<Overlay>
<div class="ant-table-filter-dropdown">
<Menu AutoCloseDropdown="false" SelectedKeys="col._selectedFilterValues">
@foreach (var filter in col.Filters)
{
<MenuItem Key="@filter.Value.ToString()" @attributes="@(new Dictionary<string, object>() { ["OnClick"] = _callbackFactory.Create<MouseEventArgs>(col, () => col.FilterSelected(filter))})">
@if (col.FilterMultiple)
{
<Checkbox Value="filter.Selected" @attributes="@(new Dictionary<string, object>() { ["ValueChanged"] = _callbackFactory.Create<bool>(col, value => col.FilterSelected(filter)) })">@filter.Text</Checkbox>
}
else
{
<Radio TValue="bool" Checked="filter.Selected" @attributes="@(new Dictionary<string, object>() { ["CheckedChanged"] = _callbackFactory.Create<bool>(col, value => col.FilterSelected(filter))})">@filter.Text</Radio>
}
</MenuItem>
}
</Menu>
<div class="ant-table-filter-dropdown-btns">
<Button Size="small" Type="link" @attributes="@(new Dictionary<string, object>() { ["OnClick"] = _callbackFactory.Create<MouseEventArgs>(col, ()=> col.FilterReset()) })">
@col.Table?.Locale.FilterReset
</Button>
<Button Size="small" Type="primary" @attributes="@(new Dictionary<string, object>() { ["OnClick"] = _callbackFactory.Create<MouseEventArgs>(col, ()=> col.FilterConfirm()) })">
@col.Table?.Locale.FilterConfirm
</Button>
</div>
</div>
</Overlay>
</Dropdown>
</div>
}
else
{
@if (col.Sortable)
{
@col.ToolTipSorter(col)
}
else if (col.TitleTemplate != null)
{
@col.TitleTemplate
}
else
{
@col.HeaderTitle
}
}
</Template>;
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
using AntDesign.Core.Reflection;
using AntDesign.Internal;
@ -49,6 +50,24 @@ namespace AntDesign
[Parameter]
public SortDirection DefaultSortOrder { get; set; }
[Parameter]
public IEnumerable<TableFilter<TData>> Filters { get; set; }
[Parameter]
public bool FilterMultiple { get; set; } = true;
/// <summary>
/// Function that determines if the row is displayed when filtered
/// <para>
/// Parameter 1: The value of the filter item
/// </para>
/// <para>
/// Parameter 2: The value of the column
/// </para>
/// </summary>
[Parameter]
public Expression<Func<TData, TData, bool>> OnFilter { get; set; }
private PropertyReflector? _propertyReflector;
public string DisplayName => _propertyReflector?.DisplayName;
@ -57,12 +76,22 @@ namespace AntDesign
public ITableSortModel SortModel { get; private set; }
public ITableFilterModel FilterModel { get; private set; }
private SortDirection _sortDirection;
public Func<RowData, TData> GetValue { get; private set; }
void IFieldColumn.ClearSorter() => SetSorter(SortDirection.None);
private static readonly EventCallbackFactory _callbackFactory = new EventCallbackFactory();
private bool _filterOpened;
private bool _hasFilterSelected;
private string[] _selectedFilterValues;
private ElementReference _filterTriggerRef;
protected override void OnInitialized()
{
base.OnInitialized();
@ -158,5 +187,37 @@ namespace AntDesign
RowData.Expanded = !RowData.Expanded;
Table?.Refresh();
}
private void FilterSelected(TableFilter<TData> filter)
{
if (!FilterMultiple)
{
Filters.ForEach(x => x.Selected = false);
filter.Selected = true;
}
else
{
filter.Selected = !filter.Selected;
}
_selectedFilterValues = Filters.Where(x => x.Selected).Select(x => x.Value.ToString()).ToArray();
StateHasChanged();
}
private void FilterConfirm()
{
_filterOpened = false;
_hasFilterSelected = Filters?.Any(x => x.Selected) == true;
FilterModel = _hasFilterSelected ? new FilterModel<TData>(_propertyReflector.Value.PropertyInfo, OnFilter, Filters.Where(x => x.Selected).ToList()) : null;
Table?.ReloadAndInvokeChange();
}
private void FilterReset()
{
Filters.ForEach(x => x.Selected = false);
FilterConfirm();
}
}
}

View File

@ -1,4 +1,5 @@
using AntDesign.TableModels;
using System.Collections.Generic;
using AntDesign.TableModels;
using Microsoft.AspNetCore.Components;
namespace AntDesign
@ -17,6 +18,8 @@ namespace AntDesign
public ITableSortModel SortModel { get; }
public ITableFilterModel FilterModel { get; }
internal void ClearSorter();
}
}

View File

@ -189,9 +189,17 @@ namespace AntDesign
foreach (var col in ColumnContext.HeaderColumns)
{
if (col is IFieldColumn fieldColumn && fieldColumn.SortModel != null)
if (col is IFieldColumn fieldColumn)
{
queryModel.AddSortModel(fieldColumn.SortModel);
if (fieldColumn.SortModel != null)
{
queryModel.AddSortModel(fieldColumn.SortModel);
}
if (fieldColumn.FilterModel != null)
{
queryModel.AddFilterModel(fieldColumn.FilterModel);
}
}
}
@ -204,12 +212,16 @@ namespace AntDesign
if (_dataSource != null)
{
var query = _dataSource.AsQueryable();
var orderedSortModels = queryModel.SortModel.OrderBy(x => x.Priority);
foreach (var sort in orderedSortModels)
foreach (var sort in queryModel.SortModel.OrderBy(x => x.Priority))
{
query = sort.SortList(query);
}
foreach (var filter in queryModel.FilterModel)
{
query = filter.FilterList(query);
}
query = query.Skip((PageIndex - 1) * PageSize).Take(PageSize);
queryModel.SetQueryableLambda(query);

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AntDesign
{
public class TableFilter<TValue>
{
public string Text { get; set; }
public TValue Value { get; set; }
public bool Selected { get; set; }
internal void SelectValue(bool selected)
{
this.Selected = selected;
}
}
}

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace AntDesign.TableModels
{
public class FilterModel<TField> : ITableFilterModel
{
public string FieldName { get; }
public IEnumerable<string> SelectedValues { get; set; }
public IList<TableFilter<TField>> Filters { get; }
public Expression<Func<TField, TField, bool>> OnFilter { get; set; }
private PropertyInfo _propertyInfo;
public FilterModel(PropertyInfo propertyInfo, Expression<Func<TField, TField, bool>> onFilter, IList<TableFilter<TField>> filters)
{
this._propertyInfo = propertyInfo;
this.FieldName = _propertyInfo.Name;
this.OnFilter = onFilter;
this.SelectedValues = filters.Select(x => x.Value.ToString());
this.Filters = filters;
}
public IQueryable<TItem> FilterList<TItem>(IQueryable<TItem> source)
{
if (Filters?.Any() != true)
{
return source;
}
var sourceExpression = Expression.Parameter(typeof(TItem));
var propertyExpression = Expression.Property(sourceExpression, _propertyInfo);
Expression invocationExpression = Expression.Invoke((Expression<Func<bool>>)(() => false));
foreach (var filter in Filters)
{
invocationExpression = Expression.OrElse(invocationExpression, Expression.Invoke(OnFilter, Expression.Constant(filter.Value), propertyExpression));
}
var lambda = Expression.Lambda<Func<TItem, bool>>(invocationExpression, sourceExpression);
return source.Where(lambda);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace AntDesign.TableModels
{
public interface ITableFilterModel
{
public string FieldName { get; }
public IEnumerable<string> SelectedValues { get; }
public IQueryable<TItem> FilterList<TItem>(IQueryable<TItem> source);
}
}

View File

@ -12,6 +12,8 @@ namespace AntDesign.TableModels
public IList<ITableSortModel> SortModel { get; private set; }
public IList<ITableFilterModel> FilterModel { get; private set; }
[JsonIgnore]
public IQueryable<TItem> QueryableLambda { get; private set; }
@ -20,6 +22,7 @@ namespace AntDesign.TableModels
this.PageSize = pageSize;
this.PageIndex = pageIndex;
this.SortModel = new List<ITableSortModel>();
this.FilterModel = new List<ITableFilterModel>();
}
internal void AddSortModel(ITableSortModel model)
@ -27,6 +30,11 @@ namespace AntDesign.TableModels
SortModel.Add(model);
}
internal void AddFilterModel(ITableFilterModel model)
{
FilterModel.Add(model);
}
internal void SetQueryableLambda(IQueryable<TItem> query)
{
this.QueryableLambda = query;

View File

@ -8,10 +8,6 @@ namespace AntDesign.TableModels
{
public class SortModel<TField> : ITableSortModel, IComparer<TField>
{
private PropertyInfo _propertyInfo;
private readonly Func<TField, TField, int> _comparer;
public int Priority { get; }
public string FieldName { get; }
@ -20,14 +16,17 @@ namespace AntDesign.TableModels
SortDirection ITableSortModel.SortDirection => _sortDirection;
private readonly PropertyInfo _propertyInfo;
private readonly Func<TField, TField, int> _comparer;
private SortDirection _sortDirection;
public SortModel(PropertyInfo propertyInfo, int priority, SortDirection defaultSortOrder, Func<TField, TField, int> comparer)
{
this._propertyInfo = propertyInfo;
_comparer = comparer;
this.Priority = priority;
this.FieldName = propertyInfo?.Name;
this._propertyInfo = propertyInfo;
this._comparer = comparer;
this._sortDirection = defaultSortOrder ?? SortDirection.None;
}

View File

@ -1,36 +1,66 @@
<Table DataSource="data" OnChange="OnChange" TItem="Column">
<Column TData="string"
DataIndex="@nameof(context.Name)"
SorterCompare="@((a,b)=> a.Length - b.Length)"
SortDirections="new[] { SortDirection.Descending }"/>
<Column TData="int"
DataIndex="@nameof(context.Age)"
DefaultSortOrder="@SortDirection.Descending"
SorterCompare="@((a,b)=> a - b)" />
<Column TData="string"
DataIndex="@nameof(context.Address)"
SorterCompare="@((a,b)=> a.Length - b.Length)"
SortDirections="new[] { SortDirection.Descending, SortDirection.Ascending }"
/>
<Table DataSource="data" OnChange="OnChange" TItem="Data">
<Column TData="string"
@bind-Field="context.Name"
SorterCompare="@((a,b)=> a.Length - b.Length)"
SortDirections="new[] { SortDirection.Descending }"
Filters="nameFilters"
OnFilter="((value,name)=>name.StartsWith(value))" />
<Column TData="int"
DataIndex="@nameof(context.Age)"
DefaultSortOrder="@SortDirection.Descending"
SorterCompare="@((a,b)=> a - b)" />
<Column TData="string"
@bind-Field="context.Address"
SorterCompare="@((a,b)=> a.Length - b.Length)"
SortDirections="new[] { SortDirection.Descending, SortDirection.Ascending }"
Filters="addressFilters"
FilterMultiple="false"
OnFilter="(value,address)=>address.StartsWith(value)" />
</Table>
@using AntDesign.TableModels;
@using System.Text.Json;
@code {
record Column(string Name, int Age, string Address);
Column[] data =
class Data
{
public Data(string name, int age, string address)
{
Name = name;
Age = age;
Address = address;
}
public string Name { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
Data[] data =
{
new("John Brown",32,"New York No. 1 Lake Park"),
new("Jim Green",42, "London No. 1 Lake Park"),
new("Joe Black", 32,"Sidney No. 1 Lake Park"),
new("Jim Red",32,"London No. 2 Lake Park"),
};
void OnChange(QueryModel<Column> query)
public TableFilter<string>[] nameFilters = new TableFilter<string>[] {
new() { Text = "Joe", Value = "Joe" },
new() { Text = "Jim", Value = "Jim" },
};
public TableFilter<string>[] addressFilters = new TableFilter<string>[] {
new() { Text = "London", Value = "London" },
new() { Text = "New York", Value = "New York" },
};
void OnChange(QueryModel<Data> query)
{
Console.WriteLine(JsonSerializer.Serialize(query));
}
}
void OnRowClick(RowData<Data> row)
{
Console.WriteLine($"row {row.Data.Name} was clicked");
}
}

View File

@ -7,8 +7,6 @@ title:
## zh-CN
*⚠注意: 筛选功能 将在下个版本中提供*
对某一列数据进行筛选,使用列的 `filters` 属性来指定需要筛选菜单的列,`onFilter` 用于筛选当前数据,`filterMultiple` 用于指定多选和单选。
对某一列数据进行排序,通过指定列的 `sorter` 函数即可启动排序按钮。`sorter: function(rowA, rowB) { ... }` rowA、rowB 为比较的两个行数据。
@ -19,8 +17,6 @@ title:
## en-US
*⚠Notice: `filters` will be provided in the next release.*
Use `filters` to generate filter menu in columns, `onFilter` to determine filtered result, and `filterMultiple` to indicate whether it's multiple or single selection.
Uses `defaultFilteredValue` to make a column filtered by default.