En Blazor, ¿cómo puedo deshacer una entrada de usuario no válida sin cambiar el estado del componente para activar una nueva representación?
Aquí hay un ejemplo simple de contador de Blazor ( pruébelo en línea ):
<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(); } } } Para el elemento de input A, quiero replicar el comportamiento del elemento de input B, pero sin usar los atributos de enlace de datos de estilo bind-xxx .
Por ejemplo, cuando 123x dentro de A, quiero que vuelva a 123 automáticamente, como sucede con B.
StateHasChanged pero no funciona, supongo, porque la propiedad de count en realidad no cambia.
Entonces, básicamente necesito volver a renderizar A para deshacer la entrada de usuario no válida, aunque el estado no haya cambiado. ¿Cómo puedo hacer eso sin la magia bind-xxx ?
Claro, bind-xxx es excelente, pero hay casos en los que se puede desear un comportamiento no estándar, construido alrededor de un controlador de eventos administrado como ChangeEvent .
Actualizado , para comparar, así es como podría haberlo hecho en React ( pruébelo en línea ):
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/> </> ); }Además, así es como podría haberlo hecho en Svelte, que me parece conceptualmente muy parecido a Blazor ( pruébelo en línea ).
<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/> Actualizado , para aclarar, simplemente quiero deshacer lo que considere una entrada no válida, retrospectivamente después de que haya sucedido, al manejar el evento de cambio, sin mutar el estado del componente en sí ( counter aquí).
Es decir, sin el enlace de datos bidireccional específico de Blazor, el type=number o los atributos de coincidencia de patrones. Simplemente uso el requisito de formato de número aquí como ejemplo; Quiero poder deshacer cualquier entrada arbitraria como esa.
La experiencia de usuario que quiero (hecha a través de un truco de interoperabilidad JS): https://blazorrepl.telerik.com/wPbvcvvi128Qtzvu03
Me sorprendió que esto fuera tan difícil en Blazor en comparación con otros marcos, y que no puedo usar StateHasChanged para simplemente forzar una nueva representación del componente en su estado actual.
Deberá usar @on{DOM EVENT}-preventDefault .
Viene del mundo de JavaScript y evita el comportamiento predeterminado del botón.
En blazor y razor, la validación o DDL activa la devolución de datos, lo que significa que el servidor procesa la solicitud y se vuelve a procesar.
Al hacer esto, debe asegurarse de que su evento no "burbujee", ya que está impidiendo que el evento realice una devolución de datos.
Si encuentra que su evento burbujea, lo que significa que el evento del elemento sube a los elementos en los que está anidado, use:
stopPropagation="true";
para más información:
A continuación se muestra lo que se me ocurrió ( pruebe en línea ). Desearía que ChangeEventArgs.Value funcionara en ambos sentidos, pero no es así. Sin él, solo puedo pensar en este truco de JS.InvokeVoidAsync :
@inject IJSRuntime JS <label>Count:</label> <button @onclick=Increment>@count times!</button> <br> A: <input @oninput=OnChange value="@count" id="@id"> <br> B: <input @bind-value=count @bind-value:event="oninput"> @code { int count = 1; string id = Guid.NewGuid().ToString("N"); void Increment() => count++; async Task OnChange(ChangeEventArgs e) { var userValue = e.Value?.ToString(); if (int.TryParse(userValue, out var v)) { count = v; } else { if (String.IsNullOrWhiteSpace(userValue)) { count = 0; } // this doesn't work e.Value = count.ToString(); // this doesn't work either (no rererendering): StateHasChanged(); // using JS interop as a workaround await JS.InvokeVoidAsync("eval", $"document.getElementById('{id}').value = Number('{count}')"); } } } Para ser claros, me doy cuenta de que este es un truco horrible (por último, pero no menos importante, porque usa eval ); Posiblemente podría mejorarlo usando ElementReference y una importación JS aislada, pero todo eso no sería necesario si e.Value = count funcionara, como lo hace con Svelte . He planteado esto como un problema de Blazor en el repositorio de ASP.NET , espero que pueda llamar la atención.
Al mirar el código, ¿parece que quieres desinfectar la entrada del usuario? Por ejemplo, puede ingresar solo números, formatos de fecha, etc. Estoy de acuerdo en que es un poco difícil si desea usar los controladores de eventos manualmente en este sentido en este momento, pero aún es posible validar la entrada usando propiedades expandidas y el sistema de enlace de Blazor.
Lo que quieres se vería así: ( pruébalo aquí )
<label>Count: @Count</label> <button @onclick=Increment>@Count times!</button><br> <input @bind-value=Count @bind-value:event="oninput"> @code { int _count = 1; public string Count { get => _count.ToString(); set { if(int.TryParse(value, out var val)) { _count = val; } else { if(string.IsNullOrEmpty(value)) { _count = 0; } } } } void Increment() => _count++; }Puede usar @on{DOM EVENT}:preventDefault para evitar la acción predeterminada para un evento. Para obtener más información, consulte los documentos de Microsoft: https://docs.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-6.0#prevent-default-actions
ACTUALIZAR
Un ejemplo del uso de 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); } } }Esta fue una pregunta muy interesante. Nunca antes había usado Blazor, pero tenía una idea sobre lo que podría ayudar aquí, aunque esta también es una respuesta engañosa.
Noté que si cambiaba el elemento para vincularlo a la variable de conteo, actualizaría el valor cuando el control perdiera el foco. Así que agregué algo de código para intercambiar elementos de enfoque. Esto parece permitir escribir caracteres no numéricos sin cambiar el campo de entrada, que es lo que creo que se desea aquí.
Obviamente, no es una solución fantástica, pero pensé en ofrecerla en caso de que ayude.
<label>Count:</label> <button @onclick=Increment>@count times!</button><br> A: <input @oninput=OnChange @bind-value=count @ref="textInput1"><br> B: <input @bind-value=count @bind-value:event="oninput" @ref="textInput2"> @code { ElementReference textInput1; ElementReference textInput2; 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; e.Value = count.ToString(); textInput2.FocusAsync(); textInput1.FocusAsync(); } } }Entonces, básicamente necesito volver a renderizar A para deshacer la entrada de usuario no válida, aunque el estado no haya cambiado. ¿Cómo puedo hacer eso sin la magia bind-xxx?
Puede forzar la recreación y la reproducción de cualquier elemento/componente cambiando el valor en la directiva @key :
<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; } } }Tenga en cuenta que volverá a representar todo el elemento (y su subárbol), no solo el atributo.
El problema con su código es que el método BuildRendrerTree de su componente genera exactamente el mismo RenderTree, por lo que el algoritmo de diferenciación no encuentra nada para actualizar en el dominio real.
Entonces, ¿por qué funciona la directiva @bind?
Observe el código BuildRenderTree generado:
//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();El truco es que la directiva @bind agrega:
__builder.SetUpdatesAttributeName("value");No puede hacer esto en el marcado para EventCallback en este momento, pero hay un problema abierto para ello: https://github.com/dotnet/aspnetcore/issues/17281
Sin embargo, aún puede crear un Componente o RenderFragment y escribir el código __builder manualmente.