Kaine
Kaine

Reputation: 579

Choosing and configuring the most appropriate Blazor Fluent UI Drop-Down

I'm fairly new to Blazor and the new Fluent UI. I'm trying to create set of hierarchical drop-downs (the values in one list based on the user selection in another) based on the values from an API, but can't seem to get the placeholder text to behave as I'd like.

Using the following Blazor code, I've mocked up an example of my problem. It uses the FluentCombobox because it has a convenient Placeholder property. The problem is that, when the sub-list changes, the value chosen previously is persisted.

Repro steps:

  1. After the page loads, choose Make: 'Audi'.
  2. Then choose Model: 'A4'.
  3. Then go back to Make and choose 'BMW'.
  4. Problem: the value in Model still reflects 'A4', despite 'A4' not being a valid option. I'd like it to show the Placeholder text instead of persisting the previously selected value.

Blazor page


@page "/Vehicles"
@rendermode InteractiveServer

<h3>Vehicles</h3>

<FluentCombobox Label="Make:" Placeholder="Select a make..." Items="@Makes" ValueChanged="MakeValueChanged" Disabled="@MakesEmpty" Autocomplete="ComboboxAutocomplete.List" Height="300px" />
<br/>
<FluentCombobox Label="Model:" Placeholder="Select a model..." Items="@Models" ValueChanged="ModelValueChanged" Disabled="@ModelsEmpty" Autocomplete="ComboboxAutocomplete.List" Height="300px" />
<br />
<FluentButton @onclick="OnSearchClick" Disabled="@SearchDisabled">Search</FluentButton>

@code {
    public string Make { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;

    private IEnumerable<string> Makes { get; set; } = [];
    private IEnumerable<string> Models { get; set; } = [];

    private IDictionary<string, IEnumerable<string>> MakesModels { get; set; } = new Dictionary<string, IEnumerable<string>>
    {
        {"Audi", new string[] { "A4", "A6", "A8" } },
        {"BMW", new string[] { "3 Series", "4 Series", "6 Series" } },
        {"Volvo", new string[] { "C30", "C40", "C70" } },
    };

    private bool MakesEmpty { get { return this.Makes.Count() == 0; } }
    private bool ModelsEmpty { get { return this.Models.Count() == 0; } }
    private bool SearchDisabled { get { return this.Model.Length == 0; } }

    private void LoadMakes()
    {
        // ... do stuff...
        this.Makes = this.MakesModels.Keys;
    }

    private void LoadModels(string make)
    {
        this.Models = [];
        this.Model = string.Empty;

        // ... do stuff...
        this.Models = this.MakesModels[make];
    }

    private void MakeValueChanged(string make)
    {
        this.Make = make;
        this.LoadModels(this.Make);
    }

    private void ModelValueChanged(string model)
    {
        this.Model = model;
    }

    private void OnSearchClick()
    {
        Console.WriteLine($"Searching for make '{this.Make}' and model '{this.Model}'");
    }

    protected override void OnInitialized()
    {
        this.LoadMakes();
    }
}

Here is an (simplistic) example of an implementation of the drop-down component that works as I expected, but not using Fluent UI:


<div>
    <label>@Label
        <select id="@Id" @bind="Value" @bind:after="OnValueChanged">
            @{
                int i = 0;
                foreach (var item in Items)
                {
                    if (0 == i++)
                    {
                        <option value="" selected disabled>@PlaceholderText</option>
                    }
                    <option value="@item.Value">@item.Label</option>
                }
            }
        </select>
    </label>
</div>

As you can see, the Placeholder value is re-written when the items list changes.

I've explored using the Fluent Listbox component and the FluentSelect component instead, but these don't seem to have a Placeholder attribute, and always chooses the value matching the position of the previously selected value (which is odd behaviour in my view). I want to avoid that and force the user to have to choose a value, or no value at all if that's a valid option. I suppose that I could use the Listbox and add a blank entry at the beginning. Is there no better way?

See also:

Thanks,

Kaine

UPDATE

I've tried creating a custom component wrapper around the FluentSelect to handle the placeholder, but this seems to create some strange and quirky behaviour. For instance, the initial selected state for Make doesn't select the placeholder (this could be because I've disabled it), and the Model value reflects the ordinal position of any previously selected value, irrespective of changing the list items.

FluentSelect Wrapper Component


<FluentSelect
    Label="@Label"
    Items="@OptionItems"
    TOption="Option<string>"
    OptionText="@(i => i.Text)"
    OptionValue="@(i => i.Value)"
    OptionSelected="@(i => i.Selected)"
    OptionDisabled="@(i => i.Disabled)"
    ValueChanged="OnValueChanged"
    Disabled="@Disabled" />

@code {
    private IEnumerable<string> _items = [];

    [Parameter]
    public bool Disabled { get; set; }

    [Parameter]
    public IEnumerable<string> Items 
    {
        get { return _items; }
        set
        {
            _items = value;
            OptionItems = new()
            {
                { new() { Disabled = true, Selected = true, Text = Placeholder, Value = string.Empty } },
            };

            var options = _items.Select(item => new Option<string> { Text = item, Value = item });
            OptionItems.AddRange(options);
        }
    }

    [Parameter]
    public string Label { get; set; } = string.Empty;

    [Parameter]
    public string Placeholder { get; set; } = string.Empty;

    [Parameter]
    public string Value { get; set; } = string.Empty;

    [Parameter]
    public EventCallback<string> OnValueChanged { get; set; }

    private List<Option<string>> OptionItems = [];
}

Upvotes: 0

Views: 932

Answers (2)

Johan Greefkes
Johan Greefkes

Reputation: 61

You can add a placeholder with an extra FluentOption in the options list. This option is not displayed on the dropdown but does show the message as the placeholder.

<FluentOption Value="" Style="display:none;" aria-hidden="true">Select an option...</FluentOption>

Using the demo data, this looks like this:

<EditForm Model="Model" FormName="Starship3">
    <FluentSelect TOption="string" @bind-Value="@Model!.Classification">
        <FluentOption Value="" Style="display:none;" aria-hidden="true">Select an option...</FluentOption>
        <FluentOption Value="Defense">Defense</FluentOption>
        <FluentOption Value="Exploration">Exploration</FluentOption>
        <FluentOption Value="Diplomacy">Diplomacy</FluentOption>
    </FluentSelect>

    <FluentButton OnClick="@(() => Model!.Classification = "Exploration")">Set value to "Exploration"</FluentButton>
</EditForm>

Unfortunately, when you provide a list of options dynamically the added options is not the first one. I did suggest a change to the framework to show the Placeholder in all cases and they worked it into a PR that has already been merged. This should be available in NuGet with the next release!

With this new version of the framework, you'll be able to do this:

<FluentSelect TOption="Person"
              Label="Select a person"
              Placeholder="Make a selection..."
              ... />

Upvotes: 0

Pierre
Pierre

Reputation: 107

You can try the following

<h3>Vehicles</h3>

<FluentCombobox Label="Make:" Placeholder="Select a make..." Items="@MakesModels.Keys.ToList()" SelectedOptionChanged="@((p)=>{ Make=p; Model=null; })" SelectedOption="@Make" TOption=string Autocomplete="ComboboxAutocomplete.List" Height="300px" />
<br />
<FluentCombobox Label="Model:" Placeholder="Select a model..." Items="@(Make!=null?@MakesModels[Make]:[])" TOption=string SelectedOption="@Model" SelectedOptionChanged="@((p)=>Model=p)" Disabled="@(Make==null)" Autocomplete="ComboboxAutocomplete.List" Height="300px" />
<br />
<FluentButton @onclick="OnSearchClick" Disabled="@(Make==null||Model==null)">Search for @Make @Model</FluentButton>
 
@code {

    public string? Make { get; set; } 
    public string? Model { get; set; }  
 

    private IDictionary<string, IEnumerable<string>> MakesModels { get; set; } = new Dictionary<string, IEnumerable<string>>
    {
        {"Audi", new string[] { "A4", "A6", "A8" } },
        {"BMW", new string[] { "3 Series", "4 Series", "6 Series" } },
        {"Volvo", new string[] { "C30", "C40", "C70" } },
    };
      
    private void OnSearchClick()
    {
        Console.WriteLine($"Searching for make '{this.Make}' and model '{this.Model}'");
    }
}

Upvotes: 2

Related Questions