Blazor组件过滤击键



Blazor具有将输入限制为数字的InputNumber组件。然而,这会呈现一个firefox不尊重的<input type=number .../>(它允许任何文本(。

所以我尝试创建一个自定义组件来过滤输入:

@inherits InputNumber<int?>
<input type="number" @bind=_value @onkeypress=OnKeypress />
<p>@_value</p>
@code {
private string _value;
private void OnKeypress(KeyboardEventArgs e) {
if (Regex.IsMatch(e.Key, "[0-9]"))
_value += e.Key;
}
}

<p>显示正确的值。但输入本身显示所有按键。

如何编写自定义组件来过滤击键?(在这个例子中,我过滤数字,但我认为同样的方法也适用于过滤任何字符。(

如果要使用Blazor EditForm基础结构,可以创建自定义InputNumber

以下代码继承自InputNumber,并进行了以下更改。我已经用Firefox和Edge测试过了,但我的笔记本电脑上没有安装Chrome。

  1. 输入类型更改为text。这可以确保所有浏览器的行为一致,如果设置为`number,则情况并非如此
  2. 输入value被映射到新的内部字段,该字段将从新的setter更新
  3. CCD_ 8连接到CCD_ 9以捕获每个按键
  4. SetValue包含用于检查有效数字输入的新逻辑。该代码具有内联注释
@inherits InputNumber<TValue>
@typeparam TValue
<input type="text"
value="@this.displayValue"
class="@this.CssClass"
@oninput=SetValue
@attributes=this.AdditionalAttributes
@ref=this.Element />
@code {
// create a unique string based on the null Ascii char
//private static string emptyString = ((char)0).ToString();
private static string emptyString = string.Empty;
private string? displayValue;
public async Task SetValue(ChangeEventArgs e)
{
// Get the current typed value
var value = e.Value?.ToString();
// Check if it's a number of the TValue or null
var isValidNumber = BindConverter.TryConvertTo<TValue>(value, System.Globalization.CultureInfo.CurrentCulture, out var num)
|| value is null;
// If it's not valid we need to reset the value
if (!isValidNumber)
{
// Set the value to an empty string
displayValue = emptyString;
// Call state has changed to render the component
StateHasChanged();
// Give thw renderer some processor time to execute the queued render by creating a Task continuation
await Task.Delay(1);
// Set the display to the previous value stored in CurrentValue 
displayValue = FormatValueAsString(CurrentValue);
// done
return;
}
// We have a numbr so set the two fields to the current value
// This is the display value
displayValue = value;
// This triggers the full InputBase/EditContext logic
CurrentValueAsString = value;
}
}

为什么我们需要双重渲染技巧

如果数字无效,我们必须进行双重渲染,以修复实际DOM和render的DOM之间发生的不一致。

考虑一下:

  1. 当前值为12
  2. Renderer的DOM段是value="12"
  3. 我们将输入更改为12q
  4. 浏览器DOM现在是value="12q",而渲染器DOM仍然是value="12"

如果我们现在将Renderer DOM设置为value="12",它没有改变。Diffing引擎没有发现任何差异,也不会更新浏览器UI。

为了解决这个问题,我们必须确保在将渲染器的DOM设置为原始值之前,将其设置为其他值。我们将其设置为一个空字符串,用Task.Delay给渲染器一些处理器时间来实际渲染组件,然后最终将其设置回原始设置。

这里有一个通用的数字组件,您可以扩展它,它已经具备了使其感觉像数字输入的基本功能,包括防止不允许输入的regex。

index剃刀:

@page "/"
<p>
<label>Integer Value</label>
<InputNumeric @bind-Value="ValueInteger" DecimalPlaces="0" />
<span>@ValueInteger</span>
</p>
<p>
<label>Decimal Value</label>
<InputNumeric @bind-Value="ValueDecimal" />
<span>@ValueDecimal</span>
</p>
@code {
int ValueInteger = 1;
decimal ValueDecimal = 1;
}

InputNumeric.razor

@inject IJSRuntime JSRuntime
@typeparam T
<input @ref="@ElementReference"
type="text"
value="@ValueString"
@onfocus="FocusEvent"
@onblur="FormatValue"
@oninput="ChangeEvent"
@onchange="ChangeEvent" />
@code {
[Parameter]
public EventCallback<T> ValueChanged { get; set; }
private T _value;
[Parameter]
public T Value
{
get => _value;
set
{
if (EqualityComparer<T>.Default.Equals(_value, value)) return;
_value = value;
ValueChanged.InvokeAsync(value);
}
}
[Parameter]
public int DecimalPlaces { get; set; } = 2;
[Parameter]
public decimal? Step { get; set; }
[Parameter]
public string DisplayFormat { get; set; }
private string ValueString { get; set; }
private ElementReference ElementReference { get; set; }
bool ignoreParameterSet = false;
protected override void OnParametersSet()
{
if (!ignoreParameterSet)
FormatValue();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await JSRuntime.InvokeVoidAsync("MyBlazor.SetNumericOnly", ElementReference, DecimalPlaces);
}
private void ChangeEvent(ChangeEventArgs e)
{
decimal decimalValue = CastToDecimal(e.Value);
Value = (T)Convert.ChangeType(decimalValue, typeof(T));
ValueChanged.InvokeAsync(Value);
}
private async void FocusEvent(FocusEventArgs e)
{
ignoreParameterSet = true;
ValueString = Value?.ToString();
await InvokeAsync(StateHasChanged);
}
private void FormatValue()
{
ignoreParameterSet = false;
ValueString = String.Format(DisplayFormat ?? $"{{0:N{DecimalPlaces}}}", Value);
StateHasChanged();
}
private decimal CastToDecimal(object value) {
decimal decimalValue;
var isDecimal = decimal.TryParse(value?.ToString(), out decimalValue);
if (isDecimal)
{
decimalValue = Math.Round(decimalValue, DecimalPlaces);
}
return decimalValue;
}
private async Task OnKeyDown(KeyboardEventArgs e)
{
if (e.Code == "ArrowDown" || e.Code == "ArrowUp")
{
var step = Step ?? (DecimalPlaces > 0 ? (decimal).25 : 1);
decimal decimalValue = CastToDecimal(Value);
if (e.Code == "ArrowDown")
decimalValue -= step;
if (e.Code == "ArrowUp")
decimalValue += step;
Value = (T)Convert.ChangeType(decimalValue, typeof(T));
await ValueChanged.InvokeAsync(Value);
ValueString = Value.ToString();
}
}
}

Javascript:

window.MyBlazor = {
SetNumericOnly: function (element, decimalPlaces) {
element.oninput = function (i, d) {
return function (e) {
var valueNegative = i.value.length > 0 ? i.value[0] == "-" : false;
i.value = (valueNegative ? "-" : "") + i.value.replace(/[^0-9.]/g, '').replace(/(..*?)..*/g, '$1').replace(/^0[^.]/, '0');
if (d === 0)
i.value = i.value.replace('.', '');
}
}(element, decimalPlaces);
}
}

重要提示:

这将破坏十进制分隔符不是.的区域性。你需要满足这个需求。

你可以试试这个:

@using Microsoft.AspNetCore.Components
<input type="number" value=@_value @oninput=OnInput />
<p>@_value</p>
@code {
private int oldValue = 50;
private int _value = 50;
private async Task OnInput(ChangeEventArgs args) {
int newValue;
if(int.TryParse((string)args.Value, out newValue)){
_value = newValue;
oldValue = _value;
}
else{
_value = 0;
await Task.Yield();
_value = oldValue;
}
}
}

诀窍是在await Task.Yield()周围的3行上,当字符错误时,强制重新输入。

还要注意,此解决方案不允许大于Int32.MaxValue或小于Int32.MinValue的数字。

编辑:

为了避免在瞬间在输入中显示值0,我们可以强制重新提交完整的输入。然而,在Mozilla Firefox中,每当写入禁止字符时,它就会失去输入的焦点。然而,它似乎将重点放在了Chrome上。

@using Microsoft.AspNetCore.Components
@if(rerenderTrick){
<input type="number" value=@_value/>
}
else{
<input type="number" value=@_value @oninput=OnInput />
}
<p>@_value</p>
@code {
private int _value = 50;
private bool rerenderTrick = false;
private async Task OnInput(ChangeEventArgs args) {
int newValue;
if(int.TryParse((string)args.Value, out newValue)){
_value = newValue;
}
else{
rerenderTrick = true;
await Task.Yield();
rerenderTrick = false;
}
}
}

这里是另一个javascript解决方案,由于其简单性,它很好。

@inherits InputText
<input
@bind=CurrentValueAsString
@bind:event="oninput"
@attributes=AdditionalAttributes
class=@CssClass
onkeypress="return (48 <= event.charCode && event.charCode <= 57) || event.charCode == 8"
onpaste="return false"
/>
@code {
}

将javascript内联似乎是安全的,因为在重新渲染时它总是包含在组件中。

使用这种方法时没有任何视觉故障,我尝试过的所有c#解决方案都是这样。

最新更新