Yoeri
Yoeri

Reputation: 1906

Blazor pass binding to child component

I just created a custom Select. The API defines a SelectedValue, SeelctedText and an Items collection. All params are typed so I have a TItem and TValue type parameter.

Example:

    <Select 
         TItem="TechItem" 
         TValue="Guid" 
         ItemTextSelector="(TechItem t) => t.Name"
         ItemValueSelector="(TechItem t) => t.ID"
         Items="TechItemDataSource"
         @bind-SelectedValue="_selectedTech" />

Which works like a charm :-) And now my special case: we often have predefined sets of list-items. In that case, the parameters of select should be different:

    <Select 
       ItemListID="TechItems"
       @bind-SelectedValue="_selectedTech" />

I could just add the ItemListID to the select but that would give issues regarding the typing. I cannot make an override in Blazor. So I decided to try is by calling the same component from within itself:

@if(ItemListID != null) {
    <Select 
        TValue="string"
        TItem="IListItem" 
        Items="@(ListService.GetItems(ItemListID))" 
        ItemValueSelector="(IListItem i) => i.Value"
        ItemTextSelector="(IListItem i) => i.Caption" 
        @bind-SelectedValue=@SelectedValue
 />

}

Thought this would work ... but looks like I cannot pass the @bind-SelectedValue internally to another component (same in this case).

Any thoughts?

Upvotes: 0

Views: 412

Answers (2)

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30036

I'm guessing your Select looks something like this (very similar to one of my controls)

@typeparam TItem
@typeparam TValue

<select @attributes=this.AdditionalAttributes @bind:get="Value" @bind:set=this.OnChange>
    @if (Value is null)
    {
        <option value="" disabled selected> -- Select an Item -- </option>
    }
    @foreach (var item in this.ItemsProvider)
    {
        <option value="@GetItemValue(item)">@GetItemText(item)</option>
    }
</select>

@code {
    [Parameter] public TValue? Value { get; set; }
    [Parameter] public EventCallback<TValue> ValueChanged { get; set; }
    [Parameter, EditorRequired] public Func<TItem, object>? TextProvider { get; set; }
    [Parameter, EditorRequired] public Func<TItem, object>? ValueProvider { get; set; }
    [Parameter, EditorRequired] public IEnumerable<TItem> ItemsProvider { get; set; } = Enumerable.Empty<TItem>();
    [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

    private Func<TItem, object> _textProvider = default!;
    private Func<TItem, object> _valueProvider = default!;

    protected override void OnInitialized()
    {
        // check for nulls before anything gets rendered
        ArgumentNullException.ThrowIfNull(this.TextProvider);
        ArgumentNullException.ThrowIfNull(this.ValueProvider);
        _textProvider = this.TextProvider!;
        _valueProvider = this.ValueProvider!;
    }

    private async Task OnChange(TValue value)
    => await ValueChanged.InvokeAsync(value);

    private string? GetItemValue(TItem item)
        => _valueProvider(item).ToString();

    private string? GetItemText(TItem item)
        => _textProvider(item).ToString();
}

In which case you can create a new component for your IListItem lists. I don't believe creating an instance of self within a component is a good idea.

<Select TItem=IListItem
        TValue=string
        TextProvider="(IListItem i) => i.Caption"
        ValueProvider="(IListItem i) => i.Value"
        ItemsProvider=this.ItemsProvider
        @bind-Value:set=this.OnChange
        @bind-Value:get=this.Value
        @attributes=this.AdditionalAttributes />

@code {
    [Parameter, EditorRequired] public IEnumerable<IListItem> ItemsProvider { get; set; } = Enumerable.Empty<IListItem>();
    [Parameter] public string? Value { get; set; }
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }

    private async Task OnChange(string value)
        => await ValueChanged.InvokeAsync(value);
}

I've purposely left the List controller out as it ties the component to a specific data source implementation. You can wire the method directly into the ItemsProvider in the parent.

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>


<Select class="form-select" 
    ItemsProvider=data 
    TextProvider="(Data t) => t.Caption" 
    ValueProvider="(Data t) => t.Value" 
    TItem="Data" 
    TValue="string" 
    @bind-Value=_selectedValue1 />

<div class="alert alert-info m-3 p-2">@_selectedValue1</div>

<ListSelect class="form-select" 
    ItemsProvider="GetItems()" 
    @bind-Value=_selectedValue2 />

<div class="alert alert-info m-3 p-2">@_selectedValue2</div>

@code {
    private string? _selectedValue1;
    private string? _selectedValue2;

    private List<Data> data = new()
    {
        new(Guid.NewGuid(), "France"),
        new(Guid.NewGuid(), "Spain"),
        new(Guid.NewGuid(), "Portugal"),
    };

    public IEnumerable<IListItem> GetItems()
        => data;

    public record Data(Guid Value, string Caption) : IListItem;
}

Upvotes: 0

Dimitris Maragkos
Dimitris Maragkos

Reputation: 11322

Instead of using the @bind- syntax do it like this:

@if (ItemListID != null)
{
    <Select 
        ...
        SelectedValue="@SelectedValue"
        SelectedValueChanged="@SelectedValueChanged" />
}

Upvotes: 1

Related Questions