Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
53 changes: 32 additions & 21 deletions src/Core/Components/List/FluentAutocomplete.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ public partial class FluentAutocomplete<TOption, TValue> : FluentListBase<TOptio
private static readonly Icon BadgeCloseIcon = new CoreIcons.Regular.Size20.Dismiss();
private static readonly Icon ClearIcon = new CoreIcons.Regular.Size20.Dismiss();

private readonly EqualityComparer<TValue> ValueComparer = EqualityComparer<TValue>.Default;
private readonly EqualityComparer<TOption> OptionComparer = EqualityComparer<TOption>.Default;

private string? _textInput;
private bool _isOpen;
private bool _inProgress;
private TValue? _previousValue;

// List of items used in the internally filtered listbox
private List<TOption> _internalFilteredItems = [];
Expand Down Expand Up @@ -218,34 +222,21 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}

/// <summary />
public override async Task SetParametersAsync(ParameterView parameters)
protected override async Task OnParametersSetAsync()
{
// Check if SelectedItem is being supplied and has changed
if (parameters.TryGetValue<TOption?>(nameof(SelectedItem), out var newSelectedItem))
// This part of code cannot be moved to SetParametersAsync because we are invoking `OnSetValue` which is async,
// and SetParametersAsync doesn't allow awaiting other async calls inside it.
if (!ValueComparer.Equals(Value, _previousValue) && OnSetValue.HasDelegate)
{
var comparer = OptionSelectedComparer ?? EqualityComparer<TOption>.Default;
var currentSelectedItem = _internalSelectedItem;

if (!comparer.Equals(newSelectedItem, currentSelectedItem))
{
// Sync _internalSelectedItems with the new value
_internalSelectedItems = newSelectedItem is not null ? [newSelectedItem] : [];
SelectedItem = newSelectedItem;
}
}
_previousValue = Value;

// Check if Value is being supplied and has changed (e.g. set by the developer during initialization).
// If so, resolve the associated option via the OnSetValue callback and synchronize the selection.
if (parameters.TryGetValue<TValue?>(nameof(Value), out var newValue))
{
var valueComparer = EqualityComparer<TValue>.Default;
var currentValue = GetOptionValue(_internalSelectedItem);

if (!valueComparer.Equals(newValue, currentValue) && OnSetValue.HasDelegate)
if (!ValueComparer.Equals(Value, currentValue))
{
var args = new SetValueEventArgs<TOption, TValue>
{
Value = newValue,
Value = Value,
};

await OnSetValue.InvokeAsync(args);
Expand All @@ -263,6 +254,26 @@ public override async Task SetParametersAsync(ParameterView parameters)
}
}

await base.OnParametersSetAsync();
}

/// <summary />
public override async Task SetParametersAsync(ParameterView parameters)
{
// Check if SelectedItem is being supplied and has changed
if (parameters.TryGetValue<TOption?>(nameof(SelectedItem), out var newSelectedItem))
{
var comparer = OptionSelectedComparer ?? OptionComparer;
var currentSelectedItem = _internalSelectedItem;

if (!comparer.Equals(newSelectedItem, currentSelectedItem))
{
// Sync _internalSelectedItems with the new value
_internalSelectedItems = newSelectedItem is not null ? [newSelectedItem] : [];
SelectedItem = newSelectedItem;
}
}

await base.SetParametersAsync(parameters);
}

Expand All @@ -271,7 +282,7 @@ public override async Task SetParametersAsync(ParameterView parameters)
/// </summary>
private async Task InternalSelectedItemsChangedHandlerAsync(IEnumerable<TOption> items)
{
var comparer = OptionSelectedComparer ?? EqualityComparer<TOption>.Default;
var comparer = OptionSelectedComparer ?? OptionComparer;
var itemsToAdd = items.Where(item => !_internalSelectedItems.Contains(item, comparer)).ToList();
var itemsToRemove = _internalFilteredItems.Where(item => !items.Contains(item, comparer)).ToList();

Expand Down
65 changes: 65 additions & 0 deletions tests/Core/Components/List/FluentAutocompleteTests.razor
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,71 @@
Assert.Empty(cut.FindAll("span.fluent-badge"));
}

[Fact]
public void FluentAutocomplete_OnSetValue_InitialBoundValue_SelectsItemOnFirstRender()
{
// Arrange - Bind Value with an initial value BEFORE the component renders
SetValueEventArgs<string, string>? receivedArgs = null;
var value = "Three";

// Act - Render the component; OnParametersSetAsync should invoke OnSetValue to resolve the initial Value
var cut = Render(@<FluentAutocomplete Id="my-list"
TOption="string"
TValue="string"
Multiple="false"
Items="@Digits"
@bind-Value="@value"
OnSetValue="@(args => { receivedArgs = args; args.Item = Digits.FirstOrDefault(d => d == args.Value); })" />);

// Assert - OnSetValue was invoked with the initial Value
Assert.NotNull(receivedArgs);
Assert.Equal("Three", receivedArgs!.Value);

// Assert - The resolved item is selected and displayed as a badge on the first render
var component = cut.FindComponent<FluentAutocomplete<string, string>>().Instance;
Assert.Equal("Three", component.SelectedItem);
Assert.Single(component.SelectedItems);
Assert.Contains("Three", component.SelectedItems);

var badge = cut.Find("span.fluent-badge[alone]");
Assert.Equal("Three", badge.GetAttribute("title"));
}

[Fact]
public async Task FluentAutocomplete_OnSetValue_AsyncHandler_SelectsResolvedItem()
{
// Arrange - OnSetValue is an async handler; bind an initial Value before the first render
var value = "Six";

var cut = Render(@<FluentAutocomplete Id="my-list"
TOption="string"
TValue="string"
Multiple="false"
Items="@Digits"
@bind-Value="@value"
OnSetValue="@(async args => { await Task.Yield(); args.Item = Digits.FirstOrDefault(d => d == args.Value); })" />);

// Assert - The async handler resolved the initial Value and selected the matching item
var component = cut.FindComponent<FluentAutocomplete<string, string>>().Instance;
cut.WaitForAssertion(() => Assert.Equal("Six", cut.Find("span.fluent-badge[alone]").GetAttribute("title")));
Assert.Equal("Six", component.SelectedItem);
Assert.Single(component.SelectedItems);
Assert.Contains("Six", component.SelectedItems);

// Act - Change the Value externally to validate that the async handler also runs after render
var newParameters = ParameterView.FromDictionary(new Dictionary<string, object?>
{
{ nameof(FluentAutocomplete<string, string>.Value), "Two" }
});
await cut.InvokeAsync(() => component.SetParametersAsync(newParameters));

// Assert - The async handler resolved the new Value and the selection was updated
cut.WaitForAssertion(() => Assert.Equal("Two", cut.Find("span.fluent-badge[alone]").GetAttribute("title")));
Assert.Equal("Two", component.SelectedItem);
Assert.Single(component.SelectedItems);
Assert.Contains("Two", component.SelectedItems);
}

private void OnOptionsSearch(OptionsSearchEventArgs<string> e)
{
e.Items = Digits.Where(i => i.StartsWith(e.Text, StringComparison.OrdinalIgnoreCase))
Expand Down
Loading