Search code examples
blazorblazor-server-side.net-8.0

Input element is not re-rendered


The input element is not re-rendered, the use case is that when the user enters some text the current date is filled into the input element via the 'value' attribute. See the code below.

@page "/"

<label>Text Binding check:</label>
<input type="text" value="@TextData" @onchange="OnChange_Text" />
<br />

<br />
<label>Text Dummy:</label>
<input type="text" value="dummy" />

@code
{
    public string? TextData { get; set; } = "12-12-2022";

    protected override bool ShouldRender()
    {
        return true;
    }

    private void OnChange_Text(ChangeEventArgs e)
    {
        TextData = GetValue(e.Value?.ToString());

        InvokeAsync(() => StateHasChanged());
    }

    private string GetValue(string? strValue)
    {
        return strValue switch
        {
            "test" => DateTime.Now.ToString("dd-MM-yyyy"),
            "data" => DateTime.Now.ToString("dd-MM-yyyy"),
            "nash" => DateTime.Now.ToString("dd-MM-yyyy"),
            _ => DateTime.Now.ToString("dd-MM-yyyy")
        };
    }
}

If the user enters the same text or any text again, the date is not rendered in the input element the second time. Even with StateHasChanged() and ShouldRender set to true, the value is not rendered.

Issue demonstarion

So as per the Blazor mechanism the element is not really changed so the render tree will not render the input element to DOM. How can I solve this problem and how can I re-render the element that has not changed?. I thought SateHasChanged() and ShouldRender refresh the UI, it doesn't care about the element difference.


Solution

  • I'm assuming this is an Interactive page.

    Use @bind and @bind:after. Let binding and the UI event handler do the work.

    The bind callback [to set TextData] is a UI event. It sets the value and then calls the registered after method. Finally it calls StateHasChanged to render the new state.

    I've modified the dates produced and formatting so it's easier to see the different results.

    @page "/"
    
    <label>Text Binding check:</label>
    <input type="text" @bind="TextData" @bind:after="OnChange_Text" />
    <br />
    
    <br />
    <label>Text Dummy:</label>
    <input type="text" value="@EnteredValue" />
    
    @code
    {
        public string? TextData { get; set; } = "12-Dec-2022";
        public string? EnteredValue;
    
        private void OnChange_Text()
        {
            this.EnteredValue = this.TextData;
            this.TextData = GetValue(this.TextData);
        }
    
        private string GetValue(string? strValue)
        {
            return strValue switch
            {
                "test" => DateTime.Now.AddMonths(2).ToString("dd-MMM-yyyy"),
                "data" => DateTime.Now.AddMonths(1).ToString("dd-MMM-yyyy"),
                "nash" => DateTime.Now.ToString("dd-MMM-yyyy"),
                _ => DateTime.Now.AddMonths(-1).ToString("dd-MMM-yyyy")
            };
        }
    }
    

    An explanation of why the original code didn't work.

    It's complicated, but your problem is caused by synchronisation issues between the various DOMs when you modify the actual DOM in an edit element.

    First Change

    Starting state:

    • Actual DOM: 12-12-2022
    • Virtual DOM: 12-12-2022
    • Blazor Rendered DOM: 12-12-2022
    • TextData : 12-12-2022

    When you change the Actual DOM to "Fred"

    • Actual DOM: Fred
    • Virtual DOM: 12-12-2022
    • Blazor Rendered DOM: 12-12-2022
    • TextData : 12-12-2022

    When the onChange UI event completes:

    • Actual DOM: Fred
    • Virtual DOM: 12-12-2022
    • Blazor Rendered: 12-12-2022
    • TextData : Today

    After the UI event render:

    • Actual DOM: Fred
    • Virtual DOM: 12-12-2022
    • Blazor Rendered: Today
    • TextData : Today

    At the completion of the render the Renderer runs the diffing engine, detects the difference and send the updates to the actual DOM.

    • Actual DOM: Today
    • Virtual DOM: Today
    • Blazor Rendered: Today
    • TextData : Today

    Second Change

    Now we edit the input to "Fred" again.

    • Actual DOM: Fred
    • Virtual DOM: Today
    • Blazor Rendered DOM: Today
    • TextData : Today

    When the onChange UI event completes:

    • Actual DOM: Fred
    • Virtual DOM: Today
    • Blazor Rendered DOM: Today
    • TextData : Today

    After the UI event render:

    • Actual DOM: Fred
    • Virtual DOM: Today
    • Blazor Rendered DOM: Today
    • TextData : Today

    At the completion of the render the Renderer runs the diffing engine, detects no difference and does nothing. The actual DOM isn't updated.

    • Actual DOM: Fred
    • Virtual DOM: Today
    • Blazor Rendered DOM: Today
    • TextData : Today

    You can use my solution, or add a workaround to your code by changing to this OnChange_Text:

        private async Task OnChange_Text(ChangeEventArgs e)
        {
            // Set to nonsense value so the diffing engine sees the change
            TextData = "  ";
            // yield and let the UI event handler render the component
            await Task.Yield();
            // set to the nee value
            TextData = GetValue(e.Value?.ToString());
            // final UI event handler render will happen here
        }