Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
69 changes: 47 additions & 22 deletions src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor
Original file line number Diff line number Diff line change
@@ -1,47 +1,72 @@
@namespace BootstrapBlazor.Components
@* --- File: AutoComplete.razor --- *@
@namespace BootstrapBlazor.Components
@inherits PopoverCompleteBase<string>

@* Show label if configured *@
@if (IsShowLabel)
{
<BootstrapLabel required="@Required" for="@InputId" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText"/>
<BootstrapLabel required="@Required" for="@InputId" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText" />
}

@* Main container div *@
<div class="auto-complete" id="@Id">
<input @attributes="AdditionalAttributes" id="@InputId" class="@ClassName" autocomplete="off" type="text"
data-bs-toggle="@ToggleString" data-bs-placement="@PlacementString"
data-bs-offset="@OffsetString" data-bs-custom-class="@CustomClassString"
data-bb-auto-dropdown-focus="@ShowDropdownListOnFocusString" data-bb-debounce="@DurationString"
data-bb-skip-esc="@SkipEscString" data-bb-skip-enter="@SkipEnterString" data-bb-blur="@TriggerBlurString"
data-bb-scroll-behavior="@ScrollIntoViewBehaviorString" data-bb-trigger-delete="true"
value="@CurrentValueAsString"
placeholder="@PlaceHolder" disabled="@Disabled" @ref="FocusElement"/>
@* Input element - Value is now controlled primarily by the browser & JS setValue *@
<input @attributes="AdditionalAttributes"
id="@InputId"
class="@ClassName"
autocomplete="off"
type="text"
data-bs-toggle="@ToggleString"
data-bs-placement="@PlacementString"
data-bs-offset="@OffsetString"
data-bs-custom-class="@CustomClassString"
data-bb-auto-dropdown-focus="@ShowDropdownListOnFocusString"
data-bb-debounce="@DurationString"
data-bb-skip-esc="@SkipEscString"
data-bb-skip-enter="@SkipEnterString"
data-bb-blur="@TriggerBlurString"
data-bb-scroll-behavior="@ScrollIntoViewBehaviorString"
data-bb-trigger-delete="true"
placeholder="@PlaceHolder"
disabled="@Disabled"
@ref="FocusElement" />

@* Icon shown by default *@
<span class="form-select-append"><i class="@Icon"></i></span>
@* Loading icon shown during filtering *@
<span class="form-select-append ac-loading"><i class="@LoadingIcon"></i></span>

@* Render the dropdown menu using RenderFragment *@
<RenderTemplate @ref="_dropdown">
@RenderDropdown
</RenderTemplate>
</div>

@code {
// RenderFragment for the dropdown menu content
RenderFragment RenderDropdown =>
@<div class="dropdown-menu">
<div class="dropdown-menu-body">
@* Iterate through filtered items (Rows) *@
@foreach (var item in Rows)
{
<div @key="item" class="dropdown-item" @onclick="() => OnClickItem(item)">
@* Use ItemTemplate if provided, otherwise display item directly *@
@if (ItemTemplate == null)
{
<div @key="item" class="dropdown-item" @onclick="() => OnClickItem(item)">
@if (ItemTemplate == null)
{
<div>@item</div>
}
else
{
@ItemTemplate(item)
}
</div>
<div>@item</div>
}
@if (ShowNoDataTip && Rows.Count == 0)
else
{
<div class="dropdown-item">@NoDataTip</div>
@ItemTemplate(item)
}
</div>
}
@* Show "No data" tip if configured and no items match *@
@if (ShowNoDataTip && !Rows.Any()) // Use !Rows.Any() for clarity
{
<div class="dropdown-item disabled">@NoDataTip</div> @* Add 'disabled' class? *@
}
</div>
</div>;
}
182 changes: 152 additions & 30 deletions src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone

using Microsoft.Extensions.Localization;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; // Required for JSInvokable
using System; // Required for Func

namespace BootstrapBlazor.Components;

Expand All @@ -12,6 +19,8 @@
/// </summary>
public partial class AutoComplete
{
// Parameters... (omitted for brevity, same as latest code)
#region Parameters
/// <summary>
/// Gets or sets the collection of matching data obtained by inputting a string
/// </summary>
Expand Down Expand Up @@ -67,6 +76,7 @@
/// </summary>
[Parameter]
public bool ShowNoDataTip { get; set; } = true;
#endregion

/// <summary>
/// IStringLocalizer service instance
Expand All @@ -83,16 +93,19 @@
private List<string>? _filterItems;

[NotNull]
private RenderTemplate? _dropdown = default;
private RenderTemplate? _dropdown = default!; // Use ! assertion

// REMOVED: _currentInputValue field is no longer needed

// private bool _isFirstRender = true; // Flag for initial value setting - Handled in OnAfterRenderAsync

/// <summary>
/// <inheritdoc/>
/// </summary>
protected override void OnInitialized()
{
base.OnInitialized();

SkipRegisterEnterEscJSInvoke = true;
SkipRegisterEnterEscJSInvoke = true; // Keep this if base class needs it
}

/// <summary>
Expand All @@ -110,21 +123,52 @@
Items ??= [];
}

private bool _render = true;

/// <summary>
/// <inheritdoc/>
/// OnAfterRenderAsync method
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override bool ShouldRender() => _render;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Ensure JS interop module is loaded (likely handled by base class)
await base.OnAfterRenderAsync(firstRender);

if (firstRender)
{
// _isFirstRender = false; // Not needed
await JSSetInputValue(Value); // Set initial value using the backing field
}
// Handle external parameter changes if necessary
// This might require comparing the current Value parameter against a stored previous value
// For simplicity, we assume external changes require user interaction or parent component logic
}

// REMOVED: _render flag and ShouldRender override (may not be needed)

/// <summary>
/// Callback method when a candidate item is clicked
/// </summary>
private async Task OnClickItem(string val)
{
CurrentValue = val;
// Update C# state first, bypassing CurrentValue setter
var previousValue = Value;
var valueHasChanged = !EqualityComparer<string>.Default.Equals(val, previousValue);

if (valueHasChanged)
{
Value = val; // Update backing field directly

// Manually trigger notifications/callbacks
if (FieldIdentifier != null) ValidateForm?.NotifyFieldChanged(FieldIdentifier.Value, Value);
if (ValueChanged.HasDelegate) await ValueChanged.InvokeAsync(Value);
if (OnValueChanged != null) await OnValueChanged.Invoke(Value);
if (IsNeedValidate && FieldIdentifier != null) EditContext?.NotifyFieldChanged(FieldIdentifier.Value);
}

// Update the visual input via JS
await JSSetInputValue(val);

// Invoke selection changed callback separately
if (OnSelectedItemChanged != null)
{
await OnSelectedItemChanged(val);
Expand All @@ -133,13 +177,34 @@

private List<string> Rows => _filterItems ?? [.. Items];

// REMOVED: UpdateInputValue JSInvokable method

/// <summary>
/// TriggerFilter method
/// JSInvokable method called by JavaScript after debouncing.
/// Receives the debounced value from the input.
/// Renamed from TriggerFilter.
/// </summary>
/// <param name="val"></param>
[JSInvokable]
public override async Task TriggerFilter(string val)
/// <param name="val">The debounced input value.</param>
[JSInvokable] // This method is new/renamed, keep JSInvokable
public async Task PerformFilteringAndCommitValue(string val)
{
// --- Bypass CurrentValue Setter ---
var previousValue = Value;
var valueHasChanged = !EqualityComparer<string>.Default.Equals(val, previousValue);

if (valueHasChanged)
{
Value = val; // Update backing field directly

// Manually trigger notifications and callbacks
if (FieldIdentifier != null) ValidateForm?.NotifyFieldChanged(FieldIdentifier.Value, Value);
if (ValueChanged.HasDelegate) await ValueChanged.InvokeAsync(Value);
if (OnValueChanged != null) await OnValueChanged.Invoke(Value);
if (IsNeedValidate && FieldIdentifier != null) EditContext?.NotifyFieldChanged(FieldIdentifier.Value);
}
// --- End Bypass ---

// Perform filtering logic (using the new 'val')...
if (OnCustomFilter != null)
{
var items = await OnCustomFilter(val);
Expand All @@ -163,36 +228,93 @@
_filterItems = [.. _filterItems.Take(DisplayCount.Value)];
}

await TriggerChange(val);
// Update dropdown UI
if (_dropdown != null)
{
StateHasChanged(); // Trigger re-render of dropdown via main component render
}
}

// REMOVED: TriggerChange method from latest code

/// <summary>
/// TriggerChange method
/// Handles the Enter key press, potentially committing the current input value.
/// Hides base implementation.
/// </summary>
/// <param name="val"></param>
[JSInvokable]
public override Task TriggerChange(string val)
/// <param name="val">The current value in the input field when Enter was pressed.</param>
// Removed [JSInvokable]
public new async Task EnterCallback(string val) // Use 'new'
{
// client input does not need to be re-rendered to prevent jitter when the network is congested
_render = false;
CurrentValue = val;
_render = true;
_dropdown.Render();
return Task.CompletedTask;
// Update C# state first, bypassing setter
var previousValue = Value;
var valueHasChanged = !EqualityComparer<string>.Default.Equals(val, previousValue);

if (valueHasChanged)
{
Value = val; // Update backing field directly

// Manually trigger notifications/callbacks
if (FieldIdentifier != null) ValidateForm?.NotifyFieldChanged(FieldIdentifier.Value, Value);
if (ValueChanged.HasDelegate) await ValueChanged.InvokeAsync(Value);
if (OnValueChanged != null) await OnValueChanged.Invoke(Value);
if (IsNeedValidate && FieldIdentifier != null) EditContext?.NotifyFieldChanged(FieldIdentifier.Value);
}

// Update the visual input via JS
await JSSetInputValue(val);
}


/// <summary>
/// TriggerChange method
/// Handles the Escape key press. Hides base implementation.
/// </summary>
/// <param name="val"></param>
[JSInvokable]
// Removed [JSInvokable]
public new async Task EscCallback() // Use 'new'
{
// Reset visual input to last committed C# value
await JSSetInputValue(Value);
}

/// <summary>
/// Handles deletion - No longer directly called by JS in this version.
/// </summary>
// Removed [JSInvokable]
public Task TriggerDeleteCallback(string val)
{
CurrentValue = val;
if (!ValueChanged.HasDelegate)
// Value update is handled by PerformFilteringAndCommitValue after debounce.
return Task.CompletedTask;
}

/// <summary>
/// Helper method to call the JS function to set the input value.
/// </summary>
/// <param name="value">The value to set.</param>
private async ValueTask JSSetInputValue(string? value)
{
try
{
StateHasChanged();
// Module is JSObjectReference from BootstrapModuleComponentBase
if (Module != null)
{
await Module.InvokeVoidAsync("setValue", Id, value);
}
}
return Task.CompletedTask;
catch (JSDisconnectedException) { } // Ignore if circuit is disconnected
catch (ObjectDisposedException) { } // Ignore if Module is disposed
catch (Exception ex)

Check warning on line 304 in src/BootstrapBlazor/Components/AutoComplete/AutoComplete.razor.cs

View workflow job for this annotation

GitHub Actions / run test

The variable 'ex' is declared but never used
{
//Console.WriteLine($"Error calling JS setValue for ID {Id}: {ex.Message}"); // Log other errors
}
}

/// <summary>
/// Dispose method
/// </summary>
/// <param name="disposing"></param>
/// <returns></returns>
protected override async ValueTask DisposeAsync(bool disposing)
{
// Ensure base disposal runs, which should call JS dispose
await base.DisposeAsync(disposing);
}
}
Loading
Loading