Search code examples
firefoxselectblazorselected

FireFox and selected="selected"


NOTE: I added working examples at the end of this post. In my FireFox, they demonstrate the problem. CAN ANYONE TAKE MY EXAMPLES AND REPRO IN YOUR FIREFOX. I have no way of knowing if this is a work problem or not.

All I want is for my Blazor solution to work in Edge and FireFox, but the latter is not working as expected with a select list.

The issue I have has been talked about again and again 4, 10 and 15 years ago yet the solutions discussed are not solving the problem nor are some other Blazor ideas. You can see some of the remnants of the attempted fixes are still in my html, such as selectedIndex = "-1" and everything has a unique name.

The below picture is the initial state of the select in the rendered display and the browser's dev tools.

Edge initial state

The issue happens when I pick the third or fourth option - FireFox flips it back to the second option. In Edge it sticks to the option I picked.

Blazor generates the select using a basic loop technique. (I've also tried setting the selectedIndex using JSRuntime).

<select name="@QuestionItem.Id" style="width: 218px; height:34px;" TValue="Answer" @onchange="SelectChanged">
    <option disabled="" selected="">Select Answer</option>
    @foreach (var a in QuestionItem.Answers)
    {
        // maybe https://stackoverflow.com/questions/60599131/blazor-select-dropdown-set-active-value-by-code
        @if (a.IsDisqualified)
        {
            <option disabled="disabled" value="@a.Id">@a.Name</option>
        }
        else
        {
            @if (a.IsSelected)
            {
                <option name="@a.Id" value="@a.Id" selected="selected">@a.Name</option>
            }
            else
            {
                <option name="@a.Id" value="@a.Id">@a.Name</option>
            }
        }
    }
</select>

The objects that drive the loop have an IsSelected property that has Blazor write out an <option selected="selected"> element. The @onchange event commits the newly picked option to the database and runs a bunch of biz rules.

Now, I'll demonstrate the problem.

Using Edge, I pick the "Full Installation" option and it behaves fine - the select renders "Full Installation" and the html properly has selected="selected" on the same option.

Edge consistent state

Now I repeat those steps with FireFox. Here again is the default state.

FireFox initial state

Once again I choose "Full Installation" but now we get an inconsistent state - the select flips to "No" but the html shows "Full Installation" is selected.

FireFox inconsistent state

Regardless, the correct value is written to the database so upon refresh FireFox then shows the correct selected option. It seems inconceivable that web developers have put up with this or that Mozilla has left the problem languishing all this time. So what am I doing wrong?

The version of FireFox I have is 78.11.0esr (64-bit).

Here is a simple example that reproduces the problem.

@page "/Select"
<h3>Select</h3>

@if (SelectData != null)
{
<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled value="-1" selected>Select an Option</option>
        @foreach (var duration in SelectData)
        {
            @if (duration.IsSelected)
            {
            <option value="@duration.Name" selected>@duration.Name</option>
            }
            else
            {
            <option value="@duration.Name">@duration.Name</option>
            }
        }
    </select>
</div>
}
else
{
    <label>Loading</label>
}

<div class="p-2 m-2">
    Current Value : @_Duration.Name
</div>
<div class="p-2 m-2">
    IsSelected : @_Duration.IsSelected
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);

        if (selected != null)
        {
            _Duration = selected;
            _Duration.IsSelected = true;
        }
    }

    private List<Duration> SelectData = new List<Duration>
{
        new Duration{ Name = "Hello1", IsSelected = false},
        new Duration{ Name = "Hello2", IsSelected = false},
        new Duration{ Name = "Hello3", IsSelected = false},
    };
}

Here is a slightly different example with the same FF problem

@page "/Select"
<h3>Select</h3>

@if (SelectData != null)
{
<div class="form-group col-md-6">
    <label for="dur">Duration</label>
    <select class="custom-select" @onchange="this.SelectChanged">
        <option disabled selected>Select an Option</option>
        @foreach (var duration in SelectData)
        {
            @if (_Duration == duration)
            {
            <option value="@duration.Name" selected>@duration.Name</option>
            }
            else
            {
            <option value="@duration.Name">@duration.Name</option>
            }
        }
    </select>
</div>
}
else
{
    <label>Loading</label>
}

<div class="p-2 m-2">
    Current Value : @_Duration.Name
</div>
<div class="p-2 m-2">
    IsSelected : @_Duration.IsSelected
</div>


@code {
    private class Duration
    {
        public string Name { get; set; }
        public bool IsSelected { get; set; }
    }

    private Duration _Duration = new Duration();


    private void SelectChanged(ChangeEventArgs e)
    {
        var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);

        if (selected != null)
        {
            _Duration = selected;
            _Duration.IsSelected = true;
        }
    }

    private List<Duration> SelectData = new List<Duration>
    {
        new Duration{ Name = "Hello1", IsSelected = false},
        new Duration{ Name = "Hello2", IsSelected = false},
        new Duration{ Name = "Hello3", IsSelected = false},
    };

    protected override async Task OnInitializedAsync()
    {
        var a = SelectData.FirstOrDefault(t => t.IsSelected);
        if (a != null)
            _Duration = a;
    }
}

Solution

  • Here's a complete solution...Copy and paste, run and test.

    @page "/"
    
    
    @if (SelectData != null)
    {
        <div class="form-group col-md-6">
            <label for="dur">Duration</label>
            <select class="custom-select" @onchange="this.SelectChanged">
                <option disabled selected>Select an Option</option>
                @foreach (var duration in SelectData)
                {
                      <option value="@duration.Name" selected="@duration.IsSelected">@duration.Name</option>
                   
                }
            </select>
        </div>
    
       
    }
    else
    {
        <label>Loading</label>
    }
    
    <span class="p-2 m-2">
        Current Value : @_Duration.Name
    </span>
    <span class="p-2 m-2">
        IsSelected : @_Duration.IsSelected
    </span>
    <br />
    <div>
    @foreach (var duration in SelectData)
     {
         <div>@duration.Name: @duration.IsSelected</div>
     }
    </div>
    
    
    @code {
        private class Duration
        {
            public string Name { get; set; }
            public bool IsSelected { get; set; }
        }
    
        private Duration _Duration = new Duration();
    
    
        private void SelectChanged(ChangeEventArgs e)
        {
            var selected = SelectData.SingleOrDefault(t => t.Name == (string)e.Value);
    
               
            if (selected != null)
            {
                _Duration = selected;
                _Duration.IsSelected = true;
            }
        }
    
        private List<Duration> SelectData = new List<Duration>
    {
            new Duration{ Name = "Hello1", IsSelected = false},
            new Duration{ Name = "Hello2", IsSelected = false},
            new Duration{ Name = "Hello3", IsSelected = false},
            new Duration{ Name = "Hello4", IsSelected = false}
        };
    } 
    

    Edit

    John Mc, selected="@duration.IsSelected" is not Html. This is Razor markups that is rendered as the empty Html selected attribute or selected="", depending on the browser used. That is the way to do that in Blazor ( though in the current case you could apply only the selected attribute without a value. That is, you could have written:

    @if (_Duration == duration)
    { 
      <option value="@duration.Name" selected>@duration.Name</option>
    }
    

    ), meaning that the value of the selected attribute is processed by the Blazor team, and if it is evaluated to true, the selected Html attribute is added, if it is evaluated to 'false`, it is omitted altogether, before rendered as Html in the browser.

    The answer you provided a link to deals with Html, and this is not the case here. The @if statement above is not Html. it is Razor markups.

    The selected Html attribute is an empty attribute; that is, its presence instruct the browser to select a given option. If absent, no selection is made. If you use selected="false" the given option would be selected. If you use selected="Not false" the given option would be selected.

    To sum up, depending on what you really want to do, there are much better ways to implement this. Most importantly, you should use two way data-binding, with the @bind attribute. Are you aware that you were using one-way data-binding ?