Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Blazor, is there a way to undo invalid user input, without changing the state?

In Blazor, how can I undo invalid user input, without changing the state of the component to trigger a re-render?

Here is a simple Blazor counter example (try it online):

<label>Count:</label>
<button @onclick=Increment>@count times!</button><br>
A: <input @oninput=OnChange value="@count"><br>
B: <input @bind-value=count @bind-value:event="oninput">

@code {
    int count = 1;

    void Increment() => count++;

    void OnChange(ChangeEventArgs e)
    {
        var userValue = e.Value?.ToString(); 
        if (int.TryParse(userValue, out var v))
        {
            count = v;
        }
        else 
        {
            if (String.IsNullOrWhiteSpace(userValue))
            {
                count = 0;
            }

            // if count hasn't changed here,
            // I want to re-render "A"

            // this doesn't work
            e.Value = count.ToString();

            // this doesn't work either 
            StateHasChanged();           
       }
    }
}

For input element A, I want to replicate the behavior of input element B, but without using the bind-xxx-style data binding attributes.

E.g., when I type 123x inside A, I want it to revert back to 123 automatically, as it happens with B.

I've tried StateHasChanged but it doesn't work, I suppose, because the count property doesn't actually change.

So, basically I need to re-render A to undo invalid user input, even thought the state hasn't changed. How can I do that without the bind-xxx magic?

Sure, bind-xxx is great, but there are cases when a non-standard behavior might be desired, built around a managed event handler like ChangeEvent.


Updated, to compare, here's how I could have done it in React (try it online):

function App() {
  let [count, setCount] = useState(1);
  const handleClick = () => setCount((count) => count + 1);
  const handleChange = (e) => {
    const userValue = e.target.value;
    let newValue = userValue ? parseInt(userValue) : 0;
    if (isNaN(newValue)) newValue = count;
    // re-render even when count hasn't changed
    setCount(newValue); 
  };
  return (
    <>
      Count: <button onClick={handleClick}>{count}</button><br/>
      A: <input value={count} onInput={handleChange}/><br/>
    </>
  );
}

Also, here's how I could have done it in Svelte, which I find conceptually very close to Blazor (try it online).

<script>
  let count = 1;
  const handleClick = () => count++;
  const handleChange = e => {
    const userValue = e.target.value;
    let newValue = userValue? parseInt(userValue): 0;
    if (isNaN(newValue)) newValue = count;
    if (newValue === count)
      e.target.value = count; // undo user input
    else
      count = newValue; 
    }
  };    
</script>

Count: <button on:click={handleClick}>{count}</button><br/>
A: <input value={count} on:input={handleChange}/><br/>

Updated, to clarify, I simply want to undo whatever I consider an invalid input, retrospectively after it has happened, by handling the change event, without mutating the component's state itself (counter here).

That is, without Blazor-specific two-way data binding, HTML native type=number or pattern matching attributes. I simply use the number format requirement here as an example; I want to be able to undo any arbitrary input like that.

The user experience I want (done via a JS interop hack): https://blazorrepl.telerik.com/wPbvcvvi128Qtzvu03

Surprised this so difficult in Blazor compared to other frameworks, and that I'm unable to use StateHasChanged to simply force a re-render of the component in its current state.

like image 379
noseratio Avatar asked Nov 18 '21 08:11

noseratio


People also ask

What is @bind in Blazor?

To use two-way binding on a parameter simply prefix the HTML attribute with the text @bind- . This tells Blazor it should not only push changes to the component, but should also observe the component for any changes and update its own state accordingly.

How do you find the input value of Blazor?

The value of an input element is updated in the wrapper with the change events of elements in Blazor. To get the current value for each character input, you must use the oninput event of the input element. This can be achieved by binding the oninput event (native event) using the @bind:event=“oninput“.

Is Blazor two-way binding?

Blazor also supports two-way data binding by using bind attribute. Currently, Blazor supports only the following data types for two-way data binding. If you need other types (e.g. decimal), you need to provide getter and setter from/to a supported type.

Does Blazor support input validation?

Still, the good news is that Blazor supports basic form handling and input validation out-of-the-box. In this article, we will build an UserForm component that accepts different input types, performs input validation, and handles the form submit. You can follow along using the default Blazor application template within Visual Studio.

What is a userform component in Blazor?

All the code shown in this article is written in a single UserForm.razor component file. Blazor provides an EditForm component that wraps the HTML form tag and adds convenient functionality to handle user input. The Model property allows us to bind an instance of a model class to the form.

How to use onvalidsubmit and oninvalidsubmit in Blazor?

The other approach is to use the OnValidSubmit and OnInvalidSubmit properties. Blazor performs input validation, and depending on the validation result, either the method bound to the OnValidSubmit or the OnInvalidSubmit property will be called.

Is Blazor server stateful or stateful?

Blazor Server is a stateful app framework. Most of the time, the app maintains a connection to the server. The user's state is held in the server's memory in a circuit. Examples of user state held in a circuit include: The hierarchy of component instances and their most recent render output in the rendered UI.


2 Answers

You can use @on{DOM EVENT}:preventDefault to prevent the default action for an event. For more information look at Microsoft docs: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-6.0#prevent-default-actions

UPDATE

An example of using preventDefault

<label>Count:</label>
<button @onclick=Increment>@count times!</button><br />
<br>
A: <input value="@count" @onkeydown=KeyHandler @onkeydown:preventDefault><br>
B: <input @bind-value=count @bind-value:event="oninput">

@code {
    int count = 1;
    string input = "";

    void Increment() => count++;

    private void KeyHandler(KeyboardEventArgs e)
    {
        //Define your pattern
        var pattern = "abcdefghijklmnopqrstuvwxyz";
        if (!pattern.Contains(e.Key) && e.Key != "Backspace")
        {
            //This is just a sample and needs exception handling
            input = input + e.Key;
            count = int.Parse(input);
        }
        if (e.Key == "Backspace")
        {
            input = input.Remove(input.Length - 1);
            count = int.Parse(input);
        }
    }
}
like image 99
ggeorge Avatar answered Oct 18 '22 02:10

ggeorge


So, basically I need to re-render A to undo invalid user input, even thought the state hasn't changed. How can I do that without the bind-xxx magic?

You can force recreating and rerendering of any element/component by changing value in @key directive:

<input @oninput=OnChange value="@count" @key="version" />
void OnChange(ChangeEventArgs e)
{
    var userValue = e.Value?.ToString(); 
    if (int.TryParse(userValue, out var v))
    {
        count = v;
    }
    else 
    {
        version++;
        if (String.IsNullOrWhiteSpace(userValue))
        {
            count = 0;
        }         
   }
}

Notice, that it will rerender the entire element (and it's subtree), not just the attribute.


The problem with your code is, that the BuildRendrerTree method of your component generates exactly the same RenderTree, so the diffing algorithm doesn't find anything to update in the actual dom.

So why the @bind directive works?

Notice the generated BuildRenderTree code:

//A
__builder.OpenElement(6, "input");
__builder.AddAttribute(7, "oninput", Microsoft.AspNetCore.Components.EventCallback.Factory.Create<Microsoft.AspNetCore.Components.ChangeEventArgs>(this, OnChange));
__builder.AddAttribute(8, "value", count);
__builder.CloseElement();

//B
__builder.AddMarkupContent(9, "<br>\r\nB: ");
__builder.OpenElement(10, "input");
__builder.AddAttribute(11, "value", Microsoft.AspNetCore.Components.BindConverter.FormatValue(count));
__builder.AddAttribute(12, "oninput", Microsoft.AspNetCore.Components.EventCallback.Factory.CreateBinder(this, __value => count = __value, count));
__builder.SetUpdatesAttributeName("value");
__builder.CloseElement();

The trick is, that @bind directive adds:

__builder.SetUpdatesAttributeName("value");

You can't do this in markup for EventCallback right now, but there is an open issue for it: https://github.com/dotnet/aspnetcore/issues/17281

However you still can create a Component or RenderFragment and write the __builder code manually.

like image 2
Liero Avatar answered Oct 18 '22 04:10

Liero