Custom Blazor Select-component option-element @onclick not firing - How do I get it to fire?

2.2k Views Asked by At

I am creating a custom Blazor component, and I need the select component to update the bound field with the selected value, this part is working. I also need it to execute a method passed in from the parent component, this part is not working.

I am able to pass the method into the custom component (child), but the onclick event of the option element of the select element is not firing.

I was able to implement this in a multiple select element, but it is not working in a select element.

How do I get this to fire and still update the bound data property?

ESelect.razor Custom (child) select component

@inherits InputBase<string>

<div class="form-floating">
        <select class="form-control form-select @CssClass" id="@Id" @bind="@CurrentValue" >
            <option disabled selected></option>
            @foreach(SelectOption option in Options)
            {
                <option [email protected] onclick="@( () => OnClick(option) )" >@option.Value</option>
            }
        </select>
        @if (!string.IsNullOrWhiteSpace(Label))
        {
            <label class="form-control-label" for="@Id">@Label</label>        
        }
        <div class="form-control-validation">
            <ValidationMessage For="@ValidationFor" />
        </div>
</div>

ESelect.razor.cs (C# code for ESelect.razor)

using BebComponents.DataModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using System.Linq.Expressions;

namespace BebComponents
{
    public partial class ESelect
    {
        [Parameter, EditorRequired]
        public Expression<Func<string>> ValidationFor { get; set; } = default!;
        [Parameter]
        public string? Id { get; set; } = "ESelect";
        [Parameter]
        public string? Label { get; set; }
        [Parameter]
        public List<SelectOption> Options { get; set; }
        [Parameter]
        public SelectOption? SelectedOption { get; set; }
        [Parameter]
        public Action? Trigger { get; set; }

        protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
        {            
            result = value; 
            validationErrorMessage = null;
            return true;
        }

        public void OnClick(SelectOption option)
        {
            SelectedOption = option;            
            Trigger?.Invoke();
        }

    }
}

SelectOption.cs (datamodel)

namespace BebComponents.DataModels
{
    public class SelectOption
    {
        public int Id { get; set; }
        public string Value { get; set; }
    }
}

Index.razor

@page "/"
@using BebComponents
@using BebComponents.DataModels
@using static BebComponents.EDualSelect

<EditForm Model="Form" OnValidSubmit="ValidFormSubmit" class="mt-5">
    <DataAnnotationsValidator />
    <h3>Form Example:</h3>
    <ValidationSummary />
    <h3 class="mt-4">Enhanced Select</h3>
    <ESelect Id="ESelect" @ref="eSelect" @bind-Value="Form.LastName" Options="@options" ValidationFor="@( () => Form.LastName )" 
        Label="Last Name" Trigger="EnableEnhancedSelect2"/>
    <h5 class="mt-2">The last name selected is:</h5>
    <p>@Form.LastName</p>
</EditForm>

Index.razor.cs

using BlazorComponents.Pages.PageModels;
using BebComponents;
using static BebComponents.EDualSelect;
using static BebComponents.ESingleSelect;
using static BebComponents.ESelect;
using BebComponents.DataModels;

namespace BlazorComponents.Pages
{    
    public partial class Index
    {
        private ESelect eSelect;
        private string eSelectResult;
        private readonly List<SelectOption> options = new List<SelectOption>
        {            
            new SelectOption { Id = 1, Value = "Jones" },
            new SelectOption { Id = 2, Value = "Smith" },
            new SelectOption { Id = 3, Value = "Bender" },
            new SelectOption { Id = 4, Value = "Baggio" },
            new SelectOption { Id = 5, Value = "Allen" },
            new SelectOption { Id = 6, Value = "Biggs" },
            new SelectOption { Id = 7, Value = "Randall" },
            new SelectOption { Id = 8, Value = "Anderson" },
            new SelectOption { Id = 8, Value = "Reeves" }
        };
    }
}
2

There are 2 best solutions below

2
On BEST ANSWER

The Simple Solution

Your existing code can be made to work with one minor addition.

You can see a working example in this Blazor Fiddle Demo Blazor Fiddle is rather limited, but I've tried to keep the code close to the original.

Your click handler never fires because an <option> element only fires when the parent is a <select multiple>. Yet, in this case it's just a default single <select>and will never fire. We might use the onchange event instead, which is usually how this is done. Yet, we could also just add a wrapper around the CurrentValue property, which would allow you to reuse the code you already have.

CurrentValue is a property inherited from InputBase. And to invoke the parent supplied Trigger method you will need to wrap CurrentValue to provide that new functionality. This can be done by using the "new" keyword and calling the method in the setter as shown.

Code to add to the component:

[Parameter]
public new string CurrentValue 
{
    get => base.CurrentValue;
    set {
        base.CurrentValue = value;
        Trigger?.Invoke();
    }
}

You may also want to remove the existing onclick handler from the markup since it doesn't do anything and would only be misleading.

4
On

Updated Answer:

Here's a revised answer that shows you how to build out a composite control - in this case a select - and link in the binding. I've simplified the options setup.

I've added an extra ValueHasChanged EventCallback that gets triggered whenever the value is changed. I use it to print a time update to show it working.

@typeparam TValue
@using System.Linq.Expressions

<div class="row">
    <div class="col-2">
        @this.Label
    </div>
    <div class="col-6">
        <InputSelect TValue=TValue [email protected] ValueChanged=this.OnValueChanged ValueExpression=this.ValueExpression>
            @ChildContent
        </InputSelect>
    </div>
    <div class="col-6">
        <ValidationMessage For=this.ValidationFor />
    </div>
</div>

@code {
    [Parameter, EditorRequired] public string Label { get; set; } = "Field Label";

    [Parameter] public TValue? Value { get; set; }

    [Parameter] public EventCallback<TValue> ValueChanged { get; set; }

    [Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }

    [Parameter, EditorRequired] public Expression<Func<TValue>> ValidationFor { get; set; } = default!;

    [Parameter] public RenderFragment? ChildContent { get; set; }

    [Parameter] public EventCallback<TValue> ValueHasChanged { get; set; }

    private void OnValueChanged(TValue value)
    {
        ValueChanged.InvokeAsync(value);
        if (this.ValueHasChanged.HasDelegate)
            this.ValueHasChanged.InvokeAsync(value);
    }
}

And a test page:

@page "/"
@using Microsoft.Extensions.Options
<EditForm Model=model>
    <MySelect Label="Country" TValue=int @bind-Value=model.Value ValueHasChanged=this.OnValueChanged ValidationFor="() => model.Value" >
        <option value=1>Spain</option>
        <option value=2>Portugal</option>
        <option value=3>France</option>
    </MySelect>
</EditForm>

<div class="m-2 p-2">
    Value = @model.Value
</div>
<div class="m-2 p-2">
    Value = @message
</div>

@code {
    private MyModel model = new();
    private string message = string.Empty;

    private void OnValueChanged(int value)
        =>  this.message = $"Updated at {DateTime.Now.ToLongTimeString()}";

    public class MyModel 
    {
        public int Value { get; set; } = 3;
    }
}

Original Answer:

Your code is bit complex to decipher. Here's a simple example to demonstrate how to use a select and capture the change event on it.

The important bits to understand are:

  1. You can't bind to an Option.

  2. Binding with @bind uses the Onchanged event on the select so you can't also bind to it. Instead use @oninput to bind to an event handler.

  3. The value returned is a string, so you need to do some parsing to get it to say an int.

  4. If you're triggering an event you need to check that the value has actually changed and only call the Event Callback if it has.

@page "/"
<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<select @bind=this.Value @oninput=OnChange>
    <option value="1">1</option>
    <option value="2">2</option>
    <option value="3">3</option>
</select>

<div>
    value: @this.Value
</div>

<div>
    Int Value: @this.IntValue
</div>

@code {
    public EventCallback<int> OnValueChanged;

    private string Value = "3";

    private int IntValue;

    private async Task OnChange(ChangeEventArgs e)
    {
        var x = e.Value;
        if (int.TryParse(e.Value?.ToString(), out int value))
        {
            if (value != IntValue)
            {
            this.IntValue = value;
            if (this.OnValueChanged.HasDelegate)
                await this.OnValueChanged.InvokeAsync(value);
            }
        }
    }
}