Search code examples
data-bindingblazor-webassembly

How can I pass a value from child to parent, when there is no trigger?


I have a parent component with a child

<h3>Parent component</h3>
<Child />

The child contains a grandchild

<h3>Child component</h3>
<Grandchild @bind-SelectedVariant="_selectedVariant" />
<h4>Costs: @GetCosts()</h4>

GetCosts() depends on _selectedVariant.

The grandchild is

<h3>Grandchild component</h3>
<input type="checkbox" @onclick="(e) => ToggleCheckboxAsync("Variant 1")" />

@code {
    [Parameter] public string SelectedVariant { get; set; } = "";
    [Parameter] public EventCallback<string> SelectedVariantChanged { get; set; }
    private async Task OnSelectedVariantChange(string selectedVariant) => await SelectedVariantChanged.InvokeAsync(selectedVariant);

    private async Task ToggleCheckboxAsync(string clickedVariantName)
    {
        await OnSelectedVariantChange(clickedVariantName);
        StateHasChanged();
    }
}

This works well. When I toggle the checkbox, the displayed costs are updated. (Only on checking the checkbox, because unchecking is not implemented in this example.)
I found out, I need the costs in the parent component. So, my first try was adding another binding via EventCallback:

<h3>Child component</h3>
<Grandchild @bind-SelectedVariant="_selectedVariant" />
<h4>Costs: @GetCosts()</h4>

@code {
    [Parameter, EditorRequired] public int Costs { get; set; } = -1;
    [Parameter] public EventCallback<int> CostsChanged { get; set; }
    private async Task OnCostsChange() => await CostsChanged.InvokeAsync(Costs);
}

and

<h3>Parent component</h3>
<Child @bind-Costs="_costs" />

but how do I trigger OnCostsChange()? It was easy with OnSelectedVariantChange(), because it was simply the click event of the checkbox.


Solution

  • I think you may be overcomplicating this a little. You only need to do two way binding if you intend to pass the value from parent to child. If the child does some logic to come up with the value then just use a callback.

    Here's my Grandchild. I'm guessing there are multiple variants in reality so I coded it that way.

    <h3>Grandchild component</h3>
    
    <input type="checkbox" 
        @bind:get="@(IsChecked("Variant 1"))" 
        @bind:set="@((e) => ToggleCheckboxAsync("Variant 1"))" />
    
    @code {
        [Parameter] public string SelectedVariant { get; set; } = string.Empty;
        [Parameter] public EventCallback<string> SelectedVariantChanged { get; set; }
    
        private bool IsChecked(string value)
            => SelectedVariant == value;
    
        private async Task ToggleCheckboxAsync(string value)
        {
            if (this.IsChecked(value))
                await this.SelectedVariantChanged.InvokeAsync(string.Empty);
            else
                await this.SelectedVariantChanged.InvokeAsync(value);
    
            // not required
            //StateHasChanged();
        }
    }
    

    And then Child:

    <h3>Child component</h3>
    
    <GrandChild @bind-SelectedVariant:get="_selectedVariant" @bind-SelectedVariant:set="this.OnVariantChanged" />
    
    <h4>Costs: @_costs</h4>
    
    @code {
        // this is a calcuated value, not set from the parent
        // Parameter, EditorRequired] public int Costs { get; set; } = -1;
        [Parameter] public EventCallback<int> CostsChanged { get; set; }
    
        private int _costs;
        private string _selectedVariant = string.Empty;
    
        private async Task OnVariantChanged(string value)
        {
            _selectedVariant = value;
            _costs = Random.Shared.Next(5,25);
            if (value.Equals("Variant 1"))
                _costs = _costs + 100;
    
            await this.CostsChanged.InvokeAsync(_costs);
        }
    }
    

    And Home:

    @page "/"
    
    <PageTitle>Home</PageTitle>
    
    <h1>Hello, world!</h1>
    
    <Child CostsChanged="this.OnCostsChanged" />
    
    <div class="bg-dark text-white m-2 p-2">
        <pre>Costs: @_costs</pre>
    </div>
    
    @code{
        private int _costs;
    
        private Task OnCostsChanged(int value)
        {
            _costs = value;
            return Task.CompletedTask;
        }
    
    }