Skip to content

Commit 4cdadc7

Browse files
dvoituronCopilot
andauthored
[dev-v5] Lists - Add SelectedItemsExpression and SelectedItemExpression parameters (#4759)
* Add SelectedItemsExpression parameter to FluentSelect component and corresponding tests Co-authored-by: Copilot <copilot@github.com> * Add SelectedItemExpression parameter to FluentAutocomplete component and corresponding tests Co-authored-by: Copilot <copilot@github.com> --------- Co-authored-by: Copilot <copilot@github.com>
1 parent 99fc0ce commit 4cdadc7

4 files changed

Lines changed: 162 additions & 0 deletions

File tree

src/Core/Components/List/FluentAutocomplete.razor.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// This file is licensed to you under the MIT License.
33
// ------------------------------------------------------------------------
44

5+
using System.Linq.Expressions;
56
using Microsoft.AspNetCore.Components;
67
using Microsoft.AspNetCore.Components.Web;
78
using Microsoft.FluentUI.AspNetCore.Components.Utilities;
@@ -36,6 +37,8 @@ public FluentAutocomplete(LibraryConfiguration configuration) : base(configurati
3637
// Default values
3738
Id = Identifier.NewId();
3839

40+
SelectedItemExpression = () => SelectedItem;
41+
3942
// Set default value: if `Width` is not already set (not null),
4043
Width ??= "160px";
4144

@@ -187,6 +190,16 @@ public override IEnumerable<TOption> SelectedItems
187190
[Parameter]
188191
public EventCallback<TOption> SelectedItemChanged { get; set; }
189192

193+
/// <summary>
194+
/// Gets or sets an expression that identifies the bound <see cref="SelectedItem"/> value.
195+
/// This is required to enable the <c>@bind-SelectedItem</c> syntax (Razor automatically
196+
/// supplies it). When using manual one-way binding through <see cref="SelectedItem"/>
197+
/// and <see cref="SelectedItemChanged"/>, providing this expression is optional: a
198+
/// default expression pointing to <see cref="SelectedItem"/> is set in the constructor.
199+
/// </summary>
200+
[Parameter]
201+
public Expression<Func<TOption?>>? SelectedItemExpression { get; set; }
202+
190203
/// <summary>
191204
/// Gets a value indicating whether the number of selected options has reached the maximum defined by <see cref="MaximumSelectedOptions"/>.
192205
/// </summary>

src/Core/Components/List/FluentListBase.razor.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ------------------------------------------------------------------------
44

55
using System.Diagnostics.CodeAnalysis;
6+
using System.Linq.Expressions;
67
using Microsoft.AspNetCore.Components;
78
using Microsoft.AspNetCore.Components.Web;
89
using Microsoft.FluentUI.AspNetCore.Components.Extensions;
@@ -22,6 +23,8 @@ public abstract partial class FluentListBase<TOption, TValue> : FluentInputBase<
2223
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(DropdownEventArgs))]
2324
protected FluentListBase(LibraryConfiguration configuration) : base(configuration)
2425
{
26+
SelectedItemsExpression = () => SelectedItems;
27+
2528
// If TOption implements IEqualityComparer<TOption> and exposes a public parameterless
2629
// constructor, use a new instance of TOption as the default OptionSelectedComparer.
2730
if (OptionSelectedComparer is null && _defaultOptionSelectedComparer.Value is { } defaultComparer)
@@ -85,6 +88,16 @@ protected FluentListBase(LibraryConfiguration configuration) : base(configuratio
8588
[Parameter]
8689
public virtual EventCallback<IEnumerable<TOption>> SelectedItemsChanged { get; set; }
8790

91+
/// <summary>
92+
/// Gets or sets an expression that identifies the bound <see cref="SelectedItems"/> value.
93+
/// This is required to enable the <c>@bind-SelectedItems</c> syntax (Razor automatically
94+
/// supplies it). When using manual one-way binding through <see cref="SelectedItems"/>
95+
/// and <see cref="SelectedItemsChanged"/>, providing this expression is optional: a
96+
/// default expression pointing to <see cref="SelectedItems"/> is set in the constructor.
97+
/// </summary>
98+
[Parameter]
99+
public virtual Expression<Func<IEnumerable<TOption>>>? SelectedItemsExpression { get; set; }
100+
88101
/// <summary>
89102
/// Gets or sets the template for the <see cref="FluentListBase{TOption, TValue}.Items"/> items.
90103
/// </summary>

tests/Core/Components/List/FluentAutocompleteTests.razor

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,72 @@
11201120
Assert.Null(component.OptionSelectedComparer);
11211121
}
11221122

1123+
[Fact]
1124+
public void FluentAutocomplete_SelectedItemExpression_DefaultIsNotNull()
1125+
{
1126+
string? selectedItem = "Two";
1127+
1128+
// Arrange - Render without supplying SelectedItemExpression (manual one-way binding scenario)
1129+
var cut = Render(@<FluentAutocomplete TOption="string"
1130+
TValue="string"
1131+
Multiple="false"
1132+
Items="@Digits"
1133+
SelectedItem="@selectedItem"
1134+
SelectedItemChanged="@((string v) => selectedItem = v)" />);
1135+
1136+
// Assert - The constructor sets a default expression so consumers using manual
1137+
// one-way binding (SelectedItem + SelectedItemChanged only) still get a non-null expression.
1138+
var instance = cut.FindComponent<FluentAutocomplete<string, string>>().Instance;
1139+
Assert.NotNull(instance.SelectedItemExpression);
1140+
Assert.Equal("Two", instance.SelectedItemExpression!.Compile().Invoke());
1141+
}
1142+
1143+
[Fact]
1144+
public void FluentAutocomplete_SelectedItemExpression_ManualOneWayBinding()
1145+
{
1146+
// This test compiles ONLY because SelectedItemExpression exists as a [Parameter].
1147+
// Razor's @bind-SelectedItem:get/:set syntax emits SelectedItem, SelectedItemChanged
1148+
// AND SelectedItemExpression on the component.
1149+
string? selectedItem = "One";
1150+
1151+
// Arrange
1152+
var cut = Render(@<FluentAutocomplete TOption="string"
1153+
TValue="string"
1154+
Multiple="false"
1155+
Items="@Digits"
1156+
@bind-SelectedItem:get="selectedItem"
1157+
@bind-SelectedItem:set="@((string v) => { selectedItem = v; return Task.CompletedTask; })" />);
1158+
1159+
// Assert - The component's SelectedItemExpression is supplied by Razor and reflects the bound field
1160+
var instance = cut.FindComponent<FluentAutocomplete<string, string>>().Instance;
1161+
Assert.NotNull(instance.SelectedItemExpression);
1162+
Assert.Equal("One", instance.SelectedItemExpression!.Compile().Invoke());
1163+
1164+
// The badge displays the initial selection
1165+
var badge = cut.Find("span.fluent-badge[alone]");
1166+
Assert.Equal("One", badge.GetAttribute("title"));
1167+
}
1168+
1169+
[Fact]
1170+
public void FluentAutocomplete_SelectedItemExpression_ExplicitlyProvided()
1171+
{
1172+
string? selectedItem = "Three";
1173+
System.Linq.Expressions.Expression<Func<string?>> expression = () => selectedItem;
1174+
1175+
// Arrange - Explicitly provide SelectedItemExpression
1176+
var cut = Render(@<FluentAutocomplete TOption="string"
1177+
TValue="string"
1178+
Multiple="false"
1179+
Items="@Digits"
1180+
SelectedItem="@selectedItem"
1181+
SelectedItemChanged="@((string v) => selectedItem = v)"
1182+
SelectedItemExpression="@expression" />);
1183+
1184+
// Assert - The supplied expression overrides the default set in the constructor
1185+
var instance = cut.FindComponent<FluentAutocomplete<string, string>>().Instance;
1186+
Assert.Same(expression, instance.SelectedItemExpression);
1187+
}
1188+
11231189
public class Person : IEqualityComparer<Person>
11241190
{
11251191
public int Id { get; set; }

tests/Core/Components/List/FluentSelectTests.razor

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,6 +519,76 @@
519519
Assert.Equal(MyDigitsEnum.Three, value);
520520
}
521521

522+
[Fact]
523+
public void FluentSelect_SelectedItemsExpression_DefaultIsNotNull()
524+
{
525+
IEnumerable<string> selectedItems = ["Two"];
526+
527+
// Arrange - Render without supplying SelectedItemsExpression (manual one-way binding scenario)
528+
var cut = Render(@<FluentSelect TOption="string"
529+
TValue="string"
530+
Multiple="true"
531+
Items="@Digits"
532+
SelectedItems="@selectedItems"
533+
SelectedItemsChanged="@((IEnumerable<string> v) => selectedItems = v)" />);
534+
535+
// Assert - The constructor sets a default expression so consumers using manual
536+
// one-way binding (SelectedItems + SelectedItemsChanged only) still get a non-null expression.
537+
var instance = cut.FindComponent<FluentSelect<string, string>>().Instance;
538+
Assert.NotNull(instance.SelectedItemsExpression);
539+
Assert.Equal(new[] { "Two" }, instance.SelectedItemsExpression!.Compile().Invoke());
540+
}
541+
542+
[Fact]
543+
public async Task FluentSelect_SelectedItemsExpression_ManualOneWayBinding()
544+
{
545+
// This test compiles ONLY because SelectedItemsExpression exists as a [Parameter].
546+
// Razor's @bind-SelectedItems:get/:set syntax emits SelectedItems, SelectedItemsChanged
547+
// AND SelectedItemsExpression on the component.
548+
IEnumerable<string> selectedItems = ["One"];
549+
550+
// Arrange
551+
var cut = Render(@<FluentSelect TOption="string"
552+
TValue="string"
553+
Multiple="true"
554+
Items="@Digits"
555+
@bind-SelectedItems:get="selectedItems"
556+
@bind-SelectedItems:set="@((IEnumerable<string> v) => { selectedItems = v; return Task.CompletedTask; })" />);
557+
558+
var ids = cut.FindAll("fluent-option")
559+
.Where(i => i.GetAttribute("value") == "One" || i.GetAttribute("value") == "Two")
560+
.Select(i => i.GetAttribute("id"));
561+
562+
// Act
563+
await cut.FindComponent<FluentSelect<string?, string?>>().Instance.OnDropdownChangeHandlerAsync(new DropdownEventArgs
564+
{
565+
SelectedOptions = string.Join(';', ids),
566+
});
567+
568+
// Assert
569+
Assert.Equal(new[] { "One", "Two" }, selectedItems);
570+
}
571+
572+
[Fact]
573+
public void FluentSelect_SelectedItemsExpression_ExplicitlyProvided()
574+
{
575+
IEnumerable<string> selectedItems = ["Three"];
576+
System.Linq.Expressions.Expression<Func<IEnumerable<string>>> expression = () => selectedItems;
577+
578+
// Arrange - Explicitly provide SelectedItemsExpression
579+
var cut = Render(@<FluentSelect TOption="string"
580+
TValue="string"
581+
Multiple="true"
582+
Items="@Digits"
583+
SelectedItems="@selectedItems"
584+
SelectedItemsChanged="@((IEnumerable<string> v) => selectedItems = v)"
585+
SelectedItemsExpression="@expression" />);
586+
587+
// Assert - The supplied expression overrides the default set in the constructor
588+
var instance = cut.FindComponent<FluentSelect<string, string>>().Instance;
589+
Assert.Same(expression, instance.SelectedItemsExpression);
590+
}
591+
522592
public record Person
523593
{
524594
public int Id { get; set; }

0 commit comments

Comments
 (0)