feat(module: select): add LabelProperty, ValueProperty and DisabledPredicate as expression-style api (#3569)

* fix(module: select):add parameter for SelectOption component which should be used when SelectOption was created directly and TItem is different with TItemValue

* feat(module: select): add parameter that support use delegate to set option label and value

* rename XXXGetter to OptionXXXExpression

* rename OptionXXXExpression to OptionXXXProperty

* refactor

* fix tests

* fix test

---------

Co-authored-by: James Yeung <shunjiey@hotmail.com>
This commit is contained in:
MarvelTiter_yaoqinglin 2023-12-25 23:23:04 +08:00 коммит произвёл GitHub
Родитель 157cc3974d
Коммит 4451315af1
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 201 добавлений и 19 удалений

Просмотреть файл

@ -18,6 +18,11 @@ using OneOf;
namespace AntDesign
{
#if NET6_0_OR_GREATER
[CascadingTypeParameter(nameof(TItem))]
[CascadingTypeParameter(nameof(TItemValue))]
#endif
public partial class Select<TItemValue, TItem> : SelectBase<TItemValue, TItem>
{
#region Parameters
@ -309,6 +314,20 @@ namespace AntDesign
}
}
/// <summary>
/// Specifies the label property in the option object. If use this property, should not use <see cref="LabelName"/>
/// </summary>
[Parameter] public Func<TItem, string> LabelProperty { get => _getLabel; set => _getLabel = value; }
/// <summary>
/// Specifies the value property in the option object. If use this property, should not use <see cref="ValueName"/>
/// </summary>
[Parameter] public Func<TItem, TItemValue> ValueProperty { get => _getValue; set => _getValue = value; }
/// <summary>
/// Specifies predicate for disabled options
/// </summary>
[Parameter] public Func<TItem, bool> DisabledPredicate { get => _getDisabled; set => _getDisabled = value; }
/// <summary>
/// Used when Mode = default - The value is used during initialization and when pressing the Reset button within Forms.
/// </summary>
@ -402,7 +421,7 @@ namespace AntDesign
protected override void OnInitialized()
{
if (SelectOptions == null && typeof(TItemValue) != typeof(TItem) && string.IsNullOrWhiteSpace(ValueName))
if (SelectOptions == null && typeof(TItemValue) != typeof(TItem) && ValueProperty == null && string.IsNullOrWhiteSpace(ValueName))
{
throw new ArgumentNullException(nameof(ValueName));
}
@ -688,7 +707,7 @@ namespace AntDesign
isSelected = _selectedValues.Contains(value);
}
if (!string.IsNullOrWhiteSpace(DisabledName))
if (_getDisabled != default)
disabled = _getDisabled(item);
if (!string.IsNullOrWhiteSpace(GroupName))

Просмотреть файл

@ -65,7 +65,11 @@ namespace AntDesign
/// The parameter should only be used if the SelectOption was created directly.
/// </summary>
[Parameter] public TItemValue Value { get; set; }
/// <summary>
/// Item of the SelectOption
/// The parameter should only be used if the SelectOption was created directly.
/// </summary>
[Parameter] public TItem Item { get; set; }
#endregion
# region Properties
@ -196,7 +200,7 @@ namespace AntDesign
IsDisabled = Disabled,
GroupName = _groupName,
Value = Value,
Item = THelper.ChangeType<TItem>(Value, CultureInfo.CurrentCulture),
Item = Item ?? THelper.ChangeType<TItem>(Value, CultureInfo.CurrentCulture),
ChildComponent = this
};

Просмотреть файл

@ -1,13 +1,10 @@
<Select TItem="Person"
TItemValue="string"
DataSource="@_list"
<Select DataSource="@_list"
@bind-Value="@_selectedValue1"
DefaultValue="@("lucy")"
ValueName="@nameof(Person.Value)"
LabelName="@nameof(Person.Name)"
ValueProperty="c=>c.Value"
LabelProperty="c=>c.Name"
DisabledName="@nameof(Person.IsDisabled)"
Style="width:120px"
OnSelectedItemChanged="OnSelectedItemChangedHandler">
Style="width:120px">
</Select>
<Select @bind-Value="@_selectedValue2"
DefaultValue="@("lucy")"
@ -16,7 +13,7 @@
TItem="string"
Disabled>
<SelectOptions>
<SelectOption TItemValue="string" TItem="string" Value="@("lucy")" Label="Lucy" />
<SelectOption Value="@("lucy")" Label="Lucy" />
</SelectOptions>
</Select>
<Select @bind-Value="@_selectedValue3"
@ -26,7 +23,7 @@
TItem="string"
Loading>
<SelectOptions>
<SelectOption TItemValue="string" TItem="string" Value="@("lucy")" Label="Lucy" />
<SelectOption Value="@("lucy")" Label="Lucy" />
</SelectOptions>
</Select>
<Select DataSource="@_list"
@ -46,13 +43,26 @@
Placeholder="Choose"
OnSelectedItemChanged="@((personName) => Console.WriteLine($"selectedItem:{personName},selectedValue:{_selectedValue5}"))">
</Select>
<Select DataSource="@_dict"
@bind-Value="@_selectedValue6"
LabelProperty="c=>c.Key"
ValueProperty="c=>c.Value"
DisabledPredicate="@(c=>c.Key == "Disabled")"
Style="width: 120px;"
Placeholder="Dictionary options">
</Select>
@code
{
List<Person> _list;
List<string> _personNames;
Dictionary<string, string> _dict;
string _selectedValue1, _selectedValue2, _selectedValue3;
string _selectedValue4 = "lucy";
string _selectedValue5 = "Lucy";
string _selectedValue6 = "Lucy";
class Person
{
public string Value { get; set; }
@ -69,7 +79,10 @@
new Person {Value = "disabled", Name = "Disabled", IsDisabled = true},
new Person {Value = "yaoming", Name = "YaoMing"}
};
_personNames = new List<string> { "Jack", "Lucy", "YaoMing" };
_dict = _list.ToDictionary(c => c.Name, c => c.Value);
}
private void OnSelectedItemChangedHandler(Person value)

Просмотреть файл

@ -33,6 +33,7 @@ Select component to select value from options.
| DefaultValues | When `Mode = multiple` \| `tags` - The values are used during initialization and when pressing the Reset button within Forms. | IEnumerable&lt;TItemValues> | - | |
| Disabled | Whether the Select component is disabled. | bool | false | |
| DisabledName | The name of the property to be used as a disabled indicator. | string | | |
| DisabledPredicate | Specifies predicate for disabled options | - | - |
| DropdownMatchSelectWidth | Will match drowdown width: <br/>- for boolean: `true` - with widest item in the dropdown list <br/> - for string: with value (e.g.: `256px`). | OneOf<bool, string> | true | |
| DropdownMaxWidth | Will not allow dropdown width to grow above stated in here value (eg. "768px"). | string | "auto" | |
| DropdownRender | Customize dropdown content. | Renderfragment | - | |
@ -45,6 +46,7 @@ Select component to select value from options.
| LabelInValue | Whether to embed label in value, turn the format of value from `TItemValue` to string (JSON) e.g. { "value": `TItemValue`, "label": "`Label value`" } | bool | false | |
| LabelName | The name of the property to be used for the label. | string | | |
| LabelTemplate | Is used to customize the label style. | RenderFragment&lt;TItem> | | |
| LabelProperty | Specifies the label property in the option object. | Func<TItem, string> | - |
| Loading | Show loading indicator. You have to write the loading logic on your own. | bool | false | |
| ListboxStyle | custom listbox styles | string | display: flex; flex-direction: column; | |
| MaxTagCount | Max tag count to show. responsive will cost render performance. | int | `ResponsiveTag.Responsive` | - | |
@ -83,6 +85,7 @@ Select component to select value from options.
| ValuesChanged | Used for the two-way binding. | EventCallback&lt;IEnumerable&lt;TItemValue>> | - | |
| ValueName | The name of the property to be used for the value. | string | - | |
| ValueOnClear | When Clear button is pressed, Value will be set to whatever is set in ValueOnClear. | TItemValue | - | 0.11 |
| ValueProperty | Specifies the value property in the option object. | Func<TItem, TItemValue> | - |
### SelectOption props
@ -91,4 +94,5 @@ Select component to select value from options.
| Class | The additional class to option | string | - | |
| Disabled | Disable this option | Boolean | false | |
| Label | Label of Select after selecting this Option | string | - | |
| Value | Value of Select after selecting this Option | TItemValue | - | |
| Value | Value of Select after selecting this Option | TItemValue | - | |
| Item | Item of the option | TItem | - | |

Просмотреть файл

@ -33,6 +33,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| DefaultValues | 当`Mode = multiple` \| `tags` - 在初始化期间和在表单中按下重置按钮时使用这些值. | IEnumerable&lt;TItemValues> | - | |
| Disabled | 是否禁用 | bool | false | |
| DisabledName | 用作禁用指示器的属性名称. | string | | |
| DisabledPredicate | 指定禁用选项的判断条件 | - | - |
| DropdownMatchSelectWidth | 将匹配下拉宽度: <br/>- for boolean: `true` - 下拉列表中最宽的项目 <br/> - for string: with value (e.g.: `256px`). | OneOf<bool, string> | true | |
| DropdownMaxWidth | 不允许下拉菜单的宽度超过此处的值例如“768px”. | string | "auto" | |
| DropdownRender | 自定义下拉框内容 | Renderfragment | - | |
@ -45,6 +46,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| LabelInValue | 是否在 value 中嵌入标签,将 value 的格式从 `TItemValue` 转换为 string (JSON) e.g. { "value": `TItemValue`, "label": "`标签值`" } | bool | false | |
| LabelName | 用于标签的属性名称. | string | | |
| LabelTemplate | 用于自定义标签样式. | RenderFragment&lt;TItem> | | |
| LabelProperty | 指定 option 对象的 label属性 | `Func<TItem, string>` | - |
| Loading | 显示加载指示器。 必须编写加载逻辑. | bool | false | |
| ListboxStyle | 自定义下拉列表样式 | string | display: flex; flex-direction: column; | |
| MaxTagCount | 最多显示多少个 tag响应式模式会对性能产生损耗 | int | `ResponsiveTag.Responsive` | - | |
@ -83,12 +85,14 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
| ValuesChanged | 用于双向绑定(多选). | EventCallback&lt;IEnumerable&lt;TItemValue>> | - | |
| ValueName | 用于值的属性的名称. | string | - | |
| ValueOnClear | 按下清除按钮时,值将设置为 ValueOnClear 中设置的值. | TItemValue | - | 0.11 |
| ValueProperty | 指定 option 对象的 value 属性. | `Func<TItem, TItemValue>` | - |
### SelectOption props
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| Class | Option 器类名 | string | - | |
| Disabled | 是否禁用 | Boolean | false | |
| Label |选择此选项后选择的标签内容 | string | - | |
| Value |选择此选项后的 Select 值| TItemValue | - | |
| Class | Option 类名 | string | - | |
| Disabled | 是否禁用 | Boolean | false | |
| Label | 选择此选项后选择的标签内容 | string | - | |
| Value | 选择此选项后的 Select 值 | TItemValue | - | |
| Item | option 对象 | TItem | - | |

Просмотреть файл

@ -1,5 +1,7 @@
@inherits AntDesignTestBase
@using AntDesign.Core.JsInterop.Modules.Components;
@inherits AntDesignTestBase
@code {
record Person(int Id, string Name);
public record PersonNullable(string? Id, string Name);
class PersonClass
@ -149,6 +151,98 @@
}
/*
[Theory]
[MemberData(nameof(AllowClearWithValueOnClearTheory))]
public void AllowClear_button_behavior_with_ValueOnClear_set_with_DataSource(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? valueOnClear, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, PersonNullable>>(
@<AntDesign.Select DataSource="@dataSource"
LabelName="@nameof(PersonNullable.Name)"
ValueName="@nameof(PersonNullable.Id)"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
ValueOnClear="@valueOnClear"
AllowClear>
</AntDesign.Select>);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
[Theory]
[MemberData(nameof(AllowClearWithValueOnClearTheory))]
public void AllowClear_button_behavior_with_ValueOnClear_set_with_SelectOption(List<PersonNullable> dataSource,
string? initialValue, bool defaultActiveFirstOption, string? valueOnClear, string? expectedValue, string? expectedLabel)
{
string? value = "-1"; //set initial value to a not-possible value
Action<string?> ValueChanged = (v) =>value = v;
var cut = Render<AntDesign.Select<string?, string>>(
@<AntDesign.Select
TItemValue="string?"
TItem="string"
Value="@initialValue"
ValueChanged="@ValueChanged"
DefaultActiveFirstOption="@defaultActiveFirstOption"
ValueOnClear="@valueOnClear"
AllowClear>
<SelectOptions>
@foreach(var item in dataSource)
{
<SelectOption TItemValue="string?" TItem="string" Value="@item.Id" Label="@item.Name" />
}
</SelectOptions>
</AntDesign.Select>
);
//Act
//normally blazor would rerender and in Select.OnParametersSet()
//would load newly set value into the SelectContent, but bUnit does
//not rerender, so it has to be forced. This could probably be fixed
//by forcing StateHasChanged on the Select component, but requires
//investigation if it won't cause multiple re-renders.
cut.Render();
cut.Find("span.ant-select-clear").Click();
if (expectedLabel == string.Empty)
{
cut.Invoking(c => c.Find("span.ant-select-selection-item"))
.Should().Throw<Bunit.ElementNotFoundException>();
}
else
{
var selectContent = cut.Find("span.ant-select-selection-item");
selectContent.TextContent.Trim().Should().Be(expectedLabel);
}
value.Should().Be(expectedValue);
cut.Instance.Value.Should().Be(expectedValue);
}
*/
public static IEnumerable<object[]> AllowClearWithValueOnClearTheory()
{
return new List<object[]>
@ -165,4 +259,48 @@
}
#if NET6_0_OR_GREATER
[Fact]
public async Task Work_With_ValueProperty_and_LabelProperty()
{
JSInterop.SetupVoid("AntDesign.interop.eventHelper.addPreventKeys", _ => true);
JSInterop.SetupVoid("AntDesign.interop.domManipulationHelper.scrollTo", _ => true);
JSInterop.SetupVoid("AntDesign.interop.domManipulationHelper.focus", _ => true);
JSInterop
.Setup<OverlayPosition>("AntDesign.interop.overlayHelper.addOverlayToContainer", _ => true)
.SetResult(new OverlayPosition
{
Bottom = 10,
Left = 10,
Right = 10,
Top = 10,
Placement = Placement.TopLeft,
ZIndex = 100
});
Dictionary<string, int> dict = new()
{
["Hello"] = 1,
["World"] = 2,
["Disabled"] = 3,
};
int value = 1;
var cut = Render<AntDesign.Select<int, KeyValuePair<string, int>>>(@<AntDesign.Select DataSource="@dict" LabelProperty="c=>c.Key" ValueProperty="c=>c.Value" @bind-Value="value" DisabledPredicate="@(c=> c.Key == "Disabled")" />);
cut.Render();
var items = cut.FindAll(".ant-select-item");
var defaultSelection = cut.Find(".ant-select-selection-item").TextContent.Trim();
var overlay = cut.FindComponent<AntDesign.Internal.OverlayTrigger>();
await cut.InvokeAsync(() => overlay.Instance.Show());
cut.FindAll(".ant-select-item-option")[1].Click();
cut.WaitForState(() => value == 2);
defaultSelection.Should().Be("Hello");
value.Should().Be(2);
items.Count.Should().Be(3);
items[2].ClassList.Should().Contain("ant-select-item-option-disabled");
}
#endif
}