From cea5dc85bc9f1dd7b30d4d6064b9c8957d361c42 Mon Sep 17 00:00:00 2001 From: celadaris Date: Thu, 6 Mar 2025 11:20:12 -0600 Subject: [PATCH 01/11] update After testing live, i could still see problems, less when using OnBlurAsync but it was more noticeable in OnValueChanged. These changes address the core issue where the input text was being overwritten by stale data from older asynchronous operations. The dual approach (both C# and JavaScript checks) provides protection against race conditions. Even if one side fails, the other will catch it. --- .../AutoComplete/AutoComplete.razor.cs | 51 +++++++++++++++---- .../AutoComplete/AutoComplete.razor.js | 14 ++++- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index 1d9f4e5e50d..c67c03db06c 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -128,6 +128,10 @@ private async Task OnClickItem(string val) private List Rows => _filterItems ?? [.. Items]; + // In AutoComplete.razor.cs, add a tracking variable: + private string _userCurrentInput = string.Empty; + private SpinLock _spinLock = new SpinLock(false); // false for non-tracking mode + /// /// TriggerFilter method /// @@ -135,9 +139,19 @@ private async Task OnClickItem(string val) [JSInvokable] public override async Task TriggerFilter(string val) { - // Store the current input value to prevent it from being overwritten - var currentInputValue = val; - + // Update the current input with SpinLock protection + bool lockTaken = false; + try + { + _spinLock.Enter(ref lockTaken); + _userCurrentInput = val; + } + finally + { + if (lockTaken) _spinLock.Exit(); + } + + // Process filtering if (OnCustomFilter != null) { var items = await OnCustomFilter(val); @@ -155,19 +169,34 @@ public override async Task TriggerFilter(string val) : Items.Where(s => s.StartsWith(val, comparison)); _filterItems = [.. items]; } - + if (DisplayCount != null) { _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; } - - // Use currentInputValue here instead of potentially stale val - CurrentValue = currentInputValue; - - // Only trigger StateHasChanged if no binding is present - if (!ValueChanged.HasDelegate) + + // Check if still the latest input with SpinLock protection + string latestInput; + lockTaken = false; + try { - StateHasChanged(); + _spinLock.Enter(ref lockTaken); + latestInput = _userCurrentInput; + } + finally + { + if (lockTaken) _spinLock.Exit(); + } + + // Only update CurrentValue if this is still the latest input + if (latestInput == val) + { + CurrentValue = val; + + if (!ValueChanged.HasDelegate) + { + StateHasChanged(); + } } } diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js index 3b7e8557f35..4892883168d 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js @@ -63,8 +63,12 @@ export function init(id, invoke) { filterDuration = 200; } const filterCallback = debounce(async v => { - await invoke.invokeMethodAsync('TriggerFilter', v); - el.classList.remove('is-loading'); + // Check if the input value is still the same + // If not, this is an old operation that should be ignored + if (input.dataset.lastValue === v) { + await invoke.invokeMethodAsync('TriggerFilter', v); + el.classList.remove('is-loading'); + } }, filterDuration); Input.composition(input, v => { @@ -73,6 +77,12 @@ export function init(id, invoke) { } el.classList.add('is-loading'); + + // Store the current input value on the element + // This helps track the latest user input + input.dataset.lastValue = v; + + // Modify the filterCallback to check if the input value has changed filterCallback(v); }); From b52cff91fc3df4d06946e6f27306cac4730e55c5 Mon Sep 17 00:00:00 2001 From: celadaris Date: Thu, 6 Mar 2025 11:39:47 -0600 Subject: [PATCH 02/11] update switched SpinLock to SemaphoreSlim --- .../AutoComplete/AutoComplete.razor.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index c67c03db06c..88ab51185fe 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -4,6 +4,7 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Localization; +using System.Threading; namespace BootstrapBlazor.Components; @@ -128,9 +129,9 @@ private async Task OnClickItem(string val) private List Rows => _filterItems ?? [.. Items]; - // In AutoComplete.razor.cs, add a tracking variable: + // Thread-safe tracking using SemaphoreSlim for async compatibility private string _userCurrentInput = string.Empty; - private SpinLock _spinLock = new SpinLock(false); // false for non-tracking mode + private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); /// /// TriggerFilter method @@ -139,18 +140,17 @@ private async Task OnClickItem(string val) [JSInvokable] public override async Task TriggerFilter(string val) { - // Update the current input with SpinLock protection - bool lockTaken = false; + // Thread-safe update using SemaphoreSlim + await _semaphore.WaitAsync(); try { - _spinLock.Enter(ref lockTaken); _userCurrentInput = val; } finally { - if (lockTaken) _spinLock.Exit(); + _semaphore.Release(); } - + // Process filtering if (OnCustomFilter != null) { @@ -169,30 +169,29 @@ public override async Task TriggerFilter(string val) : Items.Where(s => s.StartsWith(val, comparison)); _filterItems = [.. items]; } - + if (DisplayCount != null) { _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; } - - // Check if still the latest input with SpinLock protection + + // Thread-safe read using SemaphoreSlim + await _semaphore.WaitAsync(); string latestInput; - lockTaken = false; try { - _spinLock.Enter(ref lockTaken); latestInput = _userCurrentInput; } finally { - if (lockTaken) _spinLock.Exit(); + _semaphore.Release(); } - + // Only update CurrentValue if this is still the latest input if (latestInput == val) { CurrentValue = val; - + if (!ValueChanged.HasDelegate) { StateHasChanged(); From 013419e9d6a0c55215db9f9263a65bec6248a587 Mon Sep 17 00:00:00 2001 From: celadaris Date: Thu, 6 Mar 2025 14:52:19 -0600 Subject: [PATCH 03/11] re-design This should work better under bad network conditions --- .../AutoComplete/AutoComplete.razor.cs | 130 ++++++++++-------- .../AutoComplete/AutoComplete.razor.js | 46 +++++-- 2 files changed, 110 insertions(+), 66 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index 88ab51185fe..7585d39bcf2 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -4,7 +4,6 @@ // Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone using Microsoft.Extensions.Localization; -using System.Threading; namespace BootstrapBlazor.Components; @@ -90,6 +89,16 @@ public partial class AutoComplete private List? _filterItems; + /// + /// Tracks the current user input to prevent it from being overwritten + /// + private string _currentUserInput = string.Empty; + + /// + /// Flag to track whether we're handling debounced filtering + /// + private bool _isFiltering = false; + /// /// /// @@ -113,6 +122,12 @@ protected override void OnParametersSet() LoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.LoadingIcon); Items ??= []; + + // Initialize _currentUserInput with current value if it hasn't been set yet + if (string.IsNullOrEmpty(_currentUserInput) && !string.IsNullOrEmpty(CurrentValueAsString)) + { + _currentUserInput = CurrentValueAsString; + } } /// @@ -120,7 +135,10 @@ protected override void OnParametersSet() /// private async Task OnClickItem(string val) { + // Update both the CurrentValue and _currentUserInput when an item is clicked + _currentUserInput = val; CurrentValue = val; + if (OnSelectedItemChanged != null) { await OnSelectedItemChanged(val); @@ -129,10 +147,6 @@ private async Task OnClickItem(string val) private List Rows => _filterItems ?? [.. Items]; - // Thread-safe tracking using SemaphoreSlim for async compatibility - private string _userCurrentInput = string.Empty; - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); - /// /// TriggerFilter method /// @@ -140,62 +154,46 @@ private async Task OnClickItem(string val) [JSInvokable] public override async Task TriggerFilter(string val) { - // Thread-safe update using SemaphoreSlim - await _semaphore.WaitAsync(); try { - _userCurrentInput = val; - } - finally - { - _semaphore.Release(); - } + _isFiltering = true; + // Update our tracking variable + _currentUserInput = val; - // Process filtering - if (OnCustomFilter != null) - { - var items = await OnCustomFilter(val); - _filterItems = [.. items]; - } - else if (string.IsNullOrEmpty(val)) - { - _filterItems = [.. Items]; - } - else - { - var comparison = IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - var items = IsLikeMatch - ? Items.Where(s => s.Contains(val, comparison)) - : Items.Where(s => s.StartsWith(val, comparison)); - _filterItems = [.. items]; - } + // Filter items as usual + if (OnCustomFilter != null) + { + var items = await OnCustomFilter(val); + _filterItems = [.. items]; + } + else if (string.IsNullOrEmpty(val)) + { + _filterItems = [.. Items]; + } + else + { + var comparison = IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + var items = IsLikeMatch + ? Items.Where(s => s.Contains(val, comparison)) + : Items.Where(s => s.StartsWith(val, comparison)); + _filterItems = [.. items]; + } - if (DisplayCount != null) - { - _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; - } + if (DisplayCount != null) + { + _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; + } - // Thread-safe read using SemaphoreSlim - await _semaphore.WaitAsync(); - string latestInput; - try - { - latestInput = _userCurrentInput; + // Update the bound value to match the user input, triggering proper value change notifications + // This ensures OnValueChanged is triggered while preventing visual disruption + CurrentValue = val; + + // Refresh UI + StateHasChanged(); } finally { - _semaphore.Release(); - } - - // Only update CurrentValue if this is still the latest input - if (latestInput == val) - { - CurrentValue = val; - - if (!ValueChanged.HasDelegate) - { - StateHasChanged(); - } + _isFiltering = false; } } @@ -206,8 +204,10 @@ public override async Task TriggerFilter(string val) [JSInvokable] public override Task TriggerChange(string val) { - // Only update CurrentValue if the value has actually changed - // This prevents overwriting the user's input + // Update our tracking variable + _currentUserInput = val; + + // Update component value and trigger change notifications if (CurrentValue != val) { CurrentValue = val; @@ -218,4 +218,24 @@ public override Task TriggerChange(string val) } return Task.CompletedTask; } + + /// + /// Override CurrentValueAsString to return the current user input + /// + protected override string? FormatValueAsString(string? value) + { + // During filtering operations, use what the user is actually typing + if (_isFiltering) + { + return _currentUserInput; + } + + // In non-filtering scenarios, sync our tracked value with the component value + if (!string.IsNullOrEmpty(value) && _currentUserInput != value) + { + _currentUserInput = value; + } + + return base.FormatValueAsString(value); + } } diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js index 4892883168d..59b55c1ffd6 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js @@ -17,11 +17,28 @@ export function init(id, invoke) { ac.popover = Popover.init(el, { toggleClass: '[data-bs-toggle="bb.dropdown"]' }); } + // Track the current user input to prevent it from being overwritten + ac.currentUserInput = input.value; + + // Save original input value + const updateCurrentInput = (e) => { + if (e && e.target) { + ac.currentUserInput = e.target.value; + } + }; + + // Add an input event listener to track user typing in real-time + EventHandler.on(input, 'input', updateCurrentInput); + // debounce const duration = parseInt(input.getAttribute('data-bb-debounce') || '0'); if (duration > 0) { ac.debounce = true EventHandler.on(input, 'keyup', debounce(e => { + // Don't let the debounce overwrite what the user is currently typing + if (input.value !== ac.currentUserInput) { + input.value = ac.currentUserInput; + } handlerKeyup(ac, e); }, duration, e => { return ['ArrowUp', 'ArrowDown', 'Escape', 'Enter', 'NumpadEnter'].indexOf(e.key) > -1 @@ -29,6 +46,8 @@ export function init(id, invoke) { } else { EventHandler.on(input, 'keyup', e => { + // Make sure we're using the most current input value + updateCurrentInput(e); handlerKeyup(ac, e); }) } @@ -55,6 +74,7 @@ export function init(id, invoke) { }); EventHandler.on(input, 'change', e => { + updateCurrentInput(e); invoke.invokeMethodAsync('TriggerChange', e.target.value); }); @@ -63,26 +83,29 @@ export function init(id, invoke) { filterDuration = 200; } const filterCallback = debounce(async v => { - // Check if the input value is still the same - // If not, this is an old operation that should be ignored - if (input.dataset.lastValue === v) { - await invoke.invokeMethodAsync('TriggerFilter', v); - el.classList.remove('is-loading'); + // Keep track of what was filtered vs what might be currently typed + const currentTypedValue = input.value; + + await invoke.invokeMethodAsync('TriggerFilter', v); + + // Only reset input value if the user hasn't typed something new + // during the async operation + if (input.value === v) { + input.value = ac.currentUserInput; } + + el.classList.remove('is-loading'); }, filterDuration); Input.composition(input, v => { + // Update our tracked input value + ac.currentUserInput = v; + if (isPopover === false) { el.classList.add('show'); } el.classList.add('is-loading'); - - // Store the current input value on the element - // This helps track the latest user input - input.dataset.lastValue = v; - - // Modify the filterCallback to check if the input value has changed filterCallback(v); }); @@ -176,6 +199,7 @@ export function dispose(id) { } EventHandler.off(input, 'change'); EventHandler.off(input, 'keyup'); + EventHandler.off(input, 'input'); // Remove the input event listener we added EventHandler.off(menu, 'click'); Input.dispose(input); From 72c7d17318e9522051c78284ad4a20d91df2bf5a Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 00:33:38 +0800 Subject: [PATCH 04/11] =?UTF-8?q?refactor:=20=E5=A2=9E=E5=8A=A0=20Dropdown?= =?UTF-8?q?Menu=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoComplete/DropdownMenu.razor | 21 +++++++ .../AutoComplete/DropdownMenu.razor.cs | 61 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor create mode 100644 src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs diff --git a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor new file mode 100644 index 00000000000..fad6b997fd4 --- /dev/null +++ b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor @@ -0,0 +1,21 @@ +@namespace BootstrapBlazor.Components + + diff --git a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs new file mode 100644 index 00000000000..78fe6d6d65b --- /dev/null +++ b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +namespace BootstrapBlazor.Components; + +/// +/// DropdownMenu component +/// +public partial class DropdownMenu +{ + /// + /// Get or set the dropdown menu items + /// + [Parameter, NotNull, EditorRequired] + public List? Rows { get; set; } + + /// + /// Get or set the dropdown menu item template default value is null. + /// + [Parameter] + public RenderFragment? ItemTemplate { get; set; } + + /// + /// Gets or sets whether to show the no matching data option, default is true + /// + [Parameter] + public bool ShowNoDataTip { get; set; } = true; + + /// + /// Gets or sets Display prompt message when there is no matching data. The default prompt is "No Data" + /// + [Parameter] + [NotNull] + public string? NoDataTip { get; set; } + + /// + /// Gets or sets Callback method when a candidate item is clicked + /// + [Parameter] + public Func? OnItemClick { get; set; } + + private Task OnClick(string val) + { + if (OnItemClick != null) + { + return OnItemClick.Invoke(val); + } + return Task.CompletedTask; + } + + /// + /// Render method + /// + public void Render(List items) + { + Rows = items; + StateHasChanged(); + } +} From 3b13e49e5628bd9813be8d570d67ee3d950dbf02 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 00:33:56 +0800 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20=E6=9B=B4=E6=96=B0=E4=B8=8B?= =?UTF-8?q?=E6=8B=89=E6=A1=86=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoComplete/AutoComplete.razor | 21 +---------- .../AutoComplete/AutoComplete.razor.cs | 37 +++++++++---------- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor index 04416dc2b20..dcfdb23ebc0 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor @@ -16,23 +16,6 @@ placeholder="@PlaceHolder" disabled="@Disabled" @ref="FocusElement"/> - + diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index 1d9f4e5e50d..463565ed3dc 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -89,6 +89,9 @@ public partial class AutoComplete private List? _filterItems; + [NotNull] + private DropdownMenu? _dropdown = default; + /// /// /// @@ -114,6 +117,14 @@ protected override void OnParametersSet() Items ??= []; } + private bool _render = true; + + /// + /// + /// + /// + protected override bool ShouldRender() => _render; + /// /// Callback method when a candidate item is clicked /// @@ -135,9 +146,6 @@ private async Task OnClickItem(string val) [JSInvokable] public override async Task TriggerFilter(string val) { - // Store the current input value to prevent it from being overwritten - var currentInputValue = val; - if (OnCustomFilter != null) { var items = await OnCustomFilter(val); @@ -161,14 +169,7 @@ public override async Task TriggerFilter(string val) _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; } - // Use currentInputValue here instead of potentially stale val - CurrentValue = currentInputValue; - - // Only trigger StateHasChanged if no binding is present - if (!ValueChanged.HasDelegate) - { - StateHasChanged(); - } + await TriggerChange(val); } /// @@ -178,16 +179,14 @@ public override async Task TriggerFilter(string val) [JSInvokable] public override Task TriggerChange(string val) { - // Only update CurrentValue if the value has actually changed - // This prevents overwriting the user's input - if (CurrentValue != val) + _render = false; + CurrentValue = val; + if (!ValueChanged.HasDelegate) { - CurrentValue = val; - if (!ValueChanged.HasDelegate) - { - StateHasChanged(); - } + StateHasChanged(); } + _render = true; + _dropdown.Render(Rows); return Task.CompletedTask; } } From 31e7672fec52c76ffff7fea48dbd86c67585da73 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 00:58:47 +0800 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E5=B0=81=E8=A3=85=20AutoCompleteItem=20=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoComplete/AutoComplete.razor | 26 +++++++- .../AutoComplete/AutoComplete.razor.cs | 12 +++- .../AutoComplete/AutoCompleteItems.cs | 60 ++++++++++++++++++ .../AutoComplete/DropdownMenu.razor | 21 ------- .../AutoComplete/DropdownMenu.razor.cs | 61 ------------------- 5 files changed, 94 insertions(+), 86 deletions(-) create mode 100644 src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs delete mode 100644 src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor delete mode 100644 src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor index dcfdb23ebc0..7286758dfe9 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor @@ -16,6 +16,28 @@ placeholder="@PlaceHolder" disabled="@Disabled" @ref="FocusElement"/> - + + +@code { + RenderFragment RenderDropdown => + @; +} diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index 463565ed3dc..b538f3ab5b8 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -90,7 +90,7 @@ public partial class AutoComplete private List? _filterItems; [NotNull] - private DropdownMenu? _dropdown = default; + private AutoCompleteItems? _dropdown = default; /// /// @@ -186,7 +186,15 @@ public override Task TriggerChange(string val) StateHasChanged(); } _render = true; - _dropdown.Render(Rows); + _dropdown.RenderContent(); return Task.CompletedTask; } + + private RenderFragment RenderItems => builder => + { + builder.OpenComponent(0); + builder.AddAttribute(10, "ChildContent", RenderDropdown); + builder.AddComponentReferenceCapture(20, dropdown => _dropdown = (AutoCompleteItems)dropdown); + builder.CloseComponent(); + }; } diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs new file mode 100644 index 00000000000..fe79d34e929 --- /dev/null +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License +// See the LICENSE file in the project root for more information. +// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone + +using Microsoft.AspNetCore.Components.Rendering; + +namespace BootstrapBlazor.Components; + +/// +/// DropdownMenu component +/// +class AutoCompleteItems : IComponent +{ + /// + /// Gets or sets the child content + /// + [Parameter, NotNull] + public RenderFragment? ChildContent { get; set; } + + private RenderHandle _renderHandle; + + /// + /// + /// + /// + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + /// + /// + /// + /// + /// + public Task SetParametersAsync(ParameterView parameters) + { + parameters.SetParameterProperties(this); + + RenderContent(); + return Task.CompletedTask; + } + /// + /// Render method + /// + public void RenderContent() + { + _renderHandle.Render(BuildRenderTree); + } + + /// + /// + /// + /// + private void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, ChildContent); + } +} diff --git a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor deleted file mode 100644 index fad6b997fd4..00000000000 --- a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor +++ /dev/null @@ -1,21 +0,0 @@ -@namespace BootstrapBlazor.Components - - diff --git a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs deleted file mode 100644 index 78fe6d6d65b..00000000000 --- a/src/BootstrapBlazor/Components/AutoComplete/DropdownMenu.razor.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License -// See the LICENSE file in the project root for more information. -// Maintainer: Argo Zhang(argo@live.ca) Website: https://www.blazor.zone - -namespace BootstrapBlazor.Components; - -/// -/// DropdownMenu component -/// -public partial class DropdownMenu -{ - /// - /// Get or set the dropdown menu items - /// - [Parameter, NotNull, EditorRequired] - public List? Rows { get; set; } - - /// - /// Get or set the dropdown menu item template default value is null. - /// - [Parameter] - public RenderFragment? ItemTemplate { get; set; } - - /// - /// Gets or sets whether to show the no matching data option, default is true - /// - [Parameter] - public bool ShowNoDataTip { get; set; } = true; - - /// - /// Gets or sets Display prompt message when there is no matching data. The default prompt is "No Data" - /// - [Parameter] - [NotNull] - public string? NoDataTip { get; set; } - - /// - /// Gets or sets Callback method when a candidate item is clicked - /// - [Parameter] - public Func? OnItemClick { get; set; } - - private Task OnClick(string val) - { - if (OnItemClick != null) - { - return OnItemClick.Invoke(val); - } - return Task.CompletedTask; - } - - /// - /// Render method - /// - public void Render(List items) - { - Rows = items; - StateHasChanged(); - } -} From 5beb0a59068413a066a1bd230e8950d592a6bab2 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 00:59:31 +0800 Subject: [PATCH 07/11] chore: bump version 9.4.9-beta01 --- src/BootstrapBlazor/BootstrapBlazor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/BootstrapBlazor.csproj b/src/BootstrapBlazor/BootstrapBlazor.csproj index bee27a533e7..dc8572088a4 100644 --- a/src/BootstrapBlazor/BootstrapBlazor.csproj +++ b/src/BootstrapBlazor/BootstrapBlazor.csproj @@ -1,7 +1,7 @@  - 9.4.8 + 9.4.9-beta01 From 2b5bcbe7e62f64cf0fc63ca80881dc4db4cfd68c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 01:11:42 +0800 Subject: [PATCH 08/11] =?UTF-8?q?revert:=20=E7=A7=BB=E9=99=A4=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoComplete/AutoComplete.razor.cs | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs index 048a21bcc1f..a1e7a8b0921 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs @@ -92,16 +92,6 @@ public partial class AutoComplete [NotNull] private AutoCompleteItems? _dropdown = default; - /// - /// Tracks the current user input to prevent it from being overwritten - /// - private string _currentUserInput = string.Empty; - - /// - /// Flag to track whether we're handling debounced filtering - /// - private bool _isFiltering = false; - /// /// /// @@ -125,12 +115,6 @@ protected override void OnParametersSet() LoadingIcon ??= IconTheme.GetIconByKey(ComponentIcons.LoadingIcon); Items ??= []; - - // Initialize _currentUserInput with current value if it hasn't been set yet - if (string.IsNullOrEmpty(_currentUserInput) && !string.IsNullOrEmpty(CurrentValueAsString)) - { - _currentUserInput = CurrentValueAsString; - } } private bool _render = true; @@ -146,8 +130,6 @@ protected override void OnParametersSet() /// private async Task OnClickItem(string val) { - // Update both the CurrentValue and _currentUserInput when an item is clicked - _currentUserInput = val; CurrentValue = val; if (OnSelectedItemChanged != null) @@ -183,10 +165,10 @@ public override async Task TriggerFilter(string val) _filterItems = [.. items]; } - if (DisplayCount != null) - { - _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; - } + if (DisplayCount != null) + { + _filterItems = [.. _filterItems.Take(DisplayCount.Value)]; + } await TriggerChange(val); } From a78335ab00b3cf2a61ff3795335baa88016c69f7 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 01:15:37 +0800 Subject: [PATCH 09/11] =?UTF-8?q?revert:=20=E9=87=8D=E6=9E=84=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AutoComplete/AutoComplete.razor.js | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js index 59b55c1ffd6..ac0b72dbbda 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js @@ -17,28 +17,11 @@ export function init(id, invoke) { ac.popover = Popover.init(el, { toggleClass: '[data-bs-toggle="bb.dropdown"]' }); } - // Track the current user input to prevent it from being overwritten - ac.currentUserInput = input.value; - - // Save original input value - const updateCurrentInput = (e) => { - if (e && e.target) { - ac.currentUserInput = e.target.value; - } - }; - - // Add an input event listener to track user typing in real-time - EventHandler.on(input, 'input', updateCurrentInput); - // debounce const duration = parseInt(input.getAttribute('data-bb-debounce') || '0'); if (duration > 0) { ac.debounce = true EventHandler.on(input, 'keyup', debounce(e => { - // Don't let the debounce overwrite what the user is currently typing - if (input.value !== ac.currentUserInput) { - input.value = ac.currentUserInput; - } handlerKeyup(ac, e); }, duration, e => { return ['ArrowUp', 'ArrowDown', 'Escape', 'Enter', 'NumpadEnter'].indexOf(e.key) > -1 @@ -46,8 +29,6 @@ export function init(id, invoke) { } else { EventHandler.on(input, 'keyup', e => { - // Make sure we're using the most current input value - updateCurrentInput(e); handlerKeyup(ac, e); }) } @@ -83,24 +64,11 @@ export function init(id, invoke) { filterDuration = 200; } const filterCallback = debounce(async v => { - // Keep track of what was filtered vs what might be currently typed - const currentTypedValue = input.value; - await invoke.invokeMethodAsync('TriggerFilter', v); - - // Only reset input value if the user hasn't typed something new - // during the async operation - if (input.value === v) { - input.value = ac.currentUserInput; - } - el.classList.remove('is-loading'); }, filterDuration); Input.composition(input, v => { - // Update our tracked input value - ac.currentUserInput = v; - if (isPopover === false) { el.classList.add('show'); } @@ -199,7 +167,6 @@ export function dispose(id) { } EventHandler.off(input, 'change'); EventHandler.off(input, 'keyup'); - EventHandler.off(input, 'input'); // Remove the input event listener we added EventHandler.off(menu, 'click'); Input.dispose(input); From 7ba07e5fba2bd746886f050a601578f9936fcb0c Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 01:16:22 +0800 Subject: [PATCH 10/11] =?UTF-8?q?revert:=20=E6=92=A4=E9=94=80=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AutoComplete/AutoComplete.razor.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js index ac0b72dbbda..3b7e8557f35 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.js @@ -55,7 +55,6 @@ export function init(id, invoke) { }); EventHandler.on(input, 'change', e => { - updateCurrentInput(e); invoke.invokeMethodAsync('TriggerChange', e.target.value); }); From 01035afadda33d0f44669bdfd02b9d1d0b0f1822 Mon Sep 17 00:00:00 2001 From: Argo Zhang Date: Sat, 8 Mar 2025 01:22:51 +0800 Subject: [PATCH 11/11] =?UTF-8?q?doc:=20=E6=9B=B4=E6=96=B0=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/AutoComplete/AutoCompleteItems.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs b/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs index fe79d34e929..d2839dc4ce4 100644 --- a/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs +++ b/src/BootstrapBlazor/Components/AutoComplete/AutoCompleteItems.cs @@ -8,7 +8,7 @@ namespace BootstrapBlazor.Components; /// -/// DropdownMenu component +/// AutoCompleteItems component /// class AutoCompleteItems : IComponent {