Megabit / Blazorise

Blazorise is a component library built on top of Blazor with support for CSS frameworks like Bootstrap, Tailwind, Bulma, AntDesign, and Material.
https://blazorise.com/
Other
3.3k stars 533 forks source link

Autocomplete Text Disappears when firing Asynchronous Function in SearchChanged #3865

Closed gkochera closed 2 years ago

gkochera commented 2 years ago

TL;DR

My AutoComplete is accepting input text, I use this string to go out and search a database for matching records, then set the data in the Autocomplete to the matching records, allowing the user to pick from the matches.

We're talking well north of 100,000 records. What I'm trying to accomplish is displaying a spinner in an Addon next to the Autocomplete while the EntityFramework routine runs so the user knows their query is being processed.

The Problem: While the spinner displays when the search fires off, the user entered text disappears. When the query completes a we're back to square one with an empty Autocomplete.

The Question: Am I missing something glaringly obvious? I've combed through the source in Autocomplete and the docs and can't see any reason why this shouldn't work, but I'm human and it's entirely possible I'm not seeing something. I'll also admit, I may not fully grasp the rendering cycle in Blazor. Is this the wrong approach or is this a bug/missing feature in the Autocomplete? Any thoughts are appreciated.

This GIF depicts me clicking on the Autocomplete and attempting to type 'S', 'm' and then 'i' (as in trying to type in the name Smith).

autocompleteIssue

Project Details

.Net 6 Blazorise 1.0.1

What I've Tried

Code

Here is the relevant portion of the Blazor page where the Autocomplete and Spinner are:

                    <Addons>
                        <Addon AddonType="AddonType.Start">
                            <AddonLabel>
                                <MySpinner @ref="SearchSpinner" />
                            </AddonLabel>
                        </Addon>
                        <Addon AddonType="AddonType.Body">
                            <Div Flex="Flex.Grow.Is1">
                                <Autocomplete 
                                    TItem="User"
                                    TValue="User"
                                    Data="@PossibleUsers"
                                    SearchChanged="@GetPossibleUsers"
                                    TextField=@(( item ) => $"{item.Id} - {item.FirstName} {item.LastName} ({item.EmployeeId})")
                                    FreeTyping
                                    Placeholder="Enter a Employee ID, First or Last Name..."
                                    ValueField="@(( item ) => item)"
                                    @bind-SelectedValue="selectedUserValue"
                                    @bind-SelectedText="selectedUserText"
                                    >
                                    <ItemContent>
                                        <Text>@context.Item.Id - @context.Item.FirstName @context.Item.LastName (@(context.Item.EmployeeId))</Text>
                                    </ItemContent>
                                </Autocomplete>
                            </Div>
                        </Addon>
                    </Addons>

Relevant supporting code for the Autocomplete:

  public partial class NewUserViewModel : ComponentBase
  {
      protected List<User> PossibleUsers { get; set; } = new List<User>();
      protected MySpinner SearchSpinner = new MySpinner();

      protected async Task GetPossibleUsers(string query)
      {
          await SearchSpinner.Show();
          PossibleUsers = await MyUserService.GetUsersByQueryAsync(query);
          await SearchSpinner.Hide();
      }
  }

Spinner Component:

@inherits MySpinnerComponent

@if(Active)
{
    <Div style="visibility: visible;">
        <SpinKit Type="SpinKitType.Circle" Centered=true Color="Black" Size="12px"  />
    </Div>
} else {
    <Div style="visibility: hidden;">
        <SpinKit Type="SpinKitType.Circle" Centered=true Color="Black" Size="12px"  />
    </Div>
}

Spinner Code (Note: I've also tried this by removing 'await InvokeAsync(StateHasChanged)' in both .Show() and .Hide()', Same issue happens:

using Microsoft.AspNetCore.Components;

namespace MyProject.Shared
{
    public partial class MySpinnerComponent : ComponentBase
    {
        [Parameter]
        public bool Active {get; set;}

        public async Task Show()
        {
            Active = true;
            await Task.Delay(1);
            await InvokeAsync(StateHasChanged);
        }

        public async Task Hide()
        {
            Active = false;
            await Task.Delay(1);
            await InvokeAsync(StateHasChanged);
        }
    }
}
David-Moreira commented 2 years ago

This PR https://github.com/Megabit/Blazorise/pull/3856 should be good news for you. I'll test this use case in the PR. I'll also check if there's anything you can do currently to get that working. In theory it should indeed work just like you are trying.

gkochera commented 2 years ago

Appreciate the information. We are using a bit of workaround now to get some reasonable results but its definitely a temporary workaround. Look forward to seeing ReadData for this component. Im sure it will be embraced with open arms by the Blazorise community. Great job!

stsrki commented 2 years ago

@David-Moreira The #3856 is for v1.1 but you added this ticket into v1.0 support. I have switched it to 1.1 dev. Am I wrong?

David-Moreira commented 2 years ago

Seems like a mistake. It is indeed for 1.1 dev.

David-Moreira commented 2 years ago

Hello @gkochera this kinda works. What I've noticed, tough, is that if we mutate the data collection, if you selected on of the items that came back from the search, it will not be able to bind it, since SearchChanged triggers back again, and the collection goes empty and AutoComplete can no longer evaluate the selected item since it's gone. So please proceed with your implementation taking this into account.

ReadData should be the way to go to handle this use case, and should not have this problem, but we'll check it.

<Addons>
    <Addon AddonType="AddonType.Start">
        <AddonLabel>
            @if (isLoading)
            {
                <text>Loading...</text>
            }
        </AddonLabel>
    </Addon>
    <Addon AddonType="AddonType.Body">
        <Div Flex="Flex.Grow.Is1">
            <Autocomplete TItem="User"
                          TValue="User"
                          Data="@PossibleUsers"
                          SearchChanged="@GetPossibleUsers"
                          TextField="@(( item ) => $"{item.Name}")"
                          FreeTyping
                          Placeholder="Enter a Employee ID, First or Last Name..."
                          ValueField="@(( item ) => item)">
                <ItemContent>
                    <Text>@context.Item.Name</Text>
                </ItemContent>
            </Autocomplete>
        </Div>
    </Addon>
</Addons>

@code {
    private bool isLoading;

    protected List<User> PossibleUsers { get; set; } = new List<User>();
    //protected MySpinner SearchSpinner = new MySpinner();

    protected async Task GetPossibleUsers(string query)
    {
        isLoading = true;
        await InvokeAsync(StateHasChanged);
        PossibleUsers = new List<User>() { new() { Name = "John" }, new User() { Name = "Victor" } };
        await Task.Delay(500);
        isLoading = false;
        await InvokeAsync(StateHasChanged);
    }
}