2
votes

I am writing a Blazor component for a Blazor server side app, that will display a table of data and allow one or more rows to be selected. The idea is that there will be a checkbox in the first column of the table and the row data in the remaining columns. The row is selected/deselected by clicking the checkbox OR by clicking anywhere in the row. This is done by binding an input checkbox to a bool on the row object and by using onclick on the tr element.

<tr @onclick="() => item.Toggle()">
    <td><input type="checkbox" @bind="item.Selected" /></td>
    <td>@item.Number</td>
    <td>@item.Text</td>
</tr>

Generally it works. Clicking the row will select the row, set the bool value to true and the checkbox will be checked. Clicking the row again will deselect the row, set the bool value to false and the checkbox will be unchecked. All as expected. When checking the checkbox it also works, however unchecking the checkbox results in a javascript runtime error. The blazor-error-ui footer is shown with the standard message "An unexpected exception has occurred. See browser dev tools for details. Reload".

Here are details from the Chrome Console

blazor.server.js:1 [2020-02-09T11:25:41.667Z] Information: Normalizing '_blazor' to 'https://localhost:44374/_blazor'.
blazor.server.js:1 [2020-02-09T11:25:41.874Z] Information: WebSocket connected to wss://localhost:44374/_blazor?id=58aEAiJBVZFuzpm9Sn_HdA.
blazor.server.js:15 [2020-02-09T11:25:49.676Z] Error: There was an error applying batch 10.
e.log @ blazor.server.js:15
blazor.server.js:8 Uncaught (in promise) TypeError: Cannot read property 'parentNode' of undefined
    at Object.e [as removeLogicalChild] (blazor.server.js:8)
    at e.applyEdits (blazor.server.js:8)
    at e.updateComponent (blazor.server.js:8)
    at Object.t.renderBatch (blazor.server.js:1)
    at e.<anonymous> (blazor.server.js:15)
    at blazor.server.js:15
    at Object.next (blazor.server.js:15)
    at blazor.server.js:15
    at new Promise (<anonymous>)
    at r (blazor.server.js:15)
blazor.server.js:15 [2020-02-09T11:25:49.864Z] Error: System.AggregateException: One or more errors occurred. (TypeError: Cannot read property 'parentNode' of undefined)
 ---> System.InvalidOperationException: TypeError: Cannot read property 'parentNode' of undefined
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.InvokeRenderCompletedCallsAfterUpdateDisplayTask(Task updateDisplayTask, Int32[] updatedComponents)
   --- End of inner exception stack trace ---

My Blazor component in my app uses a class Selectable which takes a Row class as a parameter and adds the bool for selection. However, to simplify the example I have included the bool Selected, in the Row class. The example is based on the standard Blazor server-side template project, and adds one Blazor page. Here is the full code for that page:

@page "/test"

@if (data == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table table-hover">
        <thead>
            <tr>
                <th>Select</th>
                <th>Number</th>
                <th>Text</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in data)
            {
                <tr @onclick="() => item.Toggle()">
                    <td><input type="checkbox" @bind="item.Selected" /></td>
                    <td>@item.Number</td>
                    <td>@item.Text</td>
                </tr>
            }
        </tbody>
    </table>
}

@code 
{
    private List<Row> data;

    private class Row
    {
        public bool Selected;
        public int Number;
        public string Text;

        public Row(int number)
        {
            Selected = false;
            Number = number;
            Text = $"Item {number}";
        }

        public void Toggle() => Selected = !Selected;
    }

    protected override void OnInitialized()
    {
        data = new List<Row>
        {
            new Row(1),
            new Row(2),
            new Row(3)
        };
    }
}

If I remove the onclick for the tr element it works fine. Since the onclick fires anytime the row is clicked, I'm guessing there is some conflict with the binding on the checkbox. However, I don't have enough Javascript skills to investigate further. I can avoid the situation by firing up onclick for each td element (except the first checkbox) rather than the whole tr, but that gets tedious and a bit ugly when there are lots of columns. Since this is failing within blazor.js I am wondering if it might be a bug/limitation of Blazor.

Any thoughts would be welcome. Thanks.

1

1 Answers

3
votes

Try the following code... Note: I've made some alterations not because they are the cause of the issue, etc., I just can't help to have my own style. However, you should focus on how you bind to elements. Note that it is sufficient to bind to the checkbox's checked attribute like this: checked="@row.Selected". I hope you were looking for such solution...

<table class="table table-hover">
    <thead>
        <tr>
            <th>Select</th>
            <th>Number</th>
            <th>Text</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var row in rows)
        {
            <tr @onclick="@(() => row.Selected = !row.Selected)">
                <td><input type="checkbox" checked="@row.Selected" /></td>
                <td>@row.Number</td>
                <td>@row.Text</td>
            </tr>
        }
    </tbody>
</table>

@code
{
   List<Row> rows = Enumerable.Range(1, 10).Select(i => new Row { Selected = 
            false, Number = i, Text = $"Item {i}" }).ToList();

  private class Row
  {
    public bool Selected { get; set; }
    public int Number { get; set; }
    public string Text { get; set; }

 }

}

I had assumed I had to use @bind but I'm still not sure why I can't

Ordinarily you can use the @bind directive to create a two-way data-binding between an Html element, and a data source. But in this case, a one way data-binding is sufficient, from the data source to the checkbox, as the lambda expression triggered by the click action on the tr element toggles the boolean row.Selected property. Consequently, the component is re-render, and the checkbox is either checked or unchecked. depending on the previous state.

Note: The @bind directive when processed by the compiler creates two attribute that SHOULD be equivalent to something like this, if you apply the directive to the checkbox:

<td><input type="checkbox" checked="@row.Selected" @onchange="@((args) => row.Selected = (bool) args.Value)" /></td>

If you use this code instead of the one above, your app still will be working fine, but as you must have understood by now, the change event raised for the checkbox is superfluous.

But, lo and behold, if you use the @bind directive, letting the compiler act on your behalf, your code will fail. I guess this is unreported bug.

I do hope that I did not make things rather complicated by adding the above addition...

Hope this helps...