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
108 changes: 108 additions & 0 deletions examples/Demo/Shared/Pages/Search/Examples/SearchImmediate.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
ο»Ώ<FluentStack Orientation="Orientation.Vertical" VerticalGap="10">

@* Immediate Delay *@
<FluentNumberField @bind-Value="_immediateDelay"
TValue="int"
Label="Immediate Delay"
Placeholder="Delay"
Min="0"
Max="2000"
Step="100" />

@* Search Box *@
<FluentSearch @bind-Value="_searchValue"
@bind-Value:after="OnSearch"
Immediate="true"
ImmediateDelay="_immediateDelay"
Placeholder="Search for name" />

@* Search Results *@
<p>You searched for: @_searchValue</p>
<FluentListbox aria-label="search results"
TOption="string"
Items=@_searchResults
SelectedOptionChanged="@(e => _searchValue = (e != _defaultResultsText ? e : string.Empty))" />
</FluentStack>

@code {
private string? _searchValue;
private int _immediateDelay;

private const string _defaultResultsText = "No results";
private List<string> _searchResults = DefaultResults();

private static List<string> DefaultResults() => new() { _defaultResultsText };

private void OnSearch()
{
if (!string.IsNullOrWhiteSpace(_searchValue))
{
// You can also call an API here if the list is not local.
var results = searchData
.Where(str => str.Contains(_searchValue, StringComparison.OrdinalIgnoreCase))
.Select(str => str)
.ToList();

_searchResults = results.Any() ? results : DefaultResults();
}
else
{
_searchResults = DefaultResults();
}
}

//This component is made for a lot of data. You can copy and paste a list with 6000 entries here https://sharetext.me/vfknowohwl"
private List<string> searchData = new()
{
"Alabama",
"Alaska",
"Arizona",
"Arkansas",
"California",
"Colorado",
"Connecticut",
"Delaware",
"Florida",
"Georgia",
"Hawaii",
"Idaho",
"Illinois",
"Indiana",
"Iowa",
"Kansas",
"Kentucky",
"Louisiana",
"Maine",
"Maryland",
"Massachussets",
"Michigain",
"Minnesota",
"Mississippi",
"Missouri",
"Montana",
"Nebraska",
"Nevada",
"New Hampshire",
"New Jersey",
"New Mexico",
"New York",
"North Carolina",
"North Dakota",
"Ohio",
"Oklahoma",
"Oregon",
"Pennsylvania",
"Rhode Island",
"South Carolina",
"South Dakota",
"Texas",
"Tennessee",
"Utah",
"Vermont",
"Virginia",
"Washington",
"Wisconsin",
"West Virginia",
"Wyoming"
};
}
2 changes: 2 additions & 0 deletions examples/Demo/Shared/Pages/Search/SearchPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@

<DemoSection Title="Interactive with debounce" Component="@typeof(SearchInteractiveWithDebounce)"></DemoSection>

<DemoSection Title="Immediate (with and without debounce)" Component="@typeof(SearchImmediate)"></DemoSection>

<DemoSection Title="States" Component="@typeof(SearchStates)"></DemoSection>

<DemoSection Title="Icons" Component="@typeof(SearchIcons)"></DemoSection>
Expand Down
5 changes: 3 additions & 2 deletions src/Core/Components/Base/FluentInputBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ protected async Task SetCurrentValueAsync(TValue? value)
Value = value;
if (ValueChanged.HasDelegate)
{
await ValueChanged.InvokeAsync(value);
// Thread Safety: Force `ValueChanged` to be re-associated with the Dispatcher, prior to invokation.
await InvokeAsync(async () => await ValueChanged.InvokeAsync(value));
}
if (FieldBound)
{
Expand Down Expand Up @@ -450,7 +451,7 @@ void IDisposable.Dispose()
EditContext.OnValidationStateChanged -= _validationStateChangedHandler;
}

_timerCancellationTokenSource.Dispose();
_debouncer.Dispose();

Dispose(disposing: true);
}
Expand Down
51 changes: 10 additions & 41 deletions src/Core/Components/Base/FluentInputBaseHandlers.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.FluentUI.AspNetCore.Components.Utilities;

namespace Microsoft.FluentUI.AspNetCore.Components;

public partial class FluentInputBase<TValue>
{
private PeriodicTimer? _timerForImmediate;
private CancellationTokenSource _timerCancellationTokenSource = new();
private readonly Debouncer _debouncer = new();

/// <summary>
/// Change the content of this input field when the user write text (based on 'OnInput' HTML event).
Expand Down Expand Up @@ -58,48 +58,17 @@ protected virtual async Task ChangeHandlerAsync(ChangeEventArgs e)
/// <returns></returns>
protected virtual async Task InputHandlerAsync(ChangeEventArgs e) // TODO: To update in all Input fields
{
if (Immediate)
if (!Immediate)
{
// Raise ChangeHandler after a delay
if (ImmediateDelay > 0)
{
_timerForImmediate = GetNewPeriodicTimer(ImmediateDelay);

while (await _timerForImmediate.WaitForNextTickAsync(_timerCancellationTokenSource.Token))
{
await ChangeHandlerAsync(e);
_timerCancellationTokenSource.Cancel();
}
}
// Raise ChangeHandler immediately
else
{
// Cancel a potential existing object
_timerForImmediate?.Dispose();
_timerForImmediate = null;

await ChangeHandlerAsync(e);
}
return;
}

// Cancel the previous Timer (if existing)
// And create a new Timer with a new CancellationToken
PeriodicTimer GetNewPeriodicTimer(int delay)
if (ImmediateDelay > 0)
{
_timerCancellationTokenSource.Cancel();

if (_timerForImmediate is not null)
{
_timerForImmediate.Dispose();
_timerForImmediate = null;
}

_timerForImmediate = new PeriodicTimer(TimeSpan.FromMilliseconds(delay));

_timerCancellationTokenSource.Dispose();
_timerCancellationTokenSource = new CancellationTokenSource();

return _timerForImmediate;
await _debouncer.DebounceAsync(ImmediateDelay, async () => await ChangeHandlerAsync(e));
}
else
{
await ChangeHandlerAsync(e);
}
}
}